Compare commits
1 Commits
59e9203363
...
silicone
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e75d55e0fb |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -25,12 +25,6 @@ vite.config.ts.timestamp-*
|
|||||||
/src-tauri/target/
|
/src-tauri/target/
|
||||||
/src-tauri/target-codex-check*/
|
/src-tauri/target-codex-check*/
|
||||||
/src-tauri/gen/schemas/
|
/src-tauri/gen/schemas/
|
||||||
/src-tauri/gen/android/app/build/
|
|
||||||
/src-tauri/gen/android/buildSrc/build/
|
|
||||||
/src-tauri/gen/android/.gradle/
|
|
||||||
/src-tauri/gen/android/app/.gradle/
|
|
||||||
/src-tauri/gen/android/buildSrc/.gradle/
|
|
||||||
/src-tauri/gen/android/build/reports/
|
|
||||||
|
|
||||||
/src-tauri/program.log*
|
/src-tauri/program.log*
|
||||||
/src-tauri/recording_replay_debug_*.csv
|
/src-tauri/recording_replay_debug_*.csv
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "eskin-finger-sdk"]
|
|
||||||
path = eskin-finger-sdk
|
|
||||||
url = https://gitea.e-skin.top/yanjie/eskin-finger-sdk.git
|
|
||||||
Submodule eskin-finger-sdk deleted from 705375085f
2
package-lock.json
generated
2
package-lock.json
generated
@@ -6,7 +6,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "JE-Skin",
|
"name": "JE-Skin",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
|
|
||||||
-keep class com.lenn.tauri_serial.TauriActivity {
|
|
||||||
public app.tauri.plugin.PluginManager getPluginManager();
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"$schema":"https://schema.tauri.app/config/2","productName":"JE-Skin","version":"0.4.0","identifier":"com.lenn.tauri-serial","app":{"windows":[{"label":"main","create":true,"url":"index.html","dragDropEnabled":true,"center":false,"width":1366.0,"height":860.0,"resizable":true,"maximizable":true,"minimizable":true,"closable":true,"title":"JE-Skin","fullscreen":false,"focus":true,"focusable":true,"transparent":false,"maximized":false,"visible":true,"decorations":false,"alwaysOnBottom":false,"alwaysOnTop":false,"visibleOnAllWorkspaces":false,"contentProtected":false,"skipTaskbar":false,"titleBarStyle":"Visible","hiddenTitle":false,"acceptFirstMouse":false,"shadow":true,"incognito":false,"zoomHotkeysEnabled":false,"browserExtensionsEnabled":false,"useHttpsScheme":false,"javascriptDisabled":false,"allowLinkPreview":true,"disableInputAccessoryView":false,"scrollBarStyle":"default"}],"security":{"freezePrototype":false,"dangerousDisableAssetCspModification":false,"assetProtocol":{"scope":[],"enable":false},"pattern":{"use":"brownfield"},"capabilities":[]},"macOSPrivateApi":false,"withGlobalTauri":false,"enableGTKAppId":false},"build":{"devUrl":"http://localhost:1420/","frontendDist":"../build","beforeDevCommand":"npm run dev","beforeBuildCommand":"npm run build","removeUnusedCommands":false,"additionalWatchFolders":[]},"bundle":{"active":true,"targets":"all","createUpdaterArtifacts":true,"icon":["icons/32x32.png","icons/128x128.png","icons/128x128@2x.png","icons/icon.icns","icons/icon.ico"],"resources":["resources/je-skin-devkit-server.exe"],"useLocalToolsDir":false,"windows":{"digestAlgorithm":null,"certificateThumbprint":null,"timestampUrl":null,"tsp":false,"webviewInstallMode":{"type":"downloadBootstrapper","silent":true},"allowDowngrades":true,"wix":null,"nsis":{"template":"nsis/installer.nsi","headerImage":null,"sidebarImage":null,"installerIcon":"icons/icon.ico","installMode":"both","languages":null,"customLanguageFiles":null,"displayLanguageSelector":false,"compression":"lzma","startMenuFolder":null,"installerHooks":null,"minimumWebview2Version":null},"signCommand":null},"linux":{"appimage":{"bundleMediaFramework":false,"files":{}},"deb":{"files":{}},"rpm":{"release":"1","epoch":0,"files":{}}},"macOS":{"files":{},"minimumSystemVersion":"10.13","hardenedRuntime":true,"dmg":{"windowSize":{"width":660,"height":400},"appPosition":{"x":180,"y":170},"applicationFolderPosition":{"x":480,"y":170}}},"iOS":{"minimumSystemVersion":"14.0"},"android":{"minSdkVersion":24,"autoIncrementVersionCode":false}},"plugins":{}}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
|
||||||
|
|
||||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
@file:Suppress("unused")
|
|
||||||
|
|
||||||
package com.lenn.tauri_serial
|
|
||||||
|
|
||||||
import android.webkit.*
|
|
||||||
|
|
||||||
class Ipc(val webViewClient: RustWebViewClient) {
|
|
||||||
@JavascriptInterface
|
|
||||||
fun postMessage(message: String?) {
|
|
||||||
message?.let {m ->
|
|
||||||
// we're not using WebView::getUrl() here because it needs to be executed on the main thread
|
|
||||||
// and it would slow down the Ipc
|
|
||||||
// so instead we track the current URL on the webview client
|
|
||||||
this.ipc(webViewClient.currentUrl, m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
init {
|
|
||||||
System.loadLibrary("tauri_demo_lib")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private external fun ipc(url: String, message: String)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
|
||||||
|
|
||||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
|
||||||
|
|
||||||
package com.lenn.tauri_serial
|
|
||||||
|
|
||||||
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java
|
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
|
|
||||||
class Logger {
|
|
||||||
companion object {
|
|
||||||
private const val LOG_TAG_CORE = "Tauri"
|
|
||||||
|
|
||||||
fun tags(vararg subtags: String): String {
|
|
||||||
return if (subtags.isNotEmpty()) {
|
|
||||||
LOG_TAG_CORE + "/" + TextUtils.join("/", subtags)
|
|
||||||
} else LOG_TAG_CORE
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verbose(message: String) {
|
|
||||||
verbose(LOG_TAG_CORE, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verbose(tag: String, message: String) {
|
|
||||||
if (!shouldLog()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Log.v(tag, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun debug(message: String) {
|
|
||||||
debug(LOG_TAG_CORE, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun debug(tag: String, message: String) {
|
|
||||||
if (!shouldLog()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Log.d(tag, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun info(message: String) {
|
|
||||||
info(LOG_TAG_CORE, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun info(tag: String, message: String) {
|
|
||||||
if (!shouldLog()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Log.i(tag, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun warn(message: String) {
|
|
||||||
warn(LOG_TAG_CORE, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun warn(tag: String, message: String) {
|
|
||||||
if (!shouldLog()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Log.w(tag, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun error(message: String) {
|
|
||||||
error(LOG_TAG_CORE, message, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun error(message: String, e: Throwable?) {
|
|
||||||
error(LOG_TAG_CORE, message, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun error(tag: String, message: String, e: Throwable?) {
|
|
||||||
if (!shouldLog()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Log.e(tag, message, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldLog(): Boolean {
|
|
||||||
return BuildConfig.DEBUG
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
|
||||||
|
|
||||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package com.lenn.tauri_serial
|
|
||||||
|
|
||||||
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import java.util.ArrayList
|
|
||||||
|
|
||||||
object PermissionHelper {
|
|
||||||
/**
|
|
||||||
* Checks if a list of given permissions are all granted by the user
|
|
||||||
*
|
|
||||||
* @param permissions Permissions to check.
|
|
||||||
* @return True if all permissions are granted, false if at least one is not.
|
|
||||||
*/
|
|
||||||
fun hasPermissions(context: Context?, permissions: Array<String>): Boolean {
|
|
||||||
for (perm in permissions) {
|
|
||||||
if (ActivityCompat.checkSelfPermission(
|
|
||||||
context!!,
|
|
||||||
perm
|
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether the given permission has been defined in the AndroidManifest.xml
|
|
||||||
*
|
|
||||||
* @param permission A permission to check.
|
|
||||||
* @return True if the permission has been defined in the Manifest, false if not.
|
|
||||||
*/
|
|
||||||
fun hasDefinedPermission(context: Context, permission: String): Boolean {
|
|
||||||
var hasPermission = false
|
|
||||||
val requestedPermissions = getManifestPermissions(context)
|
|
||||||
if (!requestedPermissions.isNullOrEmpty()) {
|
|
||||||
val requestedPermissionsList = listOf(*requestedPermissions)
|
|
||||||
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
|
|
||||||
if (requestedPermissionsArrayList.contains(permission)) {
|
|
||||||
hasPermission = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasPermission
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether all of the given permissions have been defined in the AndroidManifest.xml
|
|
||||||
* @param context the app context
|
|
||||||
* @param permissions a list of permissions
|
|
||||||
* @return true only if all permissions are defined in the AndroidManifest.xml
|
|
||||||
*/
|
|
||||||
fun hasDefinedPermissions(context: Context, permissions: Array<String>): Boolean {
|
|
||||||
for (permission in permissions) {
|
|
||||||
if (!hasDefinedPermission(context, permission)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the permissions defined in AndroidManifest.xml
|
|
||||||
*
|
|
||||||
* @return The permissions defined in AndroidManifest.xml
|
|
||||||
*/
|
|
||||||
private fun getManifestPermissions(context: Context): Array<String>? {
|
|
||||||
var requestedPermissions: Array<String>? = null
|
|
||||||
try {
|
|
||||||
val pm = context.packageManager
|
|
||||||
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
|
|
||||||
}
|
|
||||||
if (packageInfo != null) {
|
|
||||||
requestedPermissions = packageInfo.requestedPermissions
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
|
||||||
return requestedPermissions
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml
|
|
||||||
*
|
|
||||||
* @param neededPermissions The permissions needed.
|
|
||||||
* @return The permissions not present in AndroidManifest.xml
|
|
||||||
*/
|
|
||||||
fun getUndefinedPermissions(context: Context, neededPermissions: Array<String?>): Array<String?> {
|
|
||||||
val undefinedPermissions = ArrayList<String?>()
|
|
||||||
val requestedPermissions = getManifestPermissions(context)
|
|
||||||
if (!requestedPermissions.isNullOrEmpty()) {
|
|
||||||
val requestedPermissionsList = listOf(*requestedPermissions)
|
|
||||||
val requestedPermissionsArrayList = ArrayList(requestedPermissionsList)
|
|
||||||
for (permission in neededPermissions) {
|
|
||||||
if (!requestedPermissionsArrayList.contains(permission)) {
|
|
||||||
undefinedPermissions.add(permission)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var undefinedPermissionArray = arrayOfNulls<String>(undefinedPermissions.size)
|
|
||||||
undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray)
|
|
||||||
return undefinedPermissionArray
|
|
||||||
}
|
|
||||||
return neededPermissions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,495 +0,0 @@
|
|||||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
|
||||||
|
|
||||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
@file:Suppress("ObsoleteSdkInt", "RedundantOverride", "QueryPermissionsNeeded", "SimpleDateFormat")
|
|
||||||
|
|
||||||
package com.lenn.tauri_serial
|
|
||||||
|
|
||||||
// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.view.View
|
|
||||||
import android.webkit.*
|
|
||||||
import android.widget.EditText
|
|
||||||
import androidx.activity.result.ActivityResult
|
|
||||||
import androidx.activity.result.ActivityResultCallback
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() {
|
|
||||||
private interface PermissionListener {
|
|
||||||
fun onPermissionSelect(isGranted: Boolean?)
|
|
||||||
}
|
|
||||||
|
|
||||||
private interface ActivityResultListener {
|
|
||||||
fun onActivityResult(result: ActivityResult?)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val activity: WryActivity
|
|
||||||
private var permissionLauncher: ActivityResultLauncher<Array<String>>
|
|
||||||
private var activityLauncher: ActivityResultLauncher<Intent>
|
|
||||||
private var permissionListener: PermissionListener? = null
|
|
||||||
private var activityListener: ActivityResultListener? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
activity = appActivity
|
|
||||||
val permissionCallback =
|
|
||||||
ActivityResultCallback { isGranted: Map<String, Boolean> ->
|
|
||||||
if (permissionListener != null) {
|
|
||||||
var granted = true
|
|
||||||
for ((_, value) in isGranted) {
|
|
||||||
if (!value) granted = false
|
|
||||||
}
|
|
||||||
permissionListener!!.onPermissionSelect(granted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
permissionLauncher =
|
|
||||||
activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(), permissionCallback)
|
|
||||||
activityLauncher = activity.registerForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult()
|
|
||||||
) { result ->
|
|
||||||
if (activityListener != null) {
|
|
||||||
activityListener!!.onActivityResult(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render web content in `view`.
|
|
||||||
*
|
|
||||||
* Both this method and [.onHideCustomView] are required for
|
|
||||||
* rendering web content in full screen.
|
|
||||||
*
|
|
||||||
* @see [](https://developer.android.com/reference/android/webkit/WebChromeClient.onShowCustomView
|
|
||||||
) */
|
|
||||||
override fun onShowCustomView(view: View, callback: CustomViewCallback) {
|
|
||||||
callback.onCustomViewHidden()
|
|
||||||
super.onShowCustomView(view, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render web content in the original Web View again.
|
|
||||||
*
|
|
||||||
* Do not remove this method--@see #onShowCustomView(View, CustomViewCallback).
|
|
||||||
*/
|
|
||||||
override fun onHideCustomView() {
|
|
||||||
super.onHideCustomView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPermissionRequest(request: PermissionRequest) {
|
|
||||||
val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
|
||||||
val permissionList: MutableList<String> = ArrayList()
|
|
||||||
if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) {
|
|
||||||
permissionList.add(Manifest.permission.CAMERA)
|
|
||||||
}
|
|
||||||
if (listOf(*request.resources).contains("android.webkit.resource.AUDIO_CAPTURE")) {
|
|
||||||
permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS)
|
|
||||||
permissionList.add(Manifest.permission.RECORD_AUDIO)
|
|
||||||
}
|
|
||||||
if (permissionList.isNotEmpty() && isRequestPermissionRequired) {
|
|
||||||
val permissions = permissionList.toTypedArray()
|
|
||||||
permissionListener = object : PermissionListener {
|
|
||||||
override fun onPermissionSelect(isGranted: Boolean?) {
|
|
||||||
if (isGranted == true) {
|
|
||||||
request.grant(request.resources)
|
|
||||||
} else {
|
|
||||||
request.deny()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
permissionLauncher.launch(permissions)
|
|
||||||
} else {
|
|
||||||
request.grant(request.resources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the browser alert modal
|
|
||||||
* @param view
|
|
||||||
* @param url
|
|
||||||
* @param message
|
|
||||||
* @param result
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean {
|
|
||||||
if (activity.isFinishing) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val builder = AlertDialog.Builder(view.context)
|
|
||||||
builder
|
|
||||||
.setMessage(message)
|
|
||||||
.setPositiveButton(
|
|
||||||
"OK"
|
|
||||||
) { dialog: DialogInterface, _: Int ->
|
|
||||||
dialog.dismiss()
|
|
||||||
result.confirm()
|
|
||||||
}
|
|
||||||
.setOnCancelListener { dialog: DialogInterface ->
|
|
||||||
dialog.dismiss()
|
|
||||||
result.cancel()
|
|
||||||
}
|
|
||||||
val dialog = builder.create()
|
|
||||||
dialog.show()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the browser confirm modal
|
|
||||||
* @param view
|
|
||||||
* @param url
|
|
||||||
* @param message
|
|
||||||
* @param result
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
override fun onJsConfirm(view: WebView, url: String, message: String, result: JsResult): Boolean {
|
|
||||||
if (activity.isFinishing) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val builder = AlertDialog.Builder(view.context)
|
|
||||||
builder
|
|
||||||
.setMessage(message)
|
|
||||||
.setPositiveButton(
|
|
||||||
"OK"
|
|
||||||
) { dialog: DialogInterface, _: Int ->
|
|
||||||
dialog.dismiss()
|
|
||||||
result.confirm()
|
|
||||||
}
|
|
||||||
.setNegativeButton(
|
|
||||||
"Cancel"
|
|
||||||
) { dialog: DialogInterface, _: Int ->
|
|
||||||
dialog.dismiss()
|
|
||||||
result.cancel()
|
|
||||||
}
|
|
||||||
.setOnCancelListener { dialog: DialogInterface ->
|
|
||||||
dialog.dismiss()
|
|
||||||
result.cancel()
|
|
||||||
}
|
|
||||||
val dialog = builder.create()
|
|
||||||
dialog.show()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the browser prompt modal
|
|
||||||
* @param view
|
|
||||||
* @param url
|
|
||||||
* @param message
|
|
||||||
* @param defaultValue
|
|
||||||
* @param result
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
override fun onJsPrompt(
|
|
||||||
view: WebView,
|
|
||||||
url: String,
|
|
||||||
message: String,
|
|
||||||
defaultValue: String,
|
|
||||||
result: JsPromptResult
|
|
||||||
): Boolean {
|
|
||||||
if (activity.isFinishing) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val builder = AlertDialog.Builder(view.context)
|
|
||||||
val input = EditText(view.context)
|
|
||||||
builder
|
|
||||||
.setMessage(message)
|
|
||||||
.setView(input)
|
|
||||||
.setPositiveButton(
|
|
||||||
"OK"
|
|
||||||
) { dialog: DialogInterface, _: Int ->
|
|
||||||
dialog.dismiss()
|
|
||||||
val inputText1 = input.text.toString().trim { it <= ' ' }
|
|
||||||
result.confirm(inputText1)
|
|
||||||
}
|
|
||||||
.setNegativeButton(
|
|
||||||
"Cancel"
|
|
||||||
) { dialog: DialogInterface, _: Int ->
|
|
||||||
dialog.dismiss()
|
|
||||||
result.cancel()
|
|
||||||
}
|
|
||||||
.setOnCancelListener { dialog: DialogInterface ->
|
|
||||||
dialog.dismiss()
|
|
||||||
result.cancel()
|
|
||||||
}
|
|
||||||
val dialog = builder.create()
|
|
||||||
dialog.show()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the browser geolocation permission prompt
|
|
||||||
* @param origin
|
|
||||||
* @param callback
|
|
||||||
*/
|
|
||||||
override fun onGeolocationPermissionsShowPrompt(
|
|
||||||
origin: String,
|
|
||||||
callback: GeolocationPermissions.Callback
|
|
||||||
) {
|
|
||||||
super.onGeolocationPermissionsShowPrompt(origin, callback)
|
|
||||||
Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: $origin")
|
|
||||||
val geoPermissions =
|
|
||||||
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
|
|
||||||
if (!PermissionHelper.hasPermissions(activity, geoPermissions)) {
|
|
||||||
permissionListener = object : PermissionListener {
|
|
||||||
override fun onPermissionSelect(isGranted: Boolean?) {
|
|
||||||
if (isGranted == true) {
|
|
||||||
callback.invoke(origin, true, false)
|
|
||||||
} else {
|
|
||||||
val coarsePermission =
|
|
||||||
arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
|
||||||
PermissionHelper.hasPermissions(activity, coarsePermission)
|
|
||||||
) {
|
|
||||||
callback.invoke(origin, true, false)
|
|
||||||
} else {
|
|
||||||
callback.invoke(origin, false, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
permissionLauncher.launch(geoPermissions)
|
|
||||||
} else {
|
|
||||||
// permission is already granted
|
|
||||||
callback.invoke(origin, true, false)
|
|
||||||
Logger.debug("onGeolocationPermissionsShowPrompt: has required permission")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onShowFileChooser(
|
|
||||||
webView: WebView,
|
|
||||||
filePathCallback: ValueCallback<Array<Uri?>?>,
|
|
||||||
fileChooserParams: FileChooserParams
|
|
||||||
): Boolean {
|
|
||||||
val acceptTypes = listOf(*fileChooserParams.acceptTypes)
|
|
||||||
val captureEnabled = fileChooserParams.isCaptureEnabled
|
|
||||||
val capturePhoto = captureEnabled && acceptTypes.contains("image/*")
|
|
||||||
val captureVideo = captureEnabled && acceptTypes.contains("video/*")
|
|
||||||
if (capturePhoto || captureVideo) {
|
|
||||||
if (isMediaCaptureSupported) {
|
|
||||||
showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo)
|
|
||||||
} else {
|
|
||||||
permissionListener = object : PermissionListener {
|
|
||||||
override fun onPermissionSelect(isGranted: Boolean?) {
|
|
||||||
if (isGranted == true) {
|
|
||||||
showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo)
|
|
||||||
} else {
|
|
||||||
Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted")
|
|
||||||
filePathCallback.onReceiveValue(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val camPermission = arrayOf(Manifest.permission.CAMERA)
|
|
||||||
permissionLauncher.launch(camPermission)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showFilePicker(filePathCallback, fileChooserParams)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val isMediaCaptureSupported: Boolean
|
|
||||||
get() {
|
|
||||||
val permissions = arrayOf(Manifest.permission.CAMERA)
|
|
||||||
return PermissionHelper.hasPermissions(activity, permissions) ||
|
|
||||||
!PermissionHelper.hasDefinedPermission(activity, Manifest.permission.CAMERA)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showMediaCaptureOrFilePicker(
|
|
||||||
filePathCallback: ValueCallback<Array<Uri?>?>,
|
|
||||||
fileChooserParams: FileChooserParams,
|
|
||||||
isVideo: Boolean
|
|
||||||
) {
|
|
||||||
val isVideoCaptureSupported = true
|
|
||||||
val shown = if (isVideo && isVideoCaptureSupported) {
|
|
||||||
showVideoCapturePicker(filePathCallback)
|
|
||||||
} else {
|
|
||||||
showImageCapturePicker(filePathCallback)
|
|
||||||
}
|
|
||||||
if (!shown) {
|
|
||||||
Logger.warn(
|
|
||||||
Logger.tags("FileChooser"),
|
|
||||||
"Media capture intent could not be launched. Falling back to default file picker."
|
|
||||||
)
|
|
||||||
showFilePicker(filePathCallback, fileChooserParams)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showImageCapturePicker(filePathCallback: ValueCallback<Array<Uri?>?>): Boolean {
|
|
||||||
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
|
||||||
if (takePictureIntent.resolveActivity(activity.packageManager) == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val imageFileUri: Uri = try {
|
|
||||||
createImageFileUri()
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Logger.error("Unable to create temporary media capture file: " + ex.message)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri)
|
|
||||||
activityListener = object : ActivityResultListener {
|
|
||||||
override fun onActivityResult(result: ActivityResult?) {
|
|
||||||
var res: Array<Uri?>? = null
|
|
||||||
if (result?.resultCode == Activity.RESULT_OK) {
|
|
||||||
res = arrayOf(imageFileUri)
|
|
||||||
}
|
|
||||||
filePathCallback.onReceiveValue(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activityLauncher.launch(takePictureIntent)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showVideoCapturePicker(filePathCallback: ValueCallback<Array<Uri?>?>): Boolean {
|
|
||||||
val takeVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
|
|
||||||
if (takeVideoIntent.resolveActivity(activity.packageManager) == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
activityListener = object : ActivityResultListener {
|
|
||||||
override fun onActivityResult(result: ActivityResult?) {
|
|
||||||
var res: Array<Uri?>? = null
|
|
||||||
if (result?.resultCode == Activity.RESULT_OK) {
|
|
||||||
res = arrayOf(result.data!!.data)
|
|
||||||
}
|
|
||||||
filePathCallback.onReceiveValue(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activityLauncher.launch(takeVideoIntent)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showFilePicker(
|
|
||||||
filePathCallback: ValueCallback<Array<Uri?>?>,
|
|
||||||
fileChooserParams: FileChooserParams
|
|
||||||
) {
|
|
||||||
val intent = fileChooserParams.createIntent()
|
|
||||||
if (fileChooserParams.mode == FileChooserParams.MODE_OPEN_MULTIPLE) {
|
|
||||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
|
||||||
}
|
|
||||||
if (fileChooserParams.acceptTypes.size > 1 || intent.type!!.startsWith(".")) {
|
|
||||||
val validTypes = getValidTypes(fileChooserParams.acceptTypes)
|
|
||||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, validTypes)
|
|
||||||
if (intent.type!!.startsWith(".")) {
|
|
||||||
intent.type = validTypes[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
activityListener = object : ActivityResultListener {
|
|
||||||
override fun onActivityResult(result: ActivityResult?) {
|
|
||||||
val res: Array<Uri?>?
|
|
||||||
val resultIntent = result?.data
|
|
||||||
if (result?.resultCode == Activity.RESULT_OK && resultIntent!!.clipData != null) {
|
|
||||||
val numFiles = resultIntent.clipData!!.itemCount
|
|
||||||
res = arrayOfNulls(numFiles)
|
|
||||||
for (i in 0 until numFiles) {
|
|
||||||
res[i] = resultIntent.clipData!!.getItemAt(i).uri
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res = FileChooserParams.parseResult(
|
|
||||||
result?.resultCode ?: 0,
|
|
||||||
resultIntent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
filePathCallback.onReceiveValue(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activityLauncher.launch(intent)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
filePathCallback.onReceiveValue(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getValidTypes(currentTypes: Array<String>): Array<String> {
|
|
||||||
val validTypes: MutableList<String> = ArrayList()
|
|
||||||
val mtm = MimeTypeMap.getSingleton()
|
|
||||||
for (mime in currentTypes) {
|
|
||||||
if (mime.startsWith(".")) {
|
|
||||||
val extension = mime.substring(1)
|
|
||||||
val extensionMime = mtm.getMimeTypeFromExtension(extension)
|
|
||||||
if (extensionMime != null && !validTypes.contains(extensionMime)) {
|
|
||||||
validTypes.add(extensionMime)
|
|
||||||
}
|
|
||||||
} else if (!validTypes.contains(mime)) {
|
|
||||||
validTypes.add(mime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val validObj: Array<Any> = validTypes.toTypedArray()
|
|
||||||
return Arrays.copyOf(
|
|
||||||
validObj, validObj.size,
|
|
||||||
Array<String>::class.java
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
|
||||||
val tag: String = Logger.tags("Console")
|
|
||||||
if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) {
|
|
||||||
val msg = String.format(
|
|
||||||
"File: %s - Line %d - Msg: %s",
|
|
||||||
consoleMessage.sourceId(),
|
|
||||||
consoleMessage.lineNumber(),
|
|
||||||
consoleMessage.message()
|
|
||||||
)
|
|
||||||
val level = consoleMessage.messageLevel().name
|
|
||||||
if ("ERROR".equals(level, ignoreCase = true)) {
|
|
||||||
Logger.error(tag, msg, null)
|
|
||||||
} else if ("WARNING".equals(level, ignoreCase = true)) {
|
|
||||||
Logger.warn(tag, msg)
|
|
||||||
} else if ("TIP".equals(level, ignoreCase = true)) {
|
|
||||||
Logger.debug(tag, msg)
|
|
||||||
} else {
|
|
||||||
Logger.info(tag, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isValidMsg(msg: String): Boolean {
|
|
||||||
return !(msg.contains("%cresult %c") ||
|
|
||||||
msg.contains("%cnative %c") ||
|
|
||||||
msg.equals("[object Object]", ignoreCase = true) ||
|
|
||||||
msg.equals("console.groupEnd", ignoreCase = true))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun createImageFileUri(): Uri {
|
|
||||||
val photoFile = createImageFile(activity)
|
|
||||||
return FileProvider.getUriForFile(
|
|
||||||
activity,
|
|
||||||
activity.packageName.toString() + ".fileprovider",
|
|
||||||
photoFile
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun createImageFile(activity: Activity): File {
|
|
||||||
// Create an image file name
|
|
||||||
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
|
|
||||||
val imageFileName = "JPEG_" + timeStamp + "_"
|
|
||||||
val storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
|
||||||
return File.createTempFile(imageFileName, ".jpg", storageDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceivedTitle(
|
|
||||||
view: WebView,
|
|
||||||
title: String
|
|
||||||
) {
|
|
||||||
handleReceivedTitle(view, title)
|
|
||||||
}
|
|
||||||
|
|
||||||
private external fun handleReceivedTitle(webview: WebView, title: String)
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
|
||||||
|
|
||||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
@file:Suppress("unused", "SetJavaScriptEnabled")
|
|
||||||
|
|
||||||
package com.lenn.tauri_serial
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.webkit.*
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.webkit.WebViewCompat
|
|
||||||
import androidx.webkit.WebViewFeature
|
|
||||||
import kotlin.collections.Map
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
class RustWebView(context: Context, val initScripts: Array<String>, val id: String): WebView(context) {
|
|
||||||
val isDocumentStartScriptEnabled: Boolean
|
|
||||||
|
|
||||||
init {
|
|
||||||
settings.javaScriptEnabled = true
|
|
||||||
settings.domStorageEnabled = true
|
|
||||||
settings.setGeolocationEnabled(true)
|
|
||||||
settings.databaseEnabled = true
|
|
||||||
settings.mediaPlaybackRequiresUserGesture = false
|
|
||||||
settings.javaScriptCanOpenWindowsAutomatically = true
|
|
||||||
|
|
||||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
|
|
||||||
isDocumentStartScriptEnabled = true
|
|
||||||
for (script in initScripts) {
|
|
||||||
WebViewCompat.addDocumentStartJavaScript(this, script, setOf("*"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isDocumentStartScriptEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadUrlMainThread(url: String) {
|
|
||||||
post {
|
|
||||||
loadUrl(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadUrlMainThread(url: String, additionalHttpHeaders: Map<String, String>) {
|
|
||||||
post {
|
|
||||||
loadUrl(url, additionalHttpHeaders)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadUrl(url: String) {
|
|
||||||
if (!shouldOverride(url)) {
|
|
||||||
super.loadUrl(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadUrl(url: String, additionalHttpHeaders: Map<String, String>) {
|
|
||||||
if (!shouldOverride(url)) {
|
|
||||||
super.loadUrl(url, additionalHttpHeaders);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadHTMLMainThread(html: String) {
|
|
||||||
post {
|
|
||||||
super.loadData(html, "text/html", null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun evalScript(id: Int, script: String) {
|
|
||||||
post {
|
|
||||||
super.evaluateJavascript(script) { result ->
|
|
||||||
onEval(id, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearAllBrowsingData() {
|
|
||||||
try {
|
|
||||||
super.getContext().deleteDatabase("webviewCache.db")
|
|
||||||
super.getContext().deleteDatabase("webview.db")
|
|
||||||
super.clearCache(true)
|
|
||||||
super.clearHistory()
|
|
||||||
super.clearFormData()
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Logger.error("Unable to create temporary media capture file: " + ex.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCookies(url: String): String {
|
|
||||||
val cookieManager = CookieManager.getInstance()
|
|
||||||
return cookieManager.getCookie(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
private external fun shouldOverride(url: String): Boolean
|
|
||||||
private external fun onEval(id: Int, result: String)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
|
||||||
|
|
||||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package com.lenn.tauri_serial
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.webkit.*
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import androidx.webkit.WebViewAssetLoader
|
|
||||||
|
|
||||||
class RustWebViewClient(context: Context): WebViewClient() {
|
|
||||||
private val interceptedState = mutableMapOf<String, Boolean>()
|
|
||||||
var currentUrl: String = "about:blank"
|
|
||||||
private var lastInterceptedUrl: Uri? = null
|
|
||||||
private var pendingUrlRedirect: String? = null
|
|
||||||
|
|
||||||
private val assetLoader = WebViewAssetLoader.Builder()
|
|
||||||
.setDomain(assetLoaderDomain())
|
|
||||||
.addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(context))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun shouldInterceptRequest(
|
|
||||||
view: WebView,
|
|
||||||
request: WebResourceRequest
|
|
||||||
): WebResourceResponse? {
|
|
||||||
pendingUrlRedirect?.let {
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
view.loadUrl(it)
|
|
||||||
}
|
|
||||||
pendingUrlRedirect = null
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
lastInterceptedUrl = request.url
|
|
||||||
return if (withAssetLoader()) {
|
|
||||||
assetLoader.shouldInterceptRequest(request.url)
|
|
||||||
} else {
|
|
||||||
val rustWebview = view as RustWebView;
|
|
||||||
val response = handleRequest(rustWebview.id, request, rustWebview.isDocumentStartScriptEnabled)
|
|
||||||
interceptedState[request.url.toString()] = response != null
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shouldOverrideUrlLoading(
|
|
||||||
view: WebView,
|
|
||||||
request: WebResourceRequest
|
|
||||||
): Boolean {
|
|
||||||
return shouldOverride(request.url.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
|
||||||
currentUrl = url
|
|
||||||
if (interceptedState[url] == false) {
|
|
||||||
val webView = view as RustWebView
|
|
||||||
for (script in webView.initScripts) {
|
|
||||||
view.evaluateJavascript(script, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return onPageLoading(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageFinished(view: WebView, url: String) {
|
|
||||||
onPageLoaded(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceivedError(
|
|
||||||
view: WebView,
|
|
||||||
request: WebResourceRequest,
|
|
||||||
error: WebResourceError
|
|
||||||
) {
|
|
||||||
// we get a net::ERR_CONNECTION_REFUSED when an external URL redirects to a custom protocol
|
|
||||||
// e.g. oauth flow, because shouldInterceptRequest is not called on redirects
|
|
||||||
// so we must force retry here with loadUrl() to get a chance of the custom protocol to kick in
|
|
||||||
if (error.errorCode == ERROR_CONNECT && request.isForMainFrame && request.url != lastInterceptedUrl) {
|
|
||||||
// prevent the default error page from showing
|
|
||||||
view.stopLoading()
|
|
||||||
// without this initial loadUrl the app is stuck
|
|
||||||
view.loadUrl(request.url.toString())
|
|
||||||
// ensure the URL is actually loaded - for some reason there's a race condition and we need to call loadUrl() again later
|
|
||||||
pendingUrlRedirect = request.url.toString()
|
|
||||||
} else {
|
|
||||||
super.onReceivedError(view, request, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
init {
|
|
||||||
System.loadLibrary("tauri_demo_lib")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private external fun assetLoaderDomain(): String
|
|
||||||
private external fun withAssetLoader(): Boolean
|
|
||||||
private external fun handleRequest(webviewId: String, request: WebResourceRequest, isDocumentStartScriptEnabled: Boolean): WebResourceResponse?
|
|
||||||
private external fun shouldOverride(url: String): Boolean
|
|
||||||
private external fun onPageLoading(url: String)
|
|
||||||
private external fun onPageLoaded(url: String)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
|
||||||
|
|
||||||
package com.lenn.tauri_serial
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import app.tauri.plugin.PluginManager
|
|
||||||
|
|
||||||
abstract class TauriActivity : WryActivity() {
|
|
||||||
var pluginManager: PluginManager = PluginManager(this)
|
|
||||||
override val handleBackNavigation: Boolean = false
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
|
||||||
super.onNewIntent(intent)
|
|
||||||
pluginManager.onNewIntent(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
pluginManager.onResume()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
pluginManager.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRestart() {
|
|
||||||
super.onRestart()
|
|
||||||
pluginManager.onRestart()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
pluginManager.onStop()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
pluginManager.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
||||||
super.onConfigurationChanged(newConfig)
|
|
||||||
pluginManager.onConfigurationChanged(newConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */
|
|
||||||
|
|
||||||
// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package com.lenn.tauri_serial
|
|
||||||
|
|
||||||
import com.lenn.tauri_serial.RustWebView
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.webkit.WebView
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
|
|
||||||
abstract class WryActivity : AppCompatActivity() {
|
|
||||||
private lateinit var mWebView: RustWebView
|
|
||||||
open val handleBackNavigation: Boolean = true
|
|
||||||
|
|
||||||
open fun onWebViewCreate(webView: WebView) { }
|
|
||||||
|
|
||||||
fun setWebView(webView: RustWebView) {
|
|
||||||
mWebView = webView
|
|
||||||
|
|
||||||
if (handleBackNavigation) {
|
|
||||||
val callback = object : OnBackPressedCallback(true) {
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
if (this@WryActivity.mWebView.canGoBack()) {
|
|
||||||
this@WryActivity.mWebView.goBack()
|
|
||||||
} else {
|
|
||||||
this.isEnabled = false
|
|
||||||
this@WryActivity.onBackPressed()
|
|
||||||
this.isEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onBackPressedDispatcher.addCallback(this, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
onWebViewCreate(webView)
|
|
||||||
}
|
|
||||||
|
|
||||||
val version: String
|
|
||||||
@SuppressLint("WebViewApiAvailability", "ObsoleteSdkInt")
|
|
||||||
get() {
|
|
||||||
// Check getCurrentWebViewPackage() directly if above Android 8
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
return WebView.getCurrentWebViewPackage()?.versionName ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise manually check WebView versions
|
|
||||||
var webViewPackage = "com.google.android.webview"
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
webViewPackage = "com.android.chrome"
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val info = packageManager.getPackageInfo(webViewPackage, 0)
|
|
||||||
return info.versionName.toString()
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Logger.warn("Unable to get package info for '$webViewPackage'$ex")
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val info = packageManager.getPackageInfo("com.android.webview", 0)
|
|
||||||
return info.versionName.toString()
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Logger.warn("Unable to get package info for 'com.android.webview'$ex")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Could not detect any webview, return empty string
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
create(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
|
||||||
super.onWindowFocusChanged(hasFocus)
|
|
||||||
focus(hasFocus)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
destroy()
|
|
||||||
onActivityDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLowMemory() {
|
|
||||||
super.onLowMemory()
|
|
||||||
memory()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAppClass(name: String): Class<*> {
|
|
||||||
return Class.forName(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
init {
|
|
||||||
System.loadLibrary("tauri_demo_lib")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private external fun create(activity: WryActivity)
|
|
||||||
private external fun start()
|
|
||||||
private external fun resume()
|
|
||||||
private external fun pause()
|
|
||||||
private external fun stop()
|
|
||||||
private external fun save()
|
|
||||||
private external fun destroy()
|
|
||||||
private external fun onActivityDestroy()
|
|
||||||
private external fun memory()
|
|
||||||
private external fun focus(focus: Boolean)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!!
|
|
||||||
|
|
||||||
# Copyright 2020-2023 Tauri Programme within The Commons Conservancy
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
-keep class com.lenn.tauri_serial.* {
|
|
||||||
native <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep class com.lenn.tauri_serial.WryActivity {
|
|
||||||
public <init>(...);
|
|
||||||
|
|
||||||
void setWebView(com.lenn.tauri_serial.RustWebView);
|
|
||||||
java.lang.Class getAppClass(...);
|
|
||||||
java.lang.String getVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep class com.lenn.tauri_serial.Ipc {
|
|
||||||
public <init>(...);
|
|
||||||
|
|
||||||
@android.webkit.JavascriptInterface public <methods>;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep class com.lenn.tauri_serial.RustWebView {
|
|
||||||
public <init>(...);
|
|
||||||
|
|
||||||
void loadUrlMainThread(...);
|
|
||||||
void loadHTMLMainThread(...);
|
|
||||||
void evalScript(...);
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep class com.lenn.tauri_serial.RustWebChromeClient,com.lenn.tauri_serial.RustWebViewClient {
|
|
||||||
public <init>(...);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/home/lenn/Workspace/JE-Skin/src-tauri/target/aarch64-linux-android/release/libtauri_demo_lib.so
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/home/lenn/Workspace/JE-Skin/src-tauri/target/armv7-linux-androideabi/release/libtauri_demo_lib.so
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/home/lenn/Workspace/JE-Skin/src-tauri/target/i686-linux-android/release/libtauri_demo_lib.so
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/home/lenn/Workspace/JE-Skin/src-tauri/target/x86_64-linux-android/release/libtauri_demo_lib.so
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
val implementation by configurations
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":tauri-android"))
|
|
||||||
implementation(project(":tauri-plugin-opener"))
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
tauri.android.versionName=0.4.0
|
|
||||||
tauri.android.versionCode=4000
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
include ':tauri-android'
|
|
||||||
project(':tauri-android').projectDir = new File("/home/lenn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.10.3/mobile/android")
|
|
||||||
include ':tauri-plugin-opener'
|
|
||||||
project(':tauri-plugin-opener').projectDir = new File("/home/lenn/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-plugin-opener-2.5.3/android")
|
|
||||||
@@ -322,7 +322,6 @@ where
|
|||||||
let force = raw_to_g1(summary as u32);
|
let force = raw_to_g1(summary as u32);
|
||||||
push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force);
|
push_devkit_frame(&app, vals.as_slice(), frame.dts_ms(), force);
|
||||||
}
|
}
|
||||||
|
|
||||||
pending_sub_frame = Some(PendingSubFrame {
|
pending_sub_frame = Some(PendingSubFrame {
|
||||||
frame: frame.clone(),
|
frame: frame.clone(),
|
||||||
values: vals,
|
values: vals,
|
||||||
@@ -398,12 +397,12 @@ fn infer_matrix_shape(len: usize) -> (u32, u32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn raw_to_g1(raw: u32) -> f64 {
|
fn raw_to_g1(raw: u32) -> f64 {
|
||||||
const X: [u32; 12] = [
|
const X: [u32; 11] = [
|
||||||
0, 84402, 117218, 140176, 159126, 175812, 191484, 208758, 224703, 252448, 302361, 352703,
|
0, 75507, 93732, 122031, 145263, 168630, 189980, 226021, 253636, 307140, 361368,
|
||||||
];
|
];
|
||||||
|
|
||||||
const Y: [f64; 12] = [
|
const Y: [f64; 11] = [
|
||||||
0.0, 160.0, 260.0, 360.0, 460.0, 560.0, 660.0, 760.0, 860.0, 1060.0, 1560.0, 2060.0,
|
0.0, 197.0, 257.0, 357.0, 457.0, 557.0, 657.0, 857.0, 1057.0, 1557.0, 2057.0,
|
||||||
];
|
];
|
||||||
|
|
||||||
let n = X.len();
|
let n = X.len();
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import { fly } from "svelte/transition";
|
import { fly } from "svelte/transition";
|
||||||
import { DEFAULT_PRESSURE_RANGE_MAX, DEFAULT_PRESSURE_RANGE_MIN } from "$lib/config/pressure-range";
|
import { DEFAULT_PRESSURE_RANGE_MAX, DEFAULT_PRESSURE_RANGE_MIN } from "$lib/config/pressure-range";
|
||||||
import ConfigPanel from "$lib/components/ConfigPanel.svelte";
|
import ConfigPanel from "$lib/components/ConfigPanel.svelte";
|
||||||
import ModelStage from "$lib/components/ModelStage.svelte";
|
|
||||||
import NeonBreakoutArena from "$lib/components/NeonBreakoutArena.svelte";
|
import NeonBreakoutArena from "$lib/components/NeonBreakoutArena.svelte";
|
||||||
import PressureMatrixViewer from "$lib/components/PressureMatrixViewer.svelte";
|
import PressureMatrixViewer from "$lib/components/PressureMatrixViewer.svelte";
|
||||||
import SignalChart from "$lib/components/SignalChart.svelte";
|
import SignalChart from "$lib/components/SignalChart.svelte";
|
||||||
@@ -17,8 +16,7 @@
|
|||||||
HudSummary,
|
HudSummary,
|
||||||
LocaleCode,
|
LocaleCode,
|
||||||
MatrixDisplayMode,
|
MatrixDisplayMode,
|
||||||
PressureColorMapPreset,
|
PressureColorMapPreset
|
||||||
StageViewMode
|
|
||||||
} from "$lib/types/hud";
|
} from "$lib/types/hud";
|
||||||
|
|
||||||
export let locale: LocaleCode = "zh-CN";
|
export let locale: LocaleCode = "zh-CN";
|
||||||
@@ -43,8 +41,6 @@
|
|||||||
export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
||||||
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
||||||
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
||||||
export let stageViewMode: StageViewMode = "webgl";
|
|
||||||
export let modelUrl = "/models/je-skin-model.glb";
|
|
||||||
export let replaySectionLabel = "";
|
export let replaySectionLabel = "";
|
||||||
export let replayPlayLabel = "";
|
export let replayPlayLabel = "";
|
||||||
export let replayPauseLabel = "";
|
export let replayPauseLabel = "";
|
||||||
@@ -72,6 +68,7 @@
|
|||||||
let replaySide: "left" | "right" = "right";
|
let replaySide: "left" | "right" = "right";
|
||||||
|
|
||||||
const minRailScale = 0.2;
|
const minRailScale = 0.2;
|
||||||
|
const resultantForceZeroThreshold = 0.1;
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
configclose: void;
|
configclose: void;
|
||||||
replaytoggle: void;
|
replaytoggle: void;
|
||||||
@@ -85,10 +82,10 @@
|
|||||||
$: replaySide = summarySide === "left" ? "right" : "left";
|
$: replaySide = summarySide === "left" ? "right" : "left";
|
||||||
$: replayToggleButtonText = replayIsPlaying ? replayPauseLabel : replayPlayLabel;
|
$: replayToggleButtonText = replayIsPlaying ? replayPauseLabel : replayPlayLabel;
|
||||||
$: replayProgressPercent = Math.round(Math.min(1, Math.max(0, replayProgress)) * 100);
|
$: replayProgressPercent = Math.round(Math.min(1, Math.max(0, replayProgress)) * 100);
|
||||||
$: summaryCurveVisible = summary.points.length > 0 && summary.points.some((value) => Number.isFinite(value) && Math.abs(value) >= 0.0001);
|
$: summaryCurveVisible =
|
||||||
|
summary.latest != null && Number.isFinite(summary.latest) && summary.latest > resultantForceZeroThreshold;
|
||||||
$: splitMatrixTitle = locale === "zh-CN" ? "数字矩阵" : "Matrix";
|
$: splitMatrixTitle = locale === "zh-CN" ? "数字矩阵" : "Matrix";
|
||||||
$: splitMatrixHint = locale === "zh-CN" ? "实时压力数据 / 数字矩阵" : "Live pressure matrix";
|
$: splitMatrixHint = locale === "zh-CN" ? "实时压力数据 / 数字矩阵" : "Live pressure matrix";
|
||||||
$: isModelStage = stageViewMode === "model3d";
|
|
||||||
|
|
||||||
function toPxNumber(rawValue: string): number {
|
function toPxNumber(rawValue: string): number {
|
||||||
const value = Number.parseFloat(rawValue);
|
const value = Number.parseFloat(rawValue);
|
||||||
@@ -181,13 +178,7 @@
|
|||||||
bind:this={stagePlaneEl}
|
bind:this={stagePlaneEl}
|
||||||
style="--panel-zone-top-dyn: {panelZoneTopPx}px; --rail-scale-left: {leftRailScale}; --rail-scale-right: {rightRailScale};"
|
style="--panel-zone-top-dyn: {panelZoneTopPx}px; --rail-scale-left: {leftRailScale}; --rail-scale-right: {rightRailScale};"
|
||||||
>
|
>
|
||||||
{#if isModelStage}
|
{#if showPrecisionTestPanel}
|
||||||
<div class="canvas-wrap">
|
|
||||||
{#key modelUrl}
|
|
||||||
<ModelStage {locale} {modelUrl} />
|
|
||||||
{/key}
|
|
||||||
</div>
|
|
||||||
{:else if showPrecisionTestPanel}
|
|
||||||
<div class="split-game-wrap">
|
<div class="split-game-wrap">
|
||||||
<section class="split-panel split-matrix-panel">
|
<section class="split-panel split-matrix-panel">
|
||||||
<header class="split-panel-head">
|
<header class="split-panel-head">
|
||||||
@@ -243,7 +234,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showConfigPanel && !showPrecisionTestPanel && !isModelStage}
|
{#if showConfigPanel && !showPrecisionTestPanel}
|
||||||
<div class="config-panel-wrap">
|
<div class="config-panel-wrap">
|
||||||
<ConfigPanel
|
<ConfigPanel
|
||||||
bind:matrixRows
|
bind:matrixRows
|
||||||
@@ -265,7 +256,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !showPrecisionTestPanel && !isModelStage}
|
{#if !showPrecisionTestPanel}
|
||||||
<div class="panel-zone" bind:this={panelZoneEl}>
|
<div class="panel-zone" bind:this={panelZoneEl}>
|
||||||
<aside class="side-rail left-rail">
|
<aside class="side-rail left-rail">
|
||||||
<div class="rail-stack" bind:this={leftStackEl}>
|
<div class="rail-stack" bind:this={leftStackEl}>
|
||||||
@@ -337,7 +328,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if replayHasData && !showPrecisionTestPanel && !isModelStage}
|
{#if replayHasData && !showPrecisionTestPanel}
|
||||||
<aside class="replay-floating-panel" class:is-left={replaySide === "left"} class:is-right={replaySide === "right"}>
|
<aside class="replay-floating-panel" class:is-left={replaySide === "left"} class:is-right={replaySide === "right"}>
|
||||||
<div class="replay-panel-head">
|
<div class="replay-panel-head">
|
||||||
<div class="replay-panel-title-group">
|
<div class="replay-panel-title-group">
|
||||||
@@ -375,7 +366,7 @@
|
|||||||
</aside>
|
</aside>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !showPrecisionTestPanel && !isModelStage}
|
{#if !showPrecisionTestPanel}
|
||||||
<div class="stage-bottom-overlay">
|
<div class="stage-bottom-overlay">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
HudNoticeTone,
|
HudNoticeTone,
|
||||||
LocaleCode,
|
LocaleCode,
|
||||||
MatrixDisplayMode,
|
MatrixDisplayMode,
|
||||||
StageViewMode,
|
|
||||||
WindowControlAction
|
WindowControlAction
|
||||||
} from "$lib/types/hud";
|
} from "$lib/types/hud";
|
||||||
|
|
||||||
@@ -35,10 +34,6 @@
|
|||||||
export let matrixViewNumericLabel = "";
|
export let matrixViewNumericLabel = "";
|
||||||
export let matrixViewDotsLabel = "";
|
export let matrixViewDotsLabel = "";
|
||||||
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
export let matrixDisplayMode: MatrixDisplayMode = "dots";
|
||||||
export let stageModeLabel = "";
|
|
||||||
export let stageModeWebglLabel = "";
|
|
||||||
export let stageModeModelLabel = "";
|
|
||||||
export let stageViewMode: StageViewMode = "webgl";
|
|
||||||
export let connectActionLabel = "";
|
export let connectActionLabel = "";
|
||||||
export let disconnectActionLabel = "";
|
export let disconnectActionLabel = "";
|
||||||
export let exportActionLabel = "";
|
export let exportActionLabel = "";
|
||||||
@@ -61,7 +56,6 @@
|
|||||||
localechange: LocaleCode;
|
localechange: LocaleCode;
|
||||||
configlink: string;
|
configlink: string;
|
||||||
matrixdisplaytoggle: boolean;
|
matrixdisplaytoggle: boolean;
|
||||||
stagemodechange: StageViewMode;
|
|
||||||
portchange: string;
|
portchange: string;
|
||||||
serialrefresh: void;
|
serialrefresh: void;
|
||||||
serialconnect: string;
|
serialconnect: string;
|
||||||
@@ -111,10 +105,6 @@
|
|||||||
dispatch("matrixdisplaytoggle", matrixDisplayMode !== "dots");
|
dispatch("matrixdisplaytoggle", matrixDisplayMode !== "dots");
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitStageModeChange(nextMode: StageViewMode): void {
|
|
||||||
dispatch("stagemodechange", nextMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitPortChange(event: Event): void {
|
function emitPortChange(event: Event): void {
|
||||||
const target = event.currentTarget as HTMLSelectElement;
|
const target = event.currentTarget as HTMLSelectElement;
|
||||||
dispatch("portchange", target.value);
|
dispatch("portchange", target.value);
|
||||||
@@ -227,28 +217,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="stage-mode-switch" aria-label={stageModeLabel}>
|
|
||||||
<span class="stage-mode-label">{stageModeLabel}</span>
|
|
||||||
<div class="stage-mode-options" role="group" aria-label={stageModeLabel}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="stage-mode-btn"
|
|
||||||
class:is-active={stageViewMode === "webgl"}
|
|
||||||
on:click={() => emitStageModeChange("webgl")}
|
|
||||||
>
|
|
||||||
{stageModeWebglLabel}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="stage-mode-btn"
|
|
||||||
class:is-active={stageViewMode === "model3d"}
|
|
||||||
on:click={() => emitStageModeChange("model3d")}
|
|
||||||
>
|
|
||||||
{stageModeModelLabel}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="state-card" aria-label={connectionLabel}>
|
<section class="state-card" aria-label={connectionLabel}>
|
||||||
<span class="state-dot" class:ok={connectionTone === "ok"} class:warn={connectionTone === "warn"}></span>
|
<span class="state-dot" class:ok={connectionTone === "ok"} class:warn={connectionTone === "warn"}></span>
|
||||||
<span class="state-label">{connectionLabel}</span>
|
<span class="state-label">{connectionLabel}</span>
|
||||||
@@ -517,8 +485,7 @@
|
|||||||
background: var(--panel-surface);
|
background: var(--panel-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-switch-wrap,
|
.matrix-switch-wrap {
|
||||||
.stage-mode-switch {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
@@ -529,8 +496,7 @@
|
|||||||
background: var(--panel-surface);
|
background: var(--panel-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-switch-label,
|
.matrix-switch-label {
|
||||||
.stage-mode-label {
|
|
||||||
color: var(--panel-text-dim);
|
color: var(--panel-text-dim);
|
||||||
font-size: 0.66rem;
|
font-size: 0.66rem;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
@@ -621,45 +587,6 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-mode-options {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.18rem;
|
|
||||||
padding: 0.16rem;
|
|
||||||
border: 1px solid rgb(var(--hud-border-rgb) / 0.24);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgb(var(--hud-surface-deep-rgb) / 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-mode-btn {
|
|
||||||
min-block-size: 1.38rem;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.18rem 0.54rem;
|
|
||||||
background: transparent;
|
|
||||||
color: rgb(var(--hud-text-dim-rgb) / 0.88);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
border-color 180ms ease,
|
|
||||||
background-color 180ms ease,
|
|
||||||
color 180ms ease,
|
|
||||||
box-shadow 180ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-mode-btn:hover {
|
|
||||||
color: rgb(var(--hud-text-main-rgb) / 0.96);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-mode-btn.is-active {
|
|
||||||
border-color: rgb(var(--hud-cyan-rgb) / 0.42);
|
|
||||||
background: rgb(var(--hud-cyan-rgb) / 0.14);
|
|
||||||
color: rgb(var(--hud-text-main-rgb) / 0.98);
|
|
||||||
box-shadow: 0 0 12px rgb(var(--hud-cyan-rgb) / 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.state-dot {
|
.state-dot {
|
||||||
inline-size: 0.55rem;
|
inline-size: 0.55rem;
|
||||||
block-size: 0.55rem;
|
block-size: 0.55rem;
|
||||||
@@ -1289,4 +1216,4 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,469 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import * as THREE from "three";
|
|
||||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
|
||||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
||||||
import type { GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
||||||
import type { LocaleCode } from "$lib/types/hud";
|
|
||||||
|
|
||||||
type ModelLoadState = "loading" | "ready" | "missing" | "error";
|
|
||||||
|
|
||||||
export let locale: LocaleCode = "zh-CN";
|
|
||||||
export let modelUrl = "/models/je-skin-model.glb";
|
|
||||||
|
|
||||||
let rootEl: HTMLDivElement | undefined;
|
|
||||||
let canvasEl: HTMLCanvasElement | undefined;
|
|
||||||
let loadState: ModelLoadState = "loading";
|
|
||||||
let loadProgress = 0;
|
|
||||||
let loadError = "";
|
|
||||||
|
|
||||||
const FLOOR_Y = -1.15;
|
|
||||||
const MODEL_FLOOR_CLEARANCE = 0.035;
|
|
||||||
const MODEL_TARGET_HEIGHT = 8.4;
|
|
||||||
const MODEL_MIN_SCALE = 0.02;
|
|
||||||
const MODEL_MAX_SCALE = 80;
|
|
||||||
const CAMERA_DISTANCE_FACTOR = 1.35;
|
|
||||||
const CAMERA_DISTANCE_MIN = 7.5;
|
|
||||||
const CAMERA_DISTANCE_MAX = 24;
|
|
||||||
|
|
||||||
$: copy =
|
|
||||||
locale === "zh-CN"
|
|
||||||
? {
|
|
||||||
title: "3D 模型舱",
|
|
||||||
subtitle: "Dark Grid / Future Lab",
|
|
||||||
loading: "正在加载模型",
|
|
||||||
ready: "模型已载入",
|
|
||||||
missing: "等待模型文件",
|
|
||||||
error: "模型加载失败",
|
|
||||||
modelPath: "模型路径",
|
|
||||||
hint: "请使用 glTF 2.0 的 .glb/.gltf;旧版 glTF 1.0 需要先转换"
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
title: "3D Model Bay",
|
|
||||||
subtitle: "Dark Grid / Future Lab",
|
|
||||||
loading: "Loading model",
|
|
||||||
ready: "Model loaded",
|
|
||||||
missing: "Waiting for model file",
|
|
||||||
error: "Model load failed",
|
|
||||||
modelPath: "Model path",
|
|
||||||
hint: "Use glTF 2.0 .glb/.gltf assets; older glTF 1.0 files need conversion first"
|
|
||||||
};
|
|
||||||
$: statusText =
|
|
||||||
loadState === "ready"
|
|
||||||
? copy.ready
|
|
||||||
: loadState === "missing"
|
|
||||||
? copy.missing
|
|
||||||
: loadState === "error"
|
|
||||||
? copy.error
|
|
||||||
: `${copy.loading} ${Math.round(loadProgress)}%`;
|
|
||||||
|
|
||||||
function disposeObject3D(object: THREE.Object3D): void {
|
|
||||||
object.traverse((child) => {
|
|
||||||
const mesh = child as THREE.Mesh;
|
|
||||||
if (mesh.geometry) {
|
|
||||||
mesh.geometry.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
const material = mesh.material;
|
|
||||||
if (Array.isArray(material)) {
|
|
||||||
for (const item of material) {
|
|
||||||
item.dispose();
|
|
||||||
}
|
|
||||||
} else if (material) {
|
|
||||||
material.dispose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPlaceholderModel(): THREE.Group {
|
|
||||||
const group = new THREE.Group();
|
|
||||||
const cyan = new THREE.Color(0x5ee7ff);
|
|
||||||
const lime = new THREE.Color(0xa6ff7a);
|
|
||||||
|
|
||||||
const platform = new THREE.Mesh(
|
|
||||||
new THREE.CylinderGeometry(5.8, 6.7, 0.36, 96),
|
|
||||||
new THREE.MeshStandardMaterial({
|
|
||||||
color: 0x0c1824,
|
|
||||||
emissive: 0x07131f,
|
|
||||||
metalness: 0.62,
|
|
||||||
roughness: 0.34
|
|
||||||
})
|
|
||||||
);
|
|
||||||
platform.position.y = 0.18;
|
|
||||||
group.add(platform);
|
|
||||||
|
|
||||||
const ringGeometry = new THREE.TorusGeometry(4.35, 0.035, 10, 128);
|
|
||||||
const ringMaterial = new THREE.MeshBasicMaterial({ color: cyan, transparent: true, opacity: 0.78 });
|
|
||||||
for (let index = 0; index < 3; index += 1) {
|
|
||||||
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
|
|
||||||
ring.position.y = 0.52 + index * 0.52;
|
|
||||||
ring.rotation.x = Math.PI / 2;
|
|
||||||
group.add(ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
const coreMaterial = new THREE.MeshStandardMaterial({
|
|
||||||
color: 0x1b2a38,
|
|
||||||
emissive: 0x0a2632,
|
|
||||||
metalness: 0.48,
|
|
||||||
roughness: 0.42,
|
|
||||||
transparent: true,
|
|
||||||
opacity: 0.72
|
|
||||||
});
|
|
||||||
const core = new THREE.Mesh(new THREE.BoxGeometry(2.2, 3.4, 1.1), coreMaterial);
|
|
||||||
core.position.y = 2.4;
|
|
||||||
core.rotation.y = -0.36;
|
|
||||||
group.add(core);
|
|
||||||
|
|
||||||
const sensorMaterial = new THREE.MeshBasicMaterial({ color: lime, transparent: true, opacity: 0.88 });
|
|
||||||
for (let index = 0; index < 7; index += 1) {
|
|
||||||
const bead = new THREE.Mesh(new THREE.SphereGeometry(0.13, 18, 18), sensorMaterial);
|
|
||||||
bead.position.set(-0.72 + index * 0.24, 3.18 + Math.sin(index * 0.72) * 0.18, 0.6);
|
|
||||||
group.add(bead);
|
|
||||||
}
|
|
||||||
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number): number {
|
|
||||||
return Math.min(max, Math.max(min, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeObjectToStage(object: THREE.Object3D): THREE.Box3 {
|
|
||||||
object.updateMatrixWorld(true);
|
|
||||||
let bounds = new THREE.Box3().setFromObject(object);
|
|
||||||
const size = bounds.getSize(new THREE.Vector3());
|
|
||||||
const currentHeight = Math.max(size.y, 0.001);
|
|
||||||
const scale = clamp(MODEL_TARGET_HEIGHT / currentHeight, MODEL_MIN_SCALE, MODEL_MAX_SCALE);
|
|
||||||
|
|
||||||
object.scale.multiplyScalar(scale);
|
|
||||||
object.updateMatrixWorld(true);
|
|
||||||
|
|
||||||
bounds = new THREE.Box3().setFromObject(object);
|
|
||||||
const center = bounds.getCenter(new THREE.Vector3());
|
|
||||||
object.position.x -= center.x;
|
|
||||||
object.position.z -= center.z;
|
|
||||||
object.position.y += FLOOR_Y + MODEL_FLOOR_CLEARANCE - bounds.min.y;
|
|
||||||
object.updateMatrixWorld(true);
|
|
||||||
|
|
||||||
return new THREE.Box3().setFromObject(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
function frameObject(object: THREE.Object3D, camera: THREE.PerspectiveCamera, controls: OrbitControls): void {
|
|
||||||
const bounds = normalizeObjectToStage(object);
|
|
||||||
const size = bounds.getSize(new THREE.Vector3());
|
|
||||||
const maxAxis = Math.max(size.x, size.y, size.z, 1);
|
|
||||||
const distance = clamp(maxAxis * CAMERA_DISTANCE_FACTOR, CAMERA_DISTANCE_MIN, CAMERA_DISTANCE_MAX);
|
|
||||||
const targetY = FLOOR_Y + Math.max(size.y * 0.46, 1.4);
|
|
||||||
|
|
||||||
camera.position.set(distance * 0.48, targetY + distance * 0.24, distance * 0.68);
|
|
||||||
camera.near = Math.max(distance / 80, 0.01);
|
|
||||||
camera.far = distance * 24;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
controls.target.set(0, targetY, 0);
|
|
||||||
controls.minDistance = Math.max(distance * 0.32, 2);
|
|
||||||
controls.maxDistance = Math.max(distance * 2.5, 12);
|
|
||||||
controls.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (!rootEl || !canvasEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderer = new THREE.WebGLRenderer({
|
|
||||||
canvas: canvasEl,
|
|
||||||
antialias: true,
|
|
||||||
alpha: true,
|
|
||||||
powerPreference: "high-performance"
|
|
||||||
});
|
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
||||||
renderer.setClearColor(0x03070d, 1);
|
|
||||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
||||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
||||||
renderer.toneMappingExposure = 1.08;
|
|
||||||
|
|
||||||
const scene = new THREE.Scene();
|
|
||||||
scene.fog = new THREE.FogExp2(0x03070d, 0.028);
|
|
||||||
|
|
||||||
const camera = new THREE.PerspectiveCamera(38, 1, 0.05, 600);
|
|
||||||
camera.position.set(8, 6, 9);
|
|
||||||
|
|
||||||
const controls = new OrbitControls(camera, canvasEl);
|
|
||||||
controls.enableDamping = true;
|
|
||||||
controls.dampingFactor = 0.08;
|
|
||||||
controls.minDistance = 2.4;
|
|
||||||
controls.maxDistance = 32;
|
|
||||||
controls.target.set(0, FLOOR_Y + 3.2, 0);
|
|
||||||
|
|
||||||
const labGroup = new THREE.Group();
|
|
||||||
scene.add(labGroup);
|
|
||||||
|
|
||||||
const grid = new THREE.GridHelper(42, 42, 0x63e6ff, 0x123047);
|
|
||||||
grid.position.y = FLOOR_Y;
|
|
||||||
const gridMaterial = grid.material;
|
|
||||||
if (Array.isArray(gridMaterial)) {
|
|
||||||
for (const material of gridMaterial) {
|
|
||||||
material.transparent = true;
|
|
||||||
material.opacity = 0.28;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
gridMaterial.transparent = true;
|
|
||||||
gridMaterial.opacity = 0.28;
|
|
||||||
}
|
|
||||||
labGroup.add(grid);
|
|
||||||
|
|
||||||
const backGrid = new THREE.GridHelper(42, 42, 0x5ee7ff, 0x0c2436);
|
|
||||||
backGrid.position.set(0, 9.5, -17);
|
|
||||||
backGrid.rotation.x = Math.PI / 2;
|
|
||||||
const backGridMaterial = backGrid.material;
|
|
||||||
if (Array.isArray(backGridMaterial)) {
|
|
||||||
for (const material of backGridMaterial) {
|
|
||||||
material.transparent = true;
|
|
||||||
material.opacity = 0.12;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
backGridMaterial.transparent = true;
|
|
||||||
backGridMaterial.opacity = 0.12;
|
|
||||||
}
|
|
||||||
labGroup.add(backGrid);
|
|
||||||
|
|
||||||
const floor = new THREE.Mesh(
|
|
||||||
new THREE.PlaneGeometry(42, 42),
|
|
||||||
new THREE.MeshStandardMaterial({
|
|
||||||
color: 0x050c14,
|
|
||||||
metalness: 0.28,
|
|
||||||
roughness: 0.64,
|
|
||||||
transparent: true,
|
|
||||||
opacity: 0.72
|
|
||||||
})
|
|
||||||
);
|
|
||||||
floor.rotation.x = -Math.PI / 2;
|
|
||||||
floor.position.y = FLOOR_Y - 0.018;
|
|
||||||
labGroup.add(floor);
|
|
||||||
|
|
||||||
const ambient = new THREE.AmbientLight(0x9fb8d0, 0.22);
|
|
||||||
const keyLight = new THREE.DirectionalLight(0x7be7ff, 1.5);
|
|
||||||
keyLight.position.set(8, 12, 8);
|
|
||||||
const rimLight = new THREE.PointLight(0xa6ff7a, 26, 24, 2.1);
|
|
||||||
rimLight.position.set(-4.5, 4.8, -3.6);
|
|
||||||
const sideLight = new THREE.PointLight(0x5c8cff, 15, 28, 1.7);
|
|
||||||
sideLight.position.set(5.8, 3.2, -5.4);
|
|
||||||
scene.add(ambient, keyLight, rimLight, sideLight);
|
|
||||||
|
|
||||||
let activeModel: THREE.Object3D = buildPlaceholderModel();
|
|
||||||
scene.add(activeModel);
|
|
||||||
frameObject(activeModel, camera, controls);
|
|
||||||
|
|
||||||
const loader = new GLTFLoader();
|
|
||||||
loader.load(
|
|
||||||
modelUrl,
|
|
||||||
(gltf: GLTF) => {
|
|
||||||
scene.remove(activeModel);
|
|
||||||
disposeObject3D(activeModel);
|
|
||||||
activeModel = gltf.scene;
|
|
||||||
activeModel.traverse((child) => {
|
|
||||||
const mesh = child as THREE.Mesh;
|
|
||||||
if (mesh.isMesh) {
|
|
||||||
mesh.castShadow = true;
|
|
||||||
mesh.receiveShadow = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
scene.add(activeModel);
|
|
||||||
frameObject(activeModel, camera, controls);
|
|
||||||
loadState = "ready";
|
|
||||||
loadProgress = 100;
|
|
||||||
},
|
|
||||||
(event) => {
|
|
||||||
if (event.total > 0) {
|
|
||||||
loadProgress = (event.loaded / event.total) * 100;
|
|
||||||
} else {
|
|
||||||
loadProgress = 12;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
loadError = message || "Unknown model loader error";
|
|
||||||
loadState = message.toLowerCase().includes("404") ? "missing" : "error";
|
|
||||||
loadProgress = 0;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const resize = () => {
|
|
||||||
if (!rootEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const width = rootEl.clientWidth;
|
|
||||||
const height = rootEl.clientHeight;
|
|
||||||
if (width <= 0 || height <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.setSize(width, height, false);
|
|
||||||
camera.aspect = width / height;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
};
|
|
||||||
|
|
||||||
resize();
|
|
||||||
const resizeObserver = new ResizeObserver(resize);
|
|
||||||
resizeObserver.observe(rootEl);
|
|
||||||
|
|
||||||
renderer.setAnimationLoop((timestamp) => {
|
|
||||||
const seconds = timestamp / 1000;
|
|
||||||
labGroup.position.y = Math.sin(seconds * 0.75) * 0.015;
|
|
||||||
if (loadState !== "ready") {
|
|
||||||
activeModel.rotation.y = seconds * 0.32;
|
|
||||||
}
|
|
||||||
controls.update();
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
renderer.setAnimationLoop(null);
|
|
||||||
controls.dispose();
|
|
||||||
disposeObject3D(activeModel);
|
|
||||||
disposeObject3D(labGroup);
|
|
||||||
renderer.dispose();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="model-stage" bind:this={rootEl}>
|
|
||||||
<canvas class="model-canvas" bind:this={canvasEl} aria-label={copy.title}></canvas>
|
|
||||||
<div class="model-vignette" aria-hidden="true"></div>
|
|
||||||
<div class="model-scanlines" aria-hidden="true"></div>
|
|
||||||
|
|
||||||
<section class="model-hud" aria-label={copy.title}>
|
|
||||||
<p class="model-kicker">{copy.subtitle}</p>
|
|
||||||
<h2>{copy.title}</h2>
|
|
||||||
<div class="model-status-row">
|
|
||||||
<span class="status-light" class:is-ready={loadState === "ready"}></span>
|
|
||||||
<span>{statusText}</span>
|
|
||||||
</div>
|
|
||||||
<p class="model-path">{copy.modelPath}: {modelUrl}</p>
|
|
||||||
<p class="model-hint">{loadError || copy.hint}</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.model-stage {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 52% 62%, rgb(94 231 255 / 0.12), transparent 26%),
|
|
||||||
radial-gradient(circle at 24% 18%, rgb(166 255 122 / 0.07), transparent 24%),
|
|
||||||
linear-gradient(180deg, #03070d 0%, #07111b 48%, #02050a 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-canvas,
|
|
||||||
.model-vignette,
|
|
||||||
.model-scanlines {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
inline-size: 100%;
|
|
||||||
block-size: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-canvas {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-vignette,
|
|
||||||
.model-scanlines {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-vignette {
|
|
||||||
background:
|
|
||||||
linear-gradient(90deg, rgb(0 0 0 / 0.36), transparent 22%, transparent 78%, rgb(0 0 0 / 0.34)),
|
|
||||||
radial-gradient(circle at center, transparent 48%, rgb(0 0 0 / 0.58) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-scanlines {
|
|
||||||
opacity: 0.32;
|
|
||||||
background:
|
|
||||||
repeating-linear-gradient(180deg, rgb(94 231 255 / 0.045) 0, rgb(94 231 255 / 0.045) 1px, transparent 1px, transparent 4px);
|
|
||||||
mix-blend-mode: screen;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-hud {
|
|
||||||
position: absolute;
|
|
||||||
top: clamp(1.2rem, 2.8vw, 2.2rem);
|
|
||||||
left: clamp(1.2rem, 2.8vw, 2.4rem);
|
|
||||||
z-index: 2;
|
|
||||||
display: grid;
|
|
||||||
gap: 0.42rem;
|
|
||||||
max-inline-size: min(22rem, 42vw);
|
|
||||||
padding: 0.9rem 1rem 1rem;
|
|
||||||
border: 1px solid rgb(94 231 255 / 0.24);
|
|
||||||
border-radius: 0.7rem;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgb(8 18 28 / 0.82), rgb(3 9 15 / 0.72)),
|
|
||||||
radial-gradient(circle at 0 0, rgb(94 231 255 / 0.1), transparent 44%);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 1px 0 rgb(255 255 255 / 0.06),
|
|
||||||
0 0 28px rgb(94 231 255 / 0.08);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-kicker,
|
|
||||||
.model-path,
|
|
||||||
.model-hint {
|
|
||||||
margin: 0;
|
|
||||||
color: rgb(198 226 239 / 0.72);
|
|
||||||
font-size: 0.6rem;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
color: rgb(241 251 255 / 0.96);
|
|
||||||
font-size: clamp(1.15rem, 1.1vw + 0.88rem, 1.72rem);
|
|
||||||
line-height: 1.05;
|
|
||||||
font-weight: 650;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-status-row {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.44rem;
|
|
||||||
color: rgb(229 249 255 / 0.94);
|
|
||||||
font-size: 0.78rem;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-light {
|
|
||||||
inline-size: 0.58rem;
|
|
||||||
block-size: 0.58rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgb(255 188 92 / 0.95);
|
|
||||||
box-shadow: 0 0 0 2px rgb(255 188 92 / 0.16), 0 0 12px rgb(255 188 92 / 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-light.is-ready {
|
|
||||||
background: rgb(166 255 122 / 0.95);
|
|
||||||
box-shadow: 0 0 0 2px rgb(166 255 122 / 0.16), 0 0 14px rgb(166 255 122 / 0.22);
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-path {
|
|
||||||
color: rgb(94 231 255 / 0.78);
|
|
||||||
text-transform: none;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-hint {
|
|
||||||
color: rgb(198 226 239 / 0.66);
|
|
||||||
text-transform: none;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 960px) {
|
|
||||||
.model-hud {
|
|
||||||
max-inline-size: min(20rem, calc(100% - 2.4rem));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -145,6 +145,10 @@
|
|||||||
return "--";
|
return "--";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value === 0) {
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
|
||||||
return value.toFixed(1);
|
return value.toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export type LocaleCode = "zh-CN" | "en-US";
|
|||||||
export type WindowControlAction = "minimize" | "toggle-maximize" | "close";
|
export type WindowControlAction = "minimize" | "toggle-maximize" | "close";
|
||||||
|
|
||||||
export type ConnectionState = "online" | "connecting" | "offline";
|
export type ConnectionState = "online" | "connecting" | "offline";
|
||||||
export type StageViewMode = "webgl" | "model3d";
|
|
||||||
|
|
||||||
export type StageStatusTone = "ok" | "warn" | "idle";
|
export type StageStatusTone = "ok" | "warn" | "idle";
|
||||||
export type HudNoticeTone = "ok" | "warn" | "info";
|
export type HudNoticeTone = "ok" | "warn" | "info";
|
||||||
@@ -87,9 +86,6 @@ export interface HudCopy {
|
|||||||
matrixViewLabel: string;
|
matrixViewLabel: string;
|
||||||
matrixViewNumericLabel: string;
|
matrixViewNumericLabel: string;
|
||||||
matrixViewDotsLabel: string;
|
matrixViewDotsLabel: string;
|
||||||
stageModeLabel: string;
|
|
||||||
stageModeWebglLabel: string;
|
|
||||||
stageModeModelLabel: string;
|
|
||||||
resetConfigLabel: string;
|
resetConfigLabel: string;
|
||||||
applyLiveHint: string;
|
applyLiveHint: string;
|
||||||
runtimeReady: string;
|
runtimeReady: string;
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
SerialRecordStateResult,
|
SerialRecordStateResult,
|
||||||
SerialImportResult,
|
SerialImportResult,
|
||||||
SignalTone,
|
SignalTone,
|
||||||
StageViewMode,
|
|
||||||
WindowControlAction
|
WindowControlAction
|
||||||
} from "$lib/types/hud";
|
} from "$lib/types/hud";
|
||||||
|
|
||||||
@@ -63,9 +62,6 @@
|
|||||||
matrixViewLabel: "矩阵模式",
|
matrixViewLabel: "矩阵模式",
|
||||||
matrixViewNumericLabel: "数字矩阵",
|
matrixViewNumericLabel: "数字矩阵",
|
||||||
matrixViewDotsLabel: "点矩阵",
|
matrixViewDotsLabel: "点矩阵",
|
||||||
stageModeLabel: "渲染模式",
|
|
||||||
stageModeWebglLabel: "WebGL",
|
|
||||||
stageModeModelLabel: "3D 模型",
|
|
||||||
resetConfigLabel: "恢复默认",
|
resetConfigLabel: "恢复默认",
|
||||||
applyLiveHint: "实时生效 / 矩阵尺寸变更将重建 viewer",
|
applyLiveHint: "实时生效 / 矩阵尺寸变更将重建 viewer",
|
||||||
runtimeReady: "WEBGL2 READY",
|
runtimeReady: "WEBGL2 READY",
|
||||||
@@ -125,9 +121,6 @@
|
|||||||
matrixViewLabel: "Matrix Mode",
|
matrixViewLabel: "Matrix Mode",
|
||||||
matrixViewNumericLabel: "Numeric",
|
matrixViewNumericLabel: "Numeric",
|
||||||
matrixViewDotsLabel: "Dots",
|
matrixViewDotsLabel: "Dots",
|
||||||
stageModeLabel: "Render Mode",
|
|
||||||
stageModeWebglLabel: "WebGL",
|
|
||||||
stageModeModelLabel: "3D Model",
|
|
||||||
resetConfigLabel: "Reset",
|
resetConfigLabel: "Reset",
|
||||||
applyLiveHint: "Live apply / size changes recreate the viewer",
|
applyLiveHint: "Live apply / size changes recreate the viewer",
|
||||||
runtimeReady: "WEBGL2 READY",
|
runtimeReady: "WEBGL2 READY",
|
||||||
@@ -176,6 +169,7 @@
|
|||||||
const summaryPointsPerSeries = 42;
|
const summaryPointsPerSeries = 42;
|
||||||
const signalRenderTickMs = 1200;
|
const signalRenderTickMs = 1200;
|
||||||
const replayDefaultFrameMs = 40;
|
const replayDefaultFrameMs = 40;
|
||||||
|
const resultantForceZeroThreshold = 0.1;
|
||||||
const showSignalPanels = false;
|
const showSignalPanels = false;
|
||||||
const mockToneCycle: SignalTone[] = ["cyan", "lime", "orange", "violet", "gold", "rose"];
|
const mockToneCycle: SignalTone[] = ["cyan", "lime", "orange", "violet", "gold", "rose"];
|
||||||
|
|
||||||
@@ -234,7 +228,6 @@
|
|||||||
let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
|
||||||
let colorMapPreset: PressureColorMapPreset = "emerald";
|
let colorMapPreset: PressureColorMapPreset = "emerald";
|
||||||
let matrixDisplayMode: MatrixDisplayMode = "dots";
|
let matrixDisplayMode: MatrixDisplayMode = "dots";
|
||||||
let stageViewMode: StageViewMode = "webgl";
|
|
||||||
let replayFrames: ReplayFrame[] = [];
|
let replayFrames: ReplayFrame[] = [];
|
||||||
let replayCurrentIndex = 0;
|
let replayCurrentIndex = 0;
|
||||||
let replayHasDisplayedFrame = false;
|
let replayHasDisplayedFrame = false;
|
||||||
@@ -715,6 +708,21 @@
|
|||||||
return new Array<number>(totalCells).fill(0);
|
return new Array<number>(totalCells).fill(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldZeroPressureMatrix(summaryValue: HudSummary): boolean {
|
||||||
|
return summaryValue.latest != null && Number.isFinite(summaryValue.latest) && summaryValue.latest === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePressureMatrixForSummary(
|
||||||
|
sourceMatrix: number[] | null,
|
||||||
|
summaryValue: HudSummary
|
||||||
|
): number[] | null {
|
||||||
|
if (shouldZeroPressureMatrix(summaryValue)) {
|
||||||
|
return buildZeroMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceMatrix;
|
||||||
|
}
|
||||||
|
|
||||||
function resetReplayVisualState(): void {
|
function resetReplayVisualState(): void {
|
||||||
pressureMatrix = buildZeroMatrix();
|
pressureMatrix = buildZeroMatrix();
|
||||||
signalPanels = buildInactivePanels();
|
signalPanels = buildInactivePanels();
|
||||||
@@ -751,9 +759,10 @@
|
|||||||
replayCurrentIndex = safeIndex;
|
replayCurrentIndex = safeIndex;
|
||||||
replayHasDisplayedFrame = true;
|
replayHasDisplayedFrame = true;
|
||||||
replayProgress = replayFrames.length > 1 ? safeIndex / (replayFrames.length - 1) : 1;
|
replayProgress = replayFrames.length > 1 ? safeIndex / (replayFrames.length - 1) : 1;
|
||||||
pressureMatrix = frameValuesToMatrix(replayFrames[safeIndex].values);
|
const nextSummary = buildReplaySummaryAt(safeIndex);
|
||||||
|
pressureMatrix = resolvePressureMatrixForSummary(frameValuesToMatrix(replayFrames[safeIndex].values), nextSummary);
|
||||||
signalPanels = buildInactivePanels();
|
signalPanels = buildInactivePanels();
|
||||||
summary = buildReplaySummaryAt(safeIndex);
|
summary = nextSummary;
|
||||||
hasSignalData = true;
|
hasSignalData = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -919,16 +928,37 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isZeroLikeValue(value: number): boolean {
|
function normalizeResultantForce(value: number): number {
|
||||||
return !Number.isFinite(value) || Math.abs(value) < 0.0001;
|
if (!Number.isFinite(value)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value <= resultantForceZeroThreshold ? 0 : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldHideSummary(points: number[]): boolean {
|
function normalizeNullableResultantForce(value: number | null): number | null {
|
||||||
return points.length === 0 || points.every((value) => isZeroLikeValue(value));
|
return value == null ? null : normalizeResultantForce(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSummary(summaryValue: HudSummary): HudSummary {
|
function normalizeSummary(summaryValue: HudSummary): HudSummary {
|
||||||
return shouldHideSummary(summaryValue.points) ? buildEmptySummary() : summaryValue;
|
if (summaryValue.points.length === 0) {
|
||||||
|
return {
|
||||||
|
...summaryValue,
|
||||||
|
latest: normalizeNullableResultantForce(summaryValue.latest),
|
||||||
|
min: normalizeNullableResultantForce(summaryValue.min),
|
||||||
|
max: normalizeNullableResultantForce(summaryValue.max)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = summaryValue.points.map(normalizeResultantForce);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...summaryValue,
|
||||||
|
points,
|
||||||
|
latest: points[points.length - 1],
|
||||||
|
min: Math.min(...points),
|
||||||
|
max: Math.max(...points)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSummary(points: number[], xValues: number[] = []): HudSummary {
|
function buildSummary(points: number[], xValues: number[] = []): HudSummary {
|
||||||
@@ -936,7 +966,8 @@
|
|||||||
return buildEmptySummary();
|
return buildEmptySummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedXValues = points.map((_, index) => {
|
const normalizedPoints = points.map(normalizeResultantForce);
|
||||||
|
const resolvedXValues = normalizedPoints.map((_, index) => {
|
||||||
const x = xValues[index];
|
const x = xValues[index];
|
||||||
return Number.isFinite(x) ? Number(x) : index + 1;
|
return Number.isFinite(x) ? Number(x) : index + 1;
|
||||||
});
|
});
|
||||||
@@ -944,10 +975,10 @@
|
|||||||
return {
|
return {
|
||||||
label: "Resultant Force",
|
label: "Resultant Force",
|
||||||
xValues: resolvedXValues,
|
xValues: resolvedXValues,
|
||||||
points,
|
points: normalizedPoints,
|
||||||
latest: points[points.length - 1],
|
latest: normalizedPoints[normalizedPoints.length - 1],
|
||||||
min: Math.min(...points),
|
min: Math.min(...normalizedPoints),
|
||||||
max: Math.max(...points)
|
max: Math.max(...normalizedPoints)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -993,20 +1024,21 @@
|
|||||||
if (replayHasData) {
|
if (replayHasData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const normalizedSummary = normalizeSummary(packet.summary);
|
||||||
signalPanels = showSignalPanels ? packet.panels : buildInactivePanels();
|
signalPanels = showSignalPanels ? packet.panels : buildInactivePanels();
|
||||||
if (packet.summary.points.length > 0) {
|
if (normalizedSummary.points.length > 0) {
|
||||||
const nowSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
const nowSeconds = Math.round((Date.now() - sessionStartedAt) / 100) / 10;
|
||||||
const pointCount = packet.summary.points.length;
|
const pointCount = normalizedSummary.points.length;
|
||||||
const spacing =
|
const spacing =
|
||||||
pointCount > 1 ? Math.min(1.2, nowSeconds / Math.max(pointCount - 1, 1)) : 0;
|
pointCount > 1 ? Math.min(1.2, nowSeconds / Math.max(pointCount - 1, 1)) : 0;
|
||||||
const startX = Math.max(0, nowSeconds - spacing * Math.max(pointCount - 1, 0));
|
const startX = Math.max(0, nowSeconds - spacing * Math.max(pointCount - 1, 0));
|
||||||
const xValues = packet.summary.points.map((_, index) => Math.round((startX + index * spacing) * 10) / 10);
|
const xValues = normalizedSummary.points.map((_, index) => Math.round((startX + index * spacing) * 10) / 10);
|
||||||
summary = { ...packet.summary, xValues };
|
summary = { ...normalizedSummary, xValues };
|
||||||
} else {
|
} else {
|
||||||
summary = packet.summary;
|
summary = normalizedSummary;
|
||||||
}
|
}
|
||||||
pressureMatrix = packet.pressureMatrix;
|
pressureMatrix = resolvePressureMatrixForSummary(packet.pressureMatrix, normalizedSummary);
|
||||||
hasSignalData = signalPanels.length > 0 || packet.summary.points.length > 0;
|
hasSignalData = signalPanels.length > 0 || normalizedSummary.points.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearHudPanels(): void {
|
function clearHudPanels(): void {
|
||||||
@@ -1652,7 +1684,6 @@
|
|||||||
|
|
||||||
function handleConfigLink(event: CustomEvent<string>): void {
|
function handleConfigLink(event: CustomEvent<string>): void {
|
||||||
if (event.detail === "precision-test") {
|
if (event.detail === "precision-test") {
|
||||||
stageViewMode = "webgl";
|
|
||||||
isPrecisionTestOpen = !isPrecisionTestOpen;
|
isPrecisionTestOpen = !isPrecisionTestOpen;
|
||||||
isConfigPanelOpen = false;
|
isConfigPanelOpen = false;
|
||||||
isDevKitConfigOpen = false;
|
isDevKitConfigOpen = false;
|
||||||
@@ -1660,7 +1691,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.detail === "settings") {
|
if (event.detail === "settings") {
|
||||||
stageViewMode = "webgl";
|
|
||||||
isPrecisionTestOpen = false;
|
isPrecisionTestOpen = false;
|
||||||
isConfigPanelOpen = !isConfigPanelOpen;
|
isConfigPanelOpen = !isConfigPanelOpen;
|
||||||
isDevKitConfigOpen = false;
|
isDevKitConfigOpen = false;
|
||||||
@@ -1753,14 +1783,6 @@
|
|||||||
matrixDisplayMode = event.detail ? "dots" : "numeric";
|
matrixDisplayMode = event.detail ? "dots" : "numeric";
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStageModeChange(event: CustomEvent<StageViewMode>): void {
|
|
||||||
stageViewMode = event.detail;
|
|
||||||
if (stageViewMode === "model3d") {
|
|
||||||
isPrecisionTestOpen = false;
|
|
||||||
isConfigPanelOpen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
let unlistenHudStream: UnlistenFn | null = null;
|
let unlistenHudStream: UnlistenFn | null = null;
|
||||||
@@ -1856,10 +1878,6 @@
|
|||||||
matrixViewNumericLabel={uiCopy.matrixViewNumericLabel}
|
matrixViewNumericLabel={uiCopy.matrixViewNumericLabel}
|
||||||
matrixViewDotsLabel={uiCopy.matrixViewDotsLabel}
|
matrixViewDotsLabel={uiCopy.matrixViewDotsLabel}
|
||||||
{matrixDisplayMode}
|
{matrixDisplayMode}
|
||||||
stageModeLabel={uiCopy.stageModeLabel}
|
|
||||||
stageModeWebglLabel={uiCopy.stageModeWebglLabel}
|
|
||||||
stageModeModelLabel={uiCopy.stageModeModelLabel}
|
|
||||||
{stageViewMode}
|
|
||||||
connectActionLabel={uiCopy.connectActionLabel}
|
connectActionLabel={uiCopy.connectActionLabel}
|
||||||
disconnectActionLabel={uiCopy.disconnectActionLabel}
|
disconnectActionLabel={uiCopy.disconnectActionLabel}
|
||||||
exportActionLabel={uiCopy.exportActionLabel}
|
exportActionLabel={uiCopy.exportActionLabel}
|
||||||
@@ -1882,7 +1900,6 @@
|
|||||||
on:portchange={handlePortChange}
|
on:portchange={handlePortChange}
|
||||||
on:configlink={handleConfigLink}
|
on:configlink={handleConfigLink}
|
||||||
on:matrixdisplaytoggle={handleMatrixDisplayToggle}
|
on:matrixdisplaytoggle={handleMatrixDisplayToggle}
|
||||||
on:stagemodechange={handleStageModeChange}
|
|
||||||
on:serialrefresh={handleSerialRefresh}
|
on:serialrefresh={handleSerialRefresh}
|
||||||
on:serialconnect={handleSerialConnect}
|
on:serialconnect={handleSerialConnect}
|
||||||
on:serialexport={handleSerialExportRequest}
|
on:serialexport={handleSerialExportRequest}
|
||||||
@@ -1903,7 +1920,6 @@
|
|||||||
bind:rangeMax
|
bind:rangeMax
|
||||||
bind:colorMapPreset
|
bind:colorMapPreset
|
||||||
bind:matrixDisplayMode
|
bind:matrixDisplayMode
|
||||||
{stageViewMode}
|
|
||||||
configPanelTitle={uiCopy.configPanelTitle}
|
configPanelTitle={uiCopy.configPanelTitle}
|
||||||
configPanelHint={uiCopy.configPanelHint}
|
configPanelHint={uiCopy.configPanelHint}
|
||||||
matrixSizeLabel={uiCopy.matrixSizeLabel}
|
matrixSizeLabel={uiCopy.matrixSizeLabel}
|
||||||
@@ -1940,7 +1956,7 @@
|
|||||||
on:replayclose={handleReplayClose}
|
on:replayclose={handleReplayClose}
|
||||||
on:configclose={() => (isConfigPanelOpen = false)}
|
on:configclose={() => (isConfigPanelOpen = false)}
|
||||||
>
|
>
|
||||||
{#if !isPrecisionTestOpen && stageViewMode === "webgl"}
|
{#if !isPrecisionTestOpen}
|
||||||
<section class="range-scale" aria-label="Signal Range">
|
<section class="range-scale" aria-label="Signal Range">
|
||||||
<p class="range-label">{locale === "zh-CN" ? "范围" : "Range"}</p>
|
<p class="range-label">{locale === "zh-CN" ? "范围" : "Range"}</p>
|
||||||
<div class="range-track">
|
<div class="range-track">
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
# 3D model assets
|
|
||||||
|
|
||||||
Put the first pipeline model here:
|
|
||||||
|
|
||||||
- Preferred: `static/models/je-skin-model.glb`
|
|
||||||
- Format: glTF 2.0 `.glb`
|
|
||||||
- Also supported after changing `modelUrl`: glTF 2.0 `.gltf` with its `.bin` and texture files in the same folder
|
|
||||||
- Not supported directly: older glTF 1.0 assets. Convert them to glTF 2.0 first.
|
|
||||||
|
|
||||||
Runtime URL used by the app:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/models/je-skin-model.glb
|
|
||||||
```
|
|
||||||
Binary file not shown.
Reference in New Issue
Block a user