diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml deleted file mode 100644 index cffc550..0000000 --- a/.idea/jsLibraryMappings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/CrashReport.dmp b/CrashReport.dmp new file mode 100644 index 0000000..1b6cf23 Binary files /dev/null and b/CrashReport.dmp differ diff --git a/build.gradle b/build.gradle index 1338561..d008cdd 100644 --- a/build.gradle +++ b/build.gradle @@ -1,36 +1,26 @@ -import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform +import org.gradle.internal.os.OperatingSystem +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter plugins { id 'java' id 'application' - id 'edu.sc.seis.launch4j' version '2.5.4' id 'org.openjfx.javafxplugin' version '0.1.0' id 'org.springframework.boot' version '3.2.0' id 'io.spring.dependency-management' version '1.1.4' - id 'com.github.johnrengelman.shadow' version '8.1.1' apply false // 关闭 shadow -} - -configurations { - all*.exclude group: 'org.openjfx', module: 'javafx' - proguardLib -} - - -javafx { - version = "21" - modules = [ 'javafx.controls', 'javafx.graphics', 'javafx.web' ] -} - -// JDK 版本检查 -def requiredJavaVersion = 20 -def currentJavaVersion = JavaVersion.current().majorVersion.toInteger() -if (currentJavaVersion != requiredJavaVersion) { - throw new GradleException("构建需要 JDK ${requiredJavaVersion},但当前是 JDK ${currentJavaVersion}。请更换 JDK 环境。") + id 'edu.sc.seis.launch4j' version '2.5.4' } group = 'com.axis.innovators.box' version = '0.0.1' +// JDK 版本检查 +java { + toolchain { + languageVersion = JavaLanguageVersion.of(20) + } +} + repositories { maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } @@ -38,10 +28,28 @@ repositories { mavenCentral() } -dependencies { - // === 构建工具 === - proguardLib files('libs/proguard.jar') +// 操作系统 Native 标识符 +def lwjglNatives = "" +if (OperatingSystem.current().isWindows()) { + lwjglNatives = "natives-windows" +} else if (OperatingSystem.current().isLinux()) { + lwjglNatives = "natives-linux" +} else if (OperatingSystem.current().isMacOsX()) { + lwjglNatives = System.getProperty("os.arch") == "aarch64" ? "natives-macos-arm64" : "natives-macos" +} +javafx { + version = "21" + modules = [ 'javafx.controls', 'javafx.graphics', 'javafx.web', 'javafx.swing' ] +} + +// 排除冲突的日志和旧版 FX +configurations { + all*.exclude group: 'org.openjfx', module: 'javafx' + all*.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' +} + +dependencies { // === 测试框架 === testImplementation platform('org.junit:junit-bom:5.10.0') testImplementation 'org.junit.jupiter:junit-jupiter' @@ -51,19 +59,15 @@ dependencies { // === 开发工具 === developmentOnly 'org.springframework.boot:spring-boot-devtools' - // === JnaFileChooser 库 === + // === JnaFileChooser & Swing === implementation 'com.github.steos.jnafilechooser:jnafilechooser-api:1.1.2' implementation 'com.github.steos.jnafilechooser:jnafilechooser-win32:1.1.2' - - // === Swing 组件 === implementation 'org.swinglabs:swingx:1.6.1' // === 本地库文件 === - implementation files('libs/JNC-1.0-jnc.jar') - implementation files('libs/dog api 1.3.jar') - implementation files('libs/DesktopWallpaperSdk-1.0-SNAPSHOT.jar') + implementation fileTree(dir: 'libs', include: ['*.jar']) - // === DJL API === + // === AI (DJL) === implementation platform('ai.djl:bom:0.35.0') implementation 'ai.djl:api' implementation 'ai.djl:model-zoo' @@ -73,12 +77,13 @@ dependencies { implementation 'ai.djl.onnxruntime:onnxruntime-engine' runtimeOnly 'ai.djl.pytorch:pytorch-native-cpu:2.7.1' runtimeOnly 'ai.djl.onnxruntime:onnxruntime-native-cpu:1.3.0' + // === 核心工具库 === - implementation 'com.google.code.gson:gson:2.10.1' // 统一版本 + implementation 'com.google.code.gson:gson:2.10.1' implementation 'org.apache.logging.log4j:log4j-api:2.20.0' implementation 'org.apache.logging.log4j:log4j-core:2.20.0' - implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.20.0' - implementation 'commons-io:commons-io:2.18.0' // 统一版本 + implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0' + implementation 'commons-io:commons-io:2.18.0' implementation 'com.google.guava:guava:31.1-jre' implementation 'net.java.dev.jna:jna:5.13.0' implementation 'net.java.dev.jna:jna-platform:5.13.0' @@ -95,8 +100,8 @@ dependencies { implementation 'net.bytebuddy:byte-buddy:1.17.6' // === 反编译工具 === - implementation 'org.bitbucket.mstrobel:procyon-core:0.6.0' // 统一版本 - implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.6.0' // 统一版本 + implementation 'org.bitbucket.mstrobel:procyon-core:0.6.0' + implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.6.0' implementation 'org.benf:cfr:0.152' // === Java 解析与分析 === @@ -110,18 +115,14 @@ dependencies { // === Web 和网络 === implementation 'org.jsoup:jsoup:1.17.2' - implementation 'org.json:json:20231013' // 统一版本 - implementation 'org.openjfx:javafx-web:17' + implementation 'org.json:json:20231013' + implementation 'org.openjfx:javafx-web:21' // === UI 框架 === implementation 'com.formdev:flatlaf:3.2.1' implementation 'com.formdev:flatlaf-extras:3.2.1' implementation 'com.formdev:flatlaf-intellij-themes:3.2.1' implementation 'io.github.vincenzopalazzo:material-ui-swing:1.1.2' - - // JavaFX - implementation 'org.openjfx:javafx-controls:21' - implementation 'org.openjfx:javafx-graphics:21' implementation 'org.fxmisc.richtext:richtextfx:0.11.0' implementation 'org.controlsfx:controlsfx:11.1.2' implementation 'com.dlsc.formsfx:formsfx-core:11.6.0' @@ -133,32 +134,24 @@ dependencies { implementation 'com.fifesoft:languagesupport:3.3.0' implementation 'com.fifesoft:autocomplete:3.3.2' - // === 图形和游戏引擎 === - // LWJGL - implementation 'org.lwjgl:lwjgl:3.3.6' - implementation 'org.lwjgl:lwjgl-stb:3.3.6' - implementation 'org.lwjgl:lwjgl-glfw:3.3.6' - implementation 'org.lwjgl:lwjgl-opengl:3.3.6' - implementation 'org.lwjgl:lwjgl-jawt:3.3.5' + // === LWJGL 图形引擎 === + def lwjglVersion = "3.3.6" + implementation "org.lwjgl:lwjgl:$lwjglVersion" + implementation "org.lwjgl:lwjgl-stb:$lwjglVersion" + implementation "org.lwjgl:lwjgl-glfw:$lwjglVersion" + implementation "org.lwjgl:lwjgl-opengl:$lwjglVersion" + implementation "org.lwjgl:lwjgl-jawt:$lwjglVersion" - // Lwjgl natives - if (DefaultNativePlatform.currentOperatingSystem.isWindows()) { - runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-windows' - runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-windows' - runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-windows' - runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-windows' - } else if (DefaultNativePlatform.currentOperatingSystem.isLinux()) { - runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-linux' - runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-linux' - runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-linux' - runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-linux' - } else if (DefaultNativePlatform.currentOperatingSystem.isMacOsX()) { - runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-macos' - runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-macos' - runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-macos' - runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-macos' + implementation("com.github.LWJGLX:lwjgl3-awt:bc8daf521a") { + exclude group: "org.lwjgl" } + // LWJGL Natives + runtimeOnly "org.lwjgl:lwjgl:$lwjglVersion:$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-glfw:$lwjglVersion:$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-opengl:$lwjglVersion:$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-stb:$lwjglVersion:$lwjglNatives" + // 其他图形库 implementation 'com.badlogicgames.gdx:gdx:1.12.1' implementation 'org.joml:joml:1.10.7' @@ -186,6 +179,10 @@ dependencies { implementation 'org.xerial:sqlite-jdbc:3.41.2.1' implementation 'org.postgresql:postgresql:42.6.0' + // === 伪终端与测试容器 === + implementation 'org.jetbrains.pty4j:pty4j:0.12.13' + implementation 'org.testcontainers:testcontainers:1.19.0' + // === 音频处理 === implementation 'jflac:jflac:1.3' implementation 'com.github.axet:TarsosDSP:2.4' @@ -207,52 +204,39 @@ dependencies { implementation 'org.casbin:casdoor-java-sdk:1.37.0' } -configurations.configureEach { - resolutionStrategy.cacheChangingModulesFor 0, 'seconds' +// 自动生成构建信息文件 +tasks.register('generateBuildProperties') { + group = 'build' + doLast { + def resDir = file("src/main/resources/build") + resDir.mkdirs() + def propertiesFile = new File(resDir, 'build.properties') + propertiesFile.text = """# Auto-generated build information +version=${project.version} +buildTimestamp=${LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)} +buildSystem=${OperatingSystem.current().isWindows() ? "WINDOWS" : "UNIX"} +""" + } } -// 复制依赖到 libs 目录 -task copyDependencies(type: Copy) { +compileJava.dependsOn generateBuildProperties + +// 复制依赖包到构建目录 +tasks.register('copyDependencies', Copy) { + group = "build" from configurations.runtimeClasspath into "$buildDir/libs/libs" } -// 原始 jar 打包(不含依赖) tasks.jar { dependsOn copyDependencies archiveBaseName.set("${rootProject.name}") - archiveVersion.set("${version}") -} - -// ProGuard 混淆任务 -task obfuscateJar(type: JavaExec) { - dependsOn jar - group = "build" - description = "使用 ProGuard 混淆并生成映射表" - - mainClass = 'proguard.ProGuard' - classpath = configurations.proguardLib - - args = [ - '-injars', "$buildDir/libs/${rootProject.name}-${version}.jar", - '-outjars', "$buildDir/libs/${rootProject.name}-${version}-obf.jar", - '-libraryjars', "${System.getProperty('java.home')}/jmods/java.base.jmod", - '-printmapping', "$buildDir/libs/output.srg", - '-keep class com.axis.innovators.box.plugins.**', - '-keep class com.axis.innovators.box.plugins.BoxClassLoader{*;}', - '-keeppackagenames', 'com.axis.innovators.box', - '-keeppackagenames', 'com.axis.innovators.box.plugins', - '-keepnames', 'class com.axis.innovators.box.**', - '-keepnames', 'class com.axis.innovators.box.plugins.**', - '-dontwarn', - '-dontoptimize', - '-dontshrink', - '-keepattributes', 'Signature,InnerClasses,EnclosingMethod,RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations,RuntimeVisibleParameterAnnotations,RuntimeInvisibleParameterAnnotations,Deprecated,SourceFile,LineNumberTable,LocalVariableTable,LocalVariableTypeTable' - ] -} - -build { - dependsOn obfuscateJar + manifest { + attributes( + 'Main-Class': 'com.axis.innovators.box.Main', + 'Class-Path': configurations.runtimeClasspath.files.collect { "libs/${it.name}" }.join(' ') + ) + } } application { @@ -260,84 +244,11 @@ application { } tasks.register('runBoxClient', JavaExec) { - group = "run-toolboxProgram" - description = "执行工具箱程序" + group = "run" classpath = sourceSets.main.runtimeClasspath mainClass = "com.axis.innovators.box.Main" jvmArgs = [ "-Dfile.encoding=UTF-8", "-Djava.system.class.loader=com.axis.innovators.box.plugins.BoxClassLoader" ] -} - -tasks.register('runVivid2DClient', JavaExec) { - group = "run-vivid2D" - description = "执行工具箱程序" - classpath = sourceSets.main.runtimeClasspath - mainClass = "com.chuangzhou.vivid2D.Main" - jvmArgs = [ - "-Dfile.encoding=UTF-8" - ] -} - -tasks.register('test2DModelLayerPanel', JavaExec) { - group = "test-model" - description = "运行 2D Model Layer Panel 测试" - classpath = sourceSets.main.runtimeClasspath - mainClass = "com.chuangzhou.vivid2D.test.ModelLayerPanelTest" - jvmArgs = [ - "-Dfile.encoding=UTF-8" - ] -} - -tasks.register('testModelRenderLightingTest', JavaExec) { - group = "test-model" - description = "运行 2D Model 高亮灯光测试" - classpath = sourceSets.main.runtimeClasspath - mainClass = "com.chuangzhou.vivid2D.test.ModelRenderLightingTest" - jvmArgs = [ - "-Dfile.encoding=UTF-8" - ] -} - -tasks.register('testModelTest', JavaExec) { - group = "test-model" - description = "运行 2D Model 保存和完整性测试" - classpath = sourceSets.main.runtimeClasspath - mainClass = "com.chuangzhou.vivid2D.test.ModelTest" - jvmArgs = [ - "-Dfile.encoding=UTF-8" - ] -} - -tasks.register('testModelTest2', JavaExec) { - group = "test-model" - description = "运行 2D Model 物理基准测试" - classpath = sourceSets.main.runtimeClasspath - mainClass = "com.chuangzhou.vivid2D.test.ModelTest2" - jvmArgs = [ - "-Dfile.encoding=UTF-8" - ] -} - -task printFxPath { - doLast { - // 1. 获取所有运行时依赖文件 - def runtimeClasspath = configurations.runtimeClasspath.files - - // 2. 筛选出所有名字包含 "javafx" 的 jar 包 - def javafxJars = runtimeClasspath.findAll { file -> - file.name.contains("javafx") - } - - // 3. 获取这些 jar 包的绝对路径 - def javafxPaths = javafxJars.collect { it.absolutePath } - - // 4. 使用系统的路径分隔符 (Windows是';', Linux/macOS是':') 将它们连接起来 - def modulePath = javafxPaths.join(System.getProperty("path.separator")) - - println "========================================================================" - println "COPY THIS JavaFX Module Path: " + modulePath - println "========================================================================" - } } \ No newline at end of file diff --git a/javascript/HtmlJarViewer.html b/javascript/HtmlJarViewer.html new file mode 100644 index 0000000..89e9069 --- /dev/null +++ b/javascript/HtmlJarViewer.html @@ -0,0 +1,1216 @@ + + + + + Java Decompiler Pro + + + + + + + + + +
+
JD-PRO
+ + + +
+ + + + + + +
+
+ +
+ + + + + +
+
+
+
+ +

请选择文件以查看源代码

+
+
+
+
处理中...
+
+
+ + + + + + + diff --git a/javascript/LinuxTerminal.html b/javascript/LinuxTerminal.html new file mode 100644 index 0000000..6ff2bf2 --- /dev/null +++ b/javascript/LinuxTerminal.html @@ -0,0 +1,117 @@ + + + + + Real Linux Terminal + + + + + +
+ + + + + + + + + \ No newline at end of file diff --git a/javascript/SQLTerminal.html b/javascript/SQLTerminal.html new file mode 100644 index 0000000..75c623a --- /dev/null +++ b/javascript/SQLTerminal.html @@ -0,0 +1,481 @@ + + + + + SQL Command Line + + + + +
+
+ Axis SQL Client [Version 2.0.0]
+ (c) 2024 Axis Innovators.
+ Loaded local database mode.
+
+ Commands end with ;
+ Type 'help' for help. 'cls' to clear.
+
+
+
+ SQL> + +
+
+
+ + + + \ No newline at end of file diff --git a/language/saved_language.properties b/language/saved_language.properties index 0700975..0e7e828 100644 --- a/language/saved_language.properties +++ b/language/saved_language.properties @@ -1,3 +1,3 @@ #Current Loaded Language -#Sun Oct 05 18:45:33 CST 2025 +#Fri Jan 02 17:08:53 CST 2026 loadedLanguage=system\:zh_CN diff --git a/library/jcef/cache/Crowd Deny/2025.10.6.61/Preload Data b/library/jcef/cache/Crowd Deny/2025.10.6.61/Preload Data deleted file mode 100644 index e41bb39..0000000 --- a/library/jcef/cache/Crowd Deny/2025.10.6.61/Preload Data +++ /dev/null @@ -1,7713 +0,0 @@ - - -24.hu - - 33bridges.com - -4cima.my-cima.net - -777.porn - - -777.ua -! -alasdelonamerceriacreativa.es - -allo.ua - - altema.jp - -americansongwriter.com - -animesdigital.org - - -anizle.com - - anoboy.be - -app.khaddavi.net - -athlonsports.com - - avrora.ua - - aylink.co - - behmelody.in - -bigpara.hurriyet.com.tr - -biznes.interia.pl - -businessinsider.com.pl - -clammilyhamlet.boats - -clck.idealmedia.io -# -clear-reef-flow-trailhead.click - -clutchpoints.com - - comicbook.com - - cuevana-4.com - - -d-s.io - - deadline.com - - decider.com - -diversesystem.com - -dotesports.com - - dsvplay.com - -eobuwie.com.pl - -eu.nasugudau.click - -eurosport.tvn24.pl - - -ew.com - - fastpic.org - -filmora.wondershare.de - - -fishki.net - -fitgirlmilano.it - -flemmix.monster - - fortune.com - -forums.futura-sciences.com - -freeastrologers.com - - futurism.com - -gamestrend.net - - geekchamp.com - -geekweek.interia.pl - - gokutv.cyou - -haber.mynet.com - -hannahkimdesign.com - - -hdzog.tube - - highporn.net - - -hochi.news - - -hotline.ua - - igg-games.com - -infoshina.com.ua - -instantbuzz.net - -interestingengineering.com - -keskustelu.suomi24.fi - - kinoflux.cc - - -kinoukr.tv - -kobieta.onet.pl - - kompoz2.com - -kultura.onet.pl - -life.ru - -lifehacker.com - - lifehacker.ru - - lpconttop.com - -lubimyczytac.pl - - mandiner.hu - - manysex.tube - - mashable.com - - meduza.io - - melisa.pl - -mentalmars.com - - metro.co.uk - - militaria.pl - -minimalistbaker.com - -mlledemenage.fr - - modivo.pl - -motoryzacja.interia.pl - -muzyka.interia.pl - -myshoppingblog.com - - -namnak.com - - -net.hr - -newrepublic.com - -news.ohmymag.com - - -newso2.xyz - -nishispo.nishinippon.co.jp - - -nlc.hu - - nudebase.com - -nv.ua - - -nypost.com - -odelices.ouest-france.fr - - pagesix.com - -panopticplausibility.hair - - -parade.com - - pemplay.com - - -people.com - -podroze.onet.pl - -pogoda.interia.pl - - porn112.com - - pornone.com - -port.hu - -powerplaygames.net -" -preload-spammy.permission.site - -privatehomeclips.com - -progameguides.com - -przegladsportowy.onet.pl - -rg.ru - - -ria.ru - -russian.rt.com - - rusvideos.net - -sahand-music.ir - -shahid.el7l.online -* -&sharp-sanctuary-rhythm-woodlands.click - -skul.pl - -smart-flash.jp - -solarmovie2.com - -sorularlaislamiyet.com - -spammy.permission.site - - spidersweb.pl - -sport.interia.pl - -storyhorizon.net - -streamtape.com - - subdl.com - -subtitlestar.com - -suburbanfinance.com - - tajmusics.com - -taskandpurpose.com - -telemagazyn.pl - -tenfactorialrocks.com - - thehill.com - -thepiratebay.org - - theprint.in - - titis.org - -toutelatele.ouest-france.fr - - trahkino.cc - -txxx.me - - -unherd.com - - -uploady.io - - upornia.tube - -vid.shahidwbas.tv - -videocelebs.net - - vipserije.com - - vod.mycima.cc - - voyeurhit.com - -vz.ru - -wearmedicine.com - -wiadomosci.onet.pl - -wiadomosci.radiozet.pl - -wojas.pl - - worldstar.com - - wowroms.com - -ww13.myasiantv.es - -ww20.0123movie.net - -www.20minutes.fr - - www.24sata.hr - -www.3djuegos.com - -www.advocate.com - -www.agroinform.hu - -www.alltime.ru - -www.alucare.fr - -www.alvolante.it - -www.analdin.com - -www.androidcentral.com - - www.apart.pl - -www.aufeminin.com - -www.auto-swiat.pl - -www.autodoc.pl - -www.autoplus.fr - -www.autozeitung.de - - www.b92.net - -www.backenmachtgluecklich.de - -www.bkmkitap.com - -www.bobvila.com - -www.bollywoodshaadis.com - -www.boredpanda.com - - -www.brw.pl - - www.bryk.pl - -www.buzfilmizle3.com - -www.cinemablend.com - -www.closermag.fr - - www.cnet.com - -www.creativebloq.com - -www.dailykos.com - -www.dailymail.co.uk - -www.deccanherald.com - - www.delfi.lt - -www.denver7.com - -www.destructoid.com - -www.dicocitations.com - - www.dire.it - -www.dlink5.com - - www.earth.com - -www.eatthis.com - -www.ecranlarge.com - - www.elle.com - - www.eska.pl - -www.espinof.com - -www.euro.com.pl - -www.evvelcevap.com - - www.fakt.pl - -www.fantacalcio.it - -www.fikriyat.com - -www.filmweb.pl - -www.firstpost.com - -www.fotomac.com.tr - -www.futura-sciences.com - -www.gamesradar.com - - www.gazeta.pl - - -www.geo.tv - -www.gobankingrates.com - -www.guitarworld.com - - www.gzt.com - -www.happyinshape.com - -www.harpersbazaar.com - -www.heavy-r.com - -www.hellomagazine.com - -www.huffingtonpost.fr - - www.ign.com - -www.ilgiornale.it - -www.ilsussidiario.net - -www.indekskitap.com - -www.independent.co.uk - - www.infor.pl - -www.inside-games.jp - -www.interia.pl - -www.itopya.com - -www.jeuxvideo.com - -www.justjared.com - -www.komputerswiat.pl - - www.kurir.rs - -www.lacoccinelle.net - - www.lecker.de - -www.lepoint.fr - -www.libertaddigital.com - -www.limetorrents.fun - -www.livescience.com - -www.lookmovie2.to - -www.manomano.de - -www.marieclaire.com - -www.mariefrance.fr - -www.meczyki.pl - -www.mediaexpert.pl - -www.mediaite.com - -www.medonet.pl - - www.melty.fr - -www.memurlar.net - -www.mensjournal.com - -www.mentalfloss.com - -www.mercurynews.com - -www.minikoyuncu.org - -www.morele.net - - www.moyo.ua - -www.mumsnet.com - -www.musicradar.com - -www.my-personaltrainer.it - - www.mynet.com - - www.ndtv.com - -www.newsweek.pl - -www.nextplz.fr - -www.ntv.com.tr - -www.ntvspor.net - -www.obozrevatel.com - -www.ofeminin.pl - - www.oleole.pl - - www.onet.pl - - www.otodom.pl - - www.out.com - -www.outkick.com - - -www.pap.pl - -www.paribucineverse.com - -www.pccomponentes.it - -www.pcgamer.com - - www.pcmag.com - -www.pcworld.com - -www.phonearena.com - -www.polsatnews.pl - -www.polsatsport.pl - -www.pomponik.pl - -www.popsci.com - -www.pornhits.com - -www.pornohammer.com - -www.pornohirsch.net - - -www.ppe.pl - -www.quattroruote.it - - -www.rd.com - -www.reportmotori.it - -www.rexporn.sex - - www.rmf24.pl - -www.rollingstone.com - - www.rylko.com - -www.sabah.com.tr - -www.schulferien.org - - www.se.pl - -www.semafor.com - -www.serieously.com - -www.seriouseats.com - -www.sfgate.com - -www.sheknows.com - -www.skapiec.pl - -www.skuola.net - -www.soapcentral.com - -www.sololibri.net - -www.sozcu.com.tr - -www.sponichi.co.jp - -www.sport-express.ru - - www.sport1.de - -www.sportskeeda.com - -www.statesman.com - -www.studenti.it - -www.sueddeutsche.de - -www.supplementler.com - -www.takvim.com.tr - -www.tarafdari.com - -www.techbloat.com - -www.techinsider.ru - -www.techradar.com - -www.telestar.fr - -www.the-independent.com - -www.the-sun.com - -www.thedailybeast.com - -www.thenews.com.pk - -www.thestreet.com - -www.thesun.co.uk - -www.tips-and-tricks.co - - www.tmz.com - -www.tomsguide.com - -www.tomshardware.com - -www.torrentjogos.com.br - -www.totalprosports.com - -www.transfermarkt.com.tr - -www.travelandtourworld.com - - www.twz.com - -www.usmagazine.com - -www.valleyvanguardonline.com - - www.vesti.ru - - www.vice.com - -www.wallstreet-online.de - -www.wareable.com - -www.washingtontimes.com - -www.wcostream.tv - - www.wgal.com - -www.wionews.com - - www.woman.ru - -www.womanandhome.com - -www.xasiat.com - -www.xozilla.com - -www.yardbarker.com - -www.yenisafak.com - -www.zakzak.co.jp - -www.zipfilmizle.com - - www.zoomit.ir - - wyborcza.pl - -wydarzenia.interia.pl - - xcadr.online - -xn--90aivcdt6dxbc.xn--p1ai - -xxxz.tv - -yorozoonews.jp - - zawodtyper.pl - -zielona.interia.pl - - -zrzutka.pl - -zywienie.medonet.pl - -0a7kjiqn4fpp.today - -0e4jtfvhci7z.today - -0m7o22zqe0of.today - -0mcws4c253o2.today - -0r2zxpj1f5f2.today - -11hmsrx6zyvx.today - -13tl3z4bcv5b.today - - -150mbs.cfd - -1691rf0vmwaa.today - -1btk.com - -1canadianxpills.com - -1dollarnoads.com - -1goy.com - -1is2vplodfpb.today - - -200mbs.cfd - - 20ben.com - -2btv56mo8cjb.today - - 2getmewp.com - - 2stream.org - -2t5i0v47yhgc.today - - -2watch.org - -305ut7z64cyq.today - -30minuteproject.com - -33594n12hu04.today - -360propmgmt.com - -3rhoz4yvnty9.today - -41d3yspbixvp.today - - 420xnxx.com - - 45tjg.com - - 4coder.info - -4cwwu0fx30n2.today - -4gbocqr3so07.today - -4hap.com - -4thelucky1s.lol - -4vmjocaftk25.today - -51singletravel.com - -5280-crossfit.com - -5280appliances.com - - 5280deli.com - - 5chances.lol - -5e7m3cm6mddh.today - -5yjay8n325h5.today - -604grj1ozimc.today - -60kmkmyqjwnx.today - -6d673wx0l41v.today - -6o24zqcdmzvw.today - - -7758lm.com - -7b15d2bhzgx0.today - - 7getmewp.com - -7sub0a5h5mw0.today - -81i62v7kzg9s.today - - -863883.com - - 895yy.com - -8pq6j2ikwzm0.today - -96thespecials.lol - -97thelucky1s.lol - - 97thelulz.lol - - 99chengxu.com - -9lq13gwkl3ou.today - -a1appliancerepairsllc.com - -aandaflowers.com - -aandbtiresnbrakes.com - -aaronaquatech.work - -aaronarachnologist.work - -aayoyoclub.com - - abccprmi.com - -abillamberced.com - - -abmilf.com - - -abumir.com - - abxxx.com -' -!accademia-sacro-romano-impero.org - -ad0zwdojajn8.today - - -adipop.pro - - adiving.icu - -adoringirl.com - -adrianagronomist.work - - -ads4pc.com - -adsforcomputercity.com - -adsforcomputertech.com - -adsforcomputerweb.com - -adslivetraining.com - -adsmetricsnova.top - - adstopc.com - - -adulai.pro - - advtgroup.com - - advtpro.com - - afficists.icu - - -afiden.xyz - -afterhours-fun.com - -agootictaticol.com - - ahocoxuju.sbs - - ahume.sbs - -aigaithojo.com - -ainsleyagro.work - -airapandio.cfd - -airconnection.cfd - -airmacreatives.com - - ajuce.com - -akl.im - - alibulk.com - - alkhrysat.com - -allow4takeyou.com - -allrightstech.com -& - allservicesheatingandcooling.com - - alphaurge.com - -alternative-gals.com - - amarekt.cfd - - -amathy.sbs - -americanvoiceinstitute.org - -ameritechafr.com - -amigaslindas.com - -amorespictures.com - - amourhunt.xyz - -amplicogroup.com - -amrdiabmedia.com - -anapaulafnails.com - -andirasang.com - -andperformace.cfd - -anewspapers.icu - -annuity-rates.org - -antimespace.sbs - - antv2.com - - apeorut.xyz - - apitonk.xyz -! -appslotverifyfreecredit.com - - aquetists.com - - arayejo.sbs - -arcadeslourdes.com - - arketis.cfd - - aronoyo.sbs - - arousedu.xyz - -arterlessemorke.com - -arthurmurraytime.com - -artistryhearts.monster - -artsbygenesis.com - - artsykc.com - - asianads.org - - ass2waist.com - - -assect.cfd - -assistance-guides.com - - -astars.cfd - - atentric.com - -atorvastatinon.com - - -auppoe.cfd - -autblaculfixed.com - -auto-traveler.com - -autocompensation.top - -autolightech.com - - autolog.autos - -autonicengine.com - -autoselectable.cfd - -averagesapper.com - - avocafc.com - - avsdemo.com - - axelora.site - - -badafd.com - - baishi360.com - -banana-wear.com - -bananafanana.ru - -banfftvfest.com - -bangcraver.com - - bangpit.com - -bangthrust.com - -banhcanhqueemqueanh.com - -banyanestatehuahin.com - -barrybikebot.work - -bathroomwall.org - - bau-messe.com - -bayroc-marina.com - - bbwgirls.xyz - -bdpropertymaintenance.com - -beatrizbryologist.work - -beautifultimes.me - - bellring.cfd - - belltower.cfd - -bemydaddynow.com - - benflare.work - -bergluxuryhotel.com - -berlinpestcontrol.com - -best-jobs-online.com - -bestdayeversweeps.com - -bestflirt4u.com - -bestlearninginstitutes.com - - bettiest.site - - bgskh.org - -bgwib04172nd.today - -bierengezondheid.com - -big-today.life - -bigbeaksbirdtoys.com - -bigbossports.com - -bigmanmevlana.info - - bigmed.info - - bijoubrio.com - - billairs.com - -billbrains.com - -billchickens.com - - billdark.com - -billerases.com - - billfacts.com - - billfaded.com - -billincandescent.com - - billlarge.com - -billoperate.com - -billpatches.com - - billpots.com - -billquests.com - - billroomy.com - - billtight.com - -bioregions.cfd - - -biqund.com - -birdsidedesign.com - - biruowxw.com - -bizterjios.click - -bizterok.click - - blackber.sbs - -blackporn.tube - -blbul25.beauty - - blmh1.com - - blmh2.com - - blmh4.com - - blmh5.com - - blmh6.com - - blmh7.com - -bluegroundstar.shop - -bluetones.info - - blushmeet.xyz - - blystress.com - -bobknowsphones.com - -bobritonebanditot.xyz - - boiler12.bond - - bolsters.cfd - -bond-cloud.com - -bond-corner.com - - bond-dash.com - - bond-spot.com - -bond-vibes.com - -bondmate-hub.com - -bonuswheel.club - - bopraadw.ru - - botonex.com - - boustahe.com - -bpushydos.click - -bradleyprint.work - - brafico.xyz - -brayerventophew.com - -broadcaster.icu - -brooklynhardcandy.com - - bsasukl.store - - bsine.cfd - - bsinessin.cfd - - bsinesss.cfd - - bthuayuhb.com - - buine.cfd - -bukalammuliparish.org - -burningcrave.com - - burnkiss.com - -burnwhisper.com - - busakind.com - -buscasencuentras.net - - busin.cfd - -busniessin.cfd - - bussinees.cfd - - bussinss.cfd - -bustysluts.xyz - -buyolddomains.com - -buyviagracha.com - -c3a230ckp5mw.today - - -caard.site - -caardpro.online - - caardpro.site - -caardshop.site - -cacuocblog.com - -cafebullier.com - -caldedonne.com - - calland.cfd - - camsexxl.net - -canadagoose.name - -canadianembassypress.com - -capital-top-advance.sbs - -capital-top-ashprimeai.sbs - -capital-top-backer.site -" -capital-top-cashatlaslab.sbs -! -capital-top-cashboardai.sbs -! -capital-top-cashboardio.sbs -" -capital-top-cashboosthub.sbs -" -capital-top-cashboostpro.sbs - -capital-top-cashclimb.sbs -# -capital-top-cashcompassio.sbs -" -capital-top-cashcompassx.sbs - -capital-top-cashdecky.sbs -! -capital-top-cashflowlab.sbs -" -capital-top-cashflowplus.sbs -! -capital-top-cashflowpro.sbs -! -capital-top-cashfocusai.sbs -" -capital-top-cashforgelab.sbs - -capital-top-cashgenlab.sbs - -capital-top-cashgenx.sbs - -capital-top-cashgridai.sbs - -capital-top-cashhyper.sbs - -capital-top-cashhyper.site - -capital-top-cashlineai.sbs -! -capital-top-cashlinehub.sbs -" -capital-top-cashlineplus.sbs -! -capital-top-cashlinepro.sbs -" -capital-top-cashlinezone.sbs -! -capital-top-cashlogicai.sbs -" -capital-top-cashmatrixai.sbs -" -capital-top-cashmatrixio.sbs -! -capital-top-cashmeterio.sbs - -capital-top-cashmeterx.sbs - -capital-top-cashmode.sbs - -capital-top-cashnovaai.sbs -" -capital-top-cashnovaedge.sbs - -capital-top-cashnovaio.sbs - -capital-top-cashnovaq.sbs - -capital-top-cashnovaq.site - -capital-top-cashorbitq.sbs -! -capital-top-cashorbitq.site - -capital-top-cashpilotx.sbs -# -capital-top-cashpilotzone.sbs -# -capital-top-cashprimeedge.sbs -! -capital-top-cashprimeio.sbs - -capital-top-cashprimeq.sbs -! -capital-top-cashprimeq.site -" -capital-top-cashradarhub.sbs -! -capital-top-cashradario.sbs -" -capital-top-cashradarlab.sbs - -capital-top-cashradarx.sbs -! -capital-top-cashradarx.site -! -capital-top-cashscopeio.sbs -# -capital-top-cashshiftedge.sbs - -capital-top-cashsnap.sbs -" -capital-top-cashsparkhub.sbs -" -capital-top-cashsparkpro.sbs - -capital-top-cashstream.sbs -! -capital-top-cashstream.site -" -capital-top-cashstreamio.sbs -# -capital-top-cashstreampro.sbs -! -capital-top-cashstreamx.sbs -! -capital-top-cashtrackai.sbs -! -capital-top-cashtrailio.sbs - -capital-top-cashverse.sbs - -capital-top-cashverse.site -# -capital-top-cashvisionhub.sbs -" -capital-top-cashvisionio.sbs -# -capital-top-cashvisionlab.sbs -! -capital-top-cashvisionx.sbs -! -capital-top-creditatlas.sbs -" -capital-top-creditatlas.site -" -capital-top-creditatlasx.sbs - -capital-top-creditbeam.sbs -! -capital-top-creditboard.sbs -" -capital-top-creditboard.site -# -capital-top-creditboardai.sbs -" -capital-top-creditboardx.sbs -# -capital-top-creditboardx.site -# -capital-top-creditboostai.sbs -% -capital-top-creditboosthubx.sbs -# -capital-top-creditboostio.sbs -" -capital-top-creditboostx.sbs -# -capital-top-creditbridge.site -# -capital-top-creditchainio.sbs -" -capital-top-creditchainx.sbs -% -capital-top-creditcompassio.sbs -# -capital-top-creditcorehub.sbs -# -capital-top-creditcorelab.sbs -! -capital-top-creditcorex.sbs -! -capital-top-creditcraft.sbs -" -capital-top-creditcraft.site - -capital-top-creditdash.sbs -! -capital-top-creditdash.site -# -capital-top-creditdashhub.sbs -# -capital-top-creditdashlab.sbs -# -capital-top-creditdashpro.sbs -! -capital-top-creditdashx.sbs -! -capital-top-creditdeckz.sbs - -capital-top-creditdock.sbs -# -capital-top-creditedgehub.sbs -$ -capital-top-creditengineai.sbs -" -capital-top-creditflowai.sbs -$ -capital-top-creditflowedge.sbs -# -capital-top-creditflowhub.sbs - -capital-top-creditflux.sbs -! -capital-top-creditflux.site -# -capital-top-creditfluxlab.sbs -! -capital-top-creditfluxx.sbs -" -capital-top-creditfluxx.site -$ -capital-top-creditfocuspro.sbs -$ -capital-top-creditforgelab.sbs -$ -capital-top-creditforgepro.sbs -" -capital-top-creditforgeq.sbs -# -capital-top-creditforgeq.site -" -capital-top-creditforgex.sbs -# -capital-top-creditforgez.site - -capital-top-creditgenx.sbs -! -capital-top-creditgenx.site -" -capital-top-creditgridai.sbs -! -capital-top-creditgridq.sbs -# -capital-top-credithorizon.sbs -& - capital-top-credithorizonlab.sbs -" -capital-top-credithubai.site - -capital-top-credithubq.sbs -! -capital-top-credithubq.site -! -capital-top-credithyper.sbs -" -capital-top-credithyper.site -$ -capital-top-credithyperlab.sbs -" -capital-top-credithyperx.sbs -# -capital-top-creditlinehub.sbs -" -capital-top-creditlogicx.sbs -# -capital-top-creditlogicx.site - -capital-top-creditlyze.sbs - -capital-top-creditmaxx.sbs -# -capital-top-creditmeterio.sbs - -capital-top-creditmode.sbs -! -capital-top-creditmode.site -" -capital-top-creditnovaai.sbs -$ -capital-top-creditnovaedge.sbs -" -capital-top-creditnovaio.sbs -" -capital-top-creditnovaq.site -! -capital-top-creditnovax.sbs -" -capital-top-creditnovax.site -! -capital-top-creditnovay.sbs -" -capital-top-creditnovay.site -! -capital-top-creditnovaz.sbs -" -capital-top-creditnovaz.site -$ -capital-top-creditorbitlab.sbs -" -capital-top-creditpathio.sbs -# -capital-top-creditpathlab.sbs -# -capital-top-creditpathpro.sbs -# -capital-top-creditpilotai.sbs -$ -capital-top-creditpilotlab.sbs -" -capital-top-creditpilotx.sbs -$ -capital-top-creditprimehub.sbs -% -capital-top-creditpulseedge.sbs -# -capital-top-creditpulseio.sbs -% -capital-top-creditpulsezone.sbs - -capital-top-creditsnap.sbs -! -capital-top-creditsnap.site -" -capital-top-creditstream.sbs -# -capital-top-creditstream.site -$ -capital-top-creditstreamio.sbs -% -capital-top-creditstreampro.sbs -# -capital-top-creditstreamx.sbs -$ -capital-top-creditstrideai.sbs -! -capital-top-credittorch.sbs -" -capital-top-credittrackr.sbs -$ -capital-top-creditvaulthub.sbs -$ -capital-top-creditvisionai.sbs -% -capital-top-creditvisionpro.sbs -" -capital-top-creditzoneai.sbs -# -capital-top-creditzonepro.sbs -$ -capital-top-creditzoneprox.sbs -! -capital-top-creditzonex.sbs -! -capital-top-debtboardio.sbs -! -capital-top-debtchainai.sbs -! -capital-top-debtcorehub.sbs -! -capital-top-debtflowhub.sbs -! -capital-top-debtflowlab.sbs - -capital-top-debtfluxx.sbs -! -capital-top-debtfocusai.sbs -" -capital-top-debtfocuspro.sbs -" -capital-top-debtforgehub.sbs - -capital-top-debtlayer.sbs -! -capital-top-debtlinehub.sbs -# -capital-top-debtlogicedge.sbs -" -capital-top-debtlogicpro.sbs - -capital-top-debtlogicx.sbs - -capital-top-debtnova.sbs - -capital-top-debtorbit.sbs -" -capital-top-debtorbitlab.sbs -" -capital-top-debtoriginai.sbs -# -capital-top-debtoriginlab.sbs -# -capital-top-debtoriginpro.sbs -! -capital-top-debtoriginx.sbs -$ -capital-top-debtoriginzone.sbs - -capital-top-debtprime.sbs -! -capital-top-debtprimeai.sbs -" -capital-top-debtprimehub.sbs -" -capital-top-debtprimepro.sbs - -capital-top-debtprimex.sbs -" -capital-top-debtradarpro.sbs -# -capital-top-debtradarzone.sbs - -capital-top-debtriseai.sbs -" -capital-top-debtriseedge.sbs -! -capital-top-debtrisehub.sbs - -capital-top-debtriseio.sbs -! -capital-top-debtrisepro.sbs - -capital-top-debtroute.sbs - -capital-top-debtsafe.sbs - -capital-top-debtscan.sbs - -capital-top-debtscanio.sbs - -capital-top-debtscanx.sbs -! -capital-top-debtscopeai.sbs -" -capital-top-debtscopepro.sbs - -capital-top-debtsnapai.sbs - -capital-top-debtsnapio.sbs - -capital-top-debtsnapx.sbs -" -capital-top-debtsparkhub.sbs -" -capital-top-debtsparkpro.sbs -" -capital-top-debtsphereio.sbs -! -capital-top-debtspherex.sbs -# -capital-top-debtvisionpro.sbs -! -capital-top-debtvisionx.sbs - -capital-top-finatlas.sbs - -capital-top-finatlasio.sbs -! -capital-top-finatlaslab.sbs -! -capital-top-finatlaspro.sbs - -capital-top-finaxispro.sbs - -capital-top-finaxisq.sbs -! -capital-top-finboardlab.sbs - -capital-top-finboardx.sbs - -capital-top-finboostai.sbs -! -capital-top-finboosthub.sbs - -capital-top-finboostio.sbs - -capital-top-finchain.sbs -! -capital-top-finchainlab.sbs - -capital-top-finchainz.sbs - -capital-top-finlayerx.sbs - -capital-top-finlogicio.sbs - -capital-top-finlogicx.sbs -" -capital-top-finlogiczone.sbs - -capital-top-finloop.sbs - -capital-top-finmatrixx.sbs - -capital-top-finora.sbs - -capital-top-finora.site - -capital-top-finorbitai.sbs -! -capital-top-finorbitlab.sbs -! -capital-top-finorbitpro.sbs -" -capital-top-finorbitzone.sbs - -capital-top-finoriginx.sbs - -capital-top-finpathlab.sbs - -capital-top-finpilotio.sbs -! -capital-top-finpilotpro.sbs - -capital-top-finpilotx.sbs - -capital-top-finprimeq.sbs -! -capital-top-finpulsehub.sbs - -capital-top-finriseai.sbs - -capital-top-finrisehub.sbs - -capital-top-finriseio.sbs - -capital-top-finrisepro.sbs -! -capital-top-finriseprox.sbs -! -capital-top-finrisezone.sbs - -capital-top-finshiftai.sbs - -capital-top-finshiftio.sbs -! -capital-top-finshiftpro.sbs -! -capital-top-fintracehub.sbs - -capital-top-fintraceio.sbs -! -capital-top-fintrailhub.sbs - -capital-top-finverse.sbs - -capital-top-finverse.site -! -capital-top-finversehub.sbs - -capital-top-finverses.sbs -" -capital-top-finvisionhub.sbs -" -capital-top-finvisionlab.sbs -" -capital-top-finvisionpro.sbs - -capital-top-finvisionx.sbs - -capital-top-finzonepro.sbs -! -capital-top-fundatlasio.sbs -" -capital-top-fundatlaslab.sbs - -capital-top-fundatlasx.sbs - -capital-top-fundaxisz.sbs - -capital-top-fundaxisz.site - -capital-top-fundbeamio.sbs -! -capital-top-fundboostai.sbs -# -capital-top-fundboostprox.sbs -" -capital-top-fundchainpro.sbs - -capital-top-fundchainx.sbs -$ -capital-top-fundcompasslab.sbs - -capital-top-fundcube.sbs -! -capital-top-fundflowlab.sbs - -capital-top-fundflowx.sbs - -capital-top-fundfocus.sbs -" -capital-top-fundfocushub.sbs -" -capital-top-fundfocuspro.sbs - -capital-top-fundfocusq.sbs - -capital-top-fundgate.sbs - -capital-top-fundgenix.sbs - -capital-top-fundhyper.sbs - -capital-top-fundhyper.site -! -capital-top-fundlinehub.sbs -! -capital-top-fundlinepro.sbs -" -capital-top-fundlinezone.sbs - -capital-top-fundlogic.sbs -! -capital-top-fundlogicai.sbs - -capital-top-fundlogicq.sbs - -capital-top-fundlogicx.sbs -! -capital-top-fundlogicx.site -" -capital-top-fundmatrixai.sbs -# -capital-top-fundmatrixpro.sbs -! -capital-top-fundmatrixx.sbs - -capital-top-fundmeter.sbs - -capital-top-fundnexus.sbs - -capital-top-fundnovaai.sbs -" -capital-top-fundnovaedge.sbs - -capital-top-fundnovaio.sbs - -capital-top-fundnovaq.sbs -! -capital-top-fundorbitai.sbs -" -capital-top-fundorbitpro.sbs - -capital-top-fundorbitq.sbs - -capital-top-fundorbitz.sbs - -capital-top-fundpathai.sbs -! -capital-top-fundpathpro.sbs -" -capital-top-fundpilotlab.sbs -" -capital-top-fundpilotpro.sbs -# -capital-top-fundprimeedge.sbs -" -capital-top-fundprimehub.sbs -! -capital-top-fundpulseai.sbs - -capital-top-fundpulseq.sbs - -capital-top-fundriseai.sbs -" -capital-top-fundriseedge.sbs - -capital-top-fundriseio.sbs -! -capital-top-fundrisepro.sbs -" -capital-top-fundrisezone.sbs - -capital-top-fundrocket.sbs -! -capital-top-fundscopeio.sbs - -capital-top-fundshiftq.sbs - -capital-top-fundsnap.sbs - -capital-top-fundsnap.site -" -capital-top-fundsparkhub.sbs -# -capital-top-fundsparkplus.sbs -" -capital-top-fundsparkpro.sbs - -capital-top-fundsparkq.sbs - -capital-top-fundsparkz.sbs -! -capital-top-fundsparkz.site -! -capital-top-fundstreamq.sbs -" -capital-top-fundstreamq.site -! -capital-top-fundtraceio.sbs - -capital-top-fundtracex.sbs -! -capital-top-fundtrackio.sbs -" -capital-top-fundtracklab.sbs -" -capital-top-fundtrackpro.sbs - -capital-top-fundtrackx.sbs - -capital-top-fundvision.sbs -! -capital-top-fundvision.site -" -capital-top-fundvisionai.sbs -! -capital-top-fundvisionx.sbs - -capital-top-fundwise.sbs - -capital-top-fundwise.site - -capital-top-fundzoneai.sbs -! -capital-top-fundzonehub.sbs -! -capital-top-fundzonepro.sbs - -capital-top-fundzonex.sbs - -capital-top-lendatlas.sbs - -capital-top-lendatlasx.sbs - -capital-top-lendboard.site -! -capital-top-lendboostai.sbs -" -capital-top-lendboosthub.sbs -! -capital-top-lendboostio.sbs - -capital-top-lendboostx.sbs -! -capital-top-lendclimbai.sbs -! -capital-top-lendcorelab.sbs - -capital-top-lenddash.sbs -! -capital-top-lenddashhub.sbs - -capital-top-lenddeck.sbs - -capital-top-lenddeckq.sbs - -capital-top-lendflowai.sbs -" -capital-top-lendflowzone.sbs -! -capital-top-lendfluxpro.sbs -! -capital-top-lendfocusai.sbs -" -capital-top-lendfocushub.sbs -" -capital-top-lendfocuslab.sbs -" -capital-top-lendfocuspro.sbs -" -capital-top-lendfusionio.sbs - -capital-top-lendgenix.sbs - -capital-top-lendhyper.sbs -# -capital-top-lendlogicedge.sbs -" -capital-top-lendlogiclab.sbs -" -capital-top-lendlogicpro.sbs - -capital-top-lendlogicx.sbs -! -capital-top-lendlogicx.site -# -capital-top-lendmatrixhub.sbs - -capital-top-lendnova.sbs -" -capital-top-lendnovaedge.sbs - -capital-top-lendnovaq.sbs - -capital-top-lendorbitq.sbs -" -capital-top-lendoriginio.sbs -# -capital-top-lendoriginlab.sbs -# -capital-top-lendoriginpro.sbs -! -capital-top-lendoriginx.sbs - -capital-top-lendpathio.sbs -! -capital-top-lendprimeai.sbs -# -capital-top-lendprimeedge.sbs -" -capital-top-lendprimehub.sbs -! -capital-top-lendprimeio.sbs -# -capital-top-lendprimeplus.sbs - -capital-top-lendprimey.sbs -! -capital-top-lendprimey.site -# -capital-top-lendprimezone.sbs -! -capital-top-lendpulseai.sbs - -capital-top-lendpulseq.sbs - -capital-top-lendradar.sbs - -capital-top-lendradar.site - -capital-top-lendradarx.sbs - -capital-top-lendriseai.sbs -" -capital-top-lendriseedge.sbs -! -capital-top-lendscopeai.sbs -" -capital-top-lendscopehub.sbs -! -capital-top-lendscopeio.sbs -" -capital-top-lendscopepro.sbs - -capital-top-lendscopex.sbs -" -capital-top-lendshiftlab.sbs - -capital-top-lendsnap.sbs - -capital-top-lendsnap.site - -capital-top-lendsnapai.sbs -! -capital-top-lendsnappro.sbs - -capital-top-lendsnapx.sbs -# -capital-top-lendspherepro.sbs - -capital-top-lendstream.sbs -" -capital-top-lendstreamio.sbs -! -capital-top-lendstreamx.sbs -! -capital-top-lendtrackai.sbs -" -capital-top-lendvisionio.sbs -# -capital-top-lendvisionpro.sbs - -capital-top-lendzoneai.sbs -! -capital-top-lendzonehub.sbs - -capital-top-loanatlas.site -! -capital-top-loanaxispro.sbs - -capital-top-loanaxisq.sbs - -capital-top-loanbaseio.sbs - -capital-top-loanbeam.sbs -! -capital-top-loanboardio.sbs -" -capital-top-loanboostlab.sbs - -capital-top-loanboostx.sbs -" -capital-top-loanbridgeai.sbs - -capital-top-loanchart.sbs - -capital-top-loanchart.site -! -capital-top-loancompass.sbs -" -capital-top-loancompassx.sbs - -capital-top-loanedgeai.sbs -! -capital-top-loanedgepro.sbs -! -capital-top-loanenginex.sbs -! -capital-top-loanflowlab.sbs - -capital-top-loanfluxio.sbs -" -capital-top-loanfocuslab.sbs -# -capital-top-loanforgeedge.sbs -" -capital-top-loanforgehub.sbs -! -capital-top-loanforgeio.sbs -" -capital-top-loanforgepro.sbs -! -capital-top-loangridpro.sbs - -capital-top-loanhubx.sbs - -capital-top-loanhubx.site - -capital-top-loanhyper.sbs - -capital-top-loanhyper.site -" -capital-top-loanliftedge.sbs - -capital-top-loanliftx.sbs - -capital-top-loanlogicx.sbs -! -capital-top-loanlogicx.site - -capital-top-loanmapio.sbs - -capital-top-loanmaxhub.sbs - -capital-top-loanmeter.sbs - -capital-top-loannext.sbs - -capital-top-loannext.site - -capital-top-loannextai.sbs -! -capital-top-loannextgen.sbs - -capital-top-loannexus.sbs - -capital-top-loannexus.site -" -capital-top-loannovaedge.sbs - -capital-top-loanorama.sbs -" -capital-top-loanorbitlab.sbs -" -capital-top-loanorbitpro.sbs - -capital-top-loanorbitx.sbs - -capital-top-loanorbitz.sbs -! -capital-top-loanorbitz.site -# -capital-top-loanorbitzone.sbs -# -capital-top-loanoriginlab.sbs -# -capital-top-loanoriginpro.sbs -! -capital-top-loanoriginx.sbs -! -capital-top-loanpathhub.sbs -! -capital-top-loanpathlab.sbs -! -capital-top-loanpathpro.sbs - -capital-top-loanpathx.sbs - -capital-top-loanpathx.site -" -capital-top-loanpathzone.sbs -! -capital-top-loanpilotai.sbs -! -capital-top-loanpilotgo.sbs -" -capital-top-loanpilotlab.sbs -! -capital-top-loanprimeai.sbs -# -capital-top-loanprimecore.sbs -" -capital-top-loanprimehub.sbs -! -capital-top-loanprimeio.sbs -# -capital-top-loanprimepath.sbs - -capital-top-loanprimeq.sbs -! -capital-top-loanprimeq.site -! -capital-top-loanprimex.site -" -capital-top-loanpulsehub.sbs -! -capital-top-loanpulseio.sbs - -capital-top-loanpulseq.sbs -! -capital-top-loanpulseq.site - -capital-top-loanpulsez.sbs -! -capital-top-loanpulsez.site -" -capital-top-loanradarlab.sbs - -capital-top-loanriseio.sbs - -capital-top-loanriseup.sbs -! -capital-top-loanriseup.site - -capital-top-loanscanx.sbs -! -capital-top-loanshiftai.sbs -# -capital-top-loanshiftedge.sbs -" -capital-top-loanshiftpro.sbs - -capital-top-loansnap.sbs - -capital-top-loansnap.site - -capital-top-loansphere.sbs - -capital-top-loansprint.sbs -! -capital-top-loanstackio.sbs -" -capital-top-loanstreamai.sbs -" -capital-top-loantrackhub.sbs -! -capital-top-loantrackio.sbs -" -capital-top-loantracklab.sbs -! -capital-top-loanvantage.sbs -" -capital-top-loanvantage.site -# -capital-top-loanvaultedge.sbs - -capital-top-loanvaultx.sbs - -capital-top-loanversex.sbs -! -capital-top-loanversex.site -" -capital-top-loanvisionai.sbs -# -capital-top-loanvisionhub.sbs -" -capital-top-loanvisionio.sbs -# -capital-top-loanvisionpro.sbs -! -capital-top-loanvisionq.sbs -! -capital-top-loanvisionx.sbs - -capital-top-loanxpress.sbs -" -capital-top-loanxpressio.sbs - -capital-top-loanzero.sbs - -capital-top-loanzero.site - -capital-top-loanzilla.sbs - -capital-top-loanzoneio.sbs -! -capital-top-loanzonepro.sbs - -capital-top-loanzonex.sbs - -capital-top-payboostio.sbs -! -capital-top-payboostiox.sbs - -capital-top-paybridgex.sbs -! -capital-top-paychainhub.sbs - -capital-top-paychainy.sbs - -capital-top-payclimbio.sbs - -capital-top-paycorehub.sbs - -capital-top-paycorelab.sbs - -capital-top-paycorex.sbs - -capital-top-paydashai.sbs - -capital-top-paydashx.sbs - -capital-top-payfluxio.sbs - -capital-top-payfluxx.sbs -! -capital-top-payfocushub.sbs -! -capital-top-payfocuslab.sbs -! -capital-top-payfocuspro.sbs -" -capital-top-payfocuszone.sbs - -capital-top-paygridpro.sbs -! -capital-top-paymatrixai.sbs -" -capital-top-paymatrixlab.sbs - -capital-top-paymatrixx.sbs - -capital-top-paynova.sbs - -capital-top-paynovaai.sbs -! -capital-top-paynovaedge.sbs - -capital-top-paynovaq.sbs - -capital-top-payorbit.sbs -" -capital-top-payoriginhub.sbs - -capital-top-payprime.sbs -" -capital-top-payradarzone.sbs - -capital-top-payrisehub.sbs -! -capital-top-paystreamai.sbs -! -capital-top-paystreamio.sbs - -capital-top-paystreamx.sbs - -capital-top-paystride.sbs -! -capital-top-paytrackhub.sbs - -capital-top-paytrackio.sbs -! -capital-top-paytracklab.sbs -! -capital-top-paytrackpro.sbs - -capital-top-payverse.sbs -" -capital-top-payvisioniox.sbs -! -capital-top-payzoneedge.sbs - -capital-top-payzoneio.sbs - -capital-top-splitpay.sbs - -capital-top-sureloan.site - -capital-topcashflowpro.sbs -! -capital-topcashnovaedge.sbs - -capital-topcashpilotx.sbs -! -capital-topcashradarhub.sbs -" -capital-topcreditdashhub.sbs -# -capital-topcreditforgepro.sbs -! -capital-topcredithyperx.sbs -" -capital-topcreditmeterio.sbs - -capital-topfundpilotx.sbs - -capital-topfundzonehub.sbs -" -capital-toplendprimeedge.sbs -" -capital-toploanoriginlab.sbs - -capital-toploanpulsex.sbs -" -capital-toploanvaultedge.sbs -" -capital-toploanvisionpro.sbs - -capitaltop-borrowit.sbs - -capitaltop-borrowit.site - -capitaltop-cashboardai.sbs -" -capitaltop-cashcompassio.sbs - -capitaltop-cashcorehub.sbs - -capitaltop-cashflowlab.sbs - -capitaltop-cashflowpro.sbs - -capitaltop-cashgridai.sbs - -capitaltop-cashhyper.sbs - -capitaltop-cashhyper.site - -capitaltop-cashline.sbs - -capitaltop-cashline.site - -capitaltop-cashlinepro.sbs - -capitaltop-cashlogicx.sbs - -capitaltop-cashlogicx.site -! -capitaltop-cashmatrixai.sbs -! -capitaltop-cashnovaedge.sbs - -capitaltop-cashnovaio.sbs - -capitaltop-cashpilotx.sbs -! -capitaltop-cashradarhub.sbs -" -capitaltop-cashsparkedge.sbs - -capitaltop-creditbeam.sbs -" -capitaltop-creditboardai.sbs -$ -capitaltop-creditboosthubx.sbs -" -capitaltop-creditcorelab.sbs - -capitaltop-creditcraft.sbs -" -capitaltop-creditdashhub.sbs -" -capitaltop-creditdashpro.sbs - -capitaltop-creditfluxq.sbs -! -capitaltop-creditfluxq.site - -capitaltop-creditfluxx.sbs -! -capitaltop-creditfluxx.site -# -capitaltop-creditforgepro.sbs -! -capitaltop-creditforgex.sbs -! -capitaltop-creditforgez.sbs -" -capitaltop-creditforgez.site -% -capitaltop-credithorizonlab.sbs - -capitaltop-credithubq.sbs - -capitaltop-credithubq.site - -capitaltop-credithyper.sbs -! -capitaltop-credithyper.site -! -capitaltop-credithyperx.sbs -" -capitaltop-creditmeterio.sbs -# -capitaltop-creditnovaedge.sbs -# -capitaltop-creditprimehub.sbs -$ -capitaltop-creditpulseedge.sbs -" -capitaltop-creditpulseio.sbs -! -capitaltop-creditvision.sbs -" -capitaltop-creditvision.site -" -capitaltop-creditzonepro.sbs - -capitaltop-creditzonex.sbs - -capitaltop-debtlinehub.sbs - -capitaltop-debtlinepro.sbs - -capitaltop-debtlogicai.sbs -" -capitaltop-debtlogicedge.sbs -! -capitaltop-debtlogicpro.sbs -" -capitaltop-debtoriginhub.sbs -" -capitaltop-debtoriginpro.sbs - -capitaltop-debtoriginx.sbs - -capitaltop-debtprimex.sbs -! -capitaltop-debtradarpro.sbs - -capitaltop-debtriseai.sbs -! -capitaltop-debtriseedge.sbs - -capitaltop-debtriseio.sbs - -capitaltop-debtscopeai.sbs -! -capitaltop-debtsparkhub.sbs -! -capitaltop-debtsparkpro.sbs - -capitaltop-finatlasio.sbs - -capitaltop-finatlaspro.sbs - -capitaltop-finboardx.sbs - -capitaltop-finboostai.sbs - -capitaltop-finboosthub.sbs - -capitaltop-finchainlab.sbs - -capitaltop-finlogicpro.sbs - -capitaltop-finlogicx.sbs - -capitaltop-finmatrixio.sbs - -capitaltop-finorbitpro.sbs - -capitaltop-finpilotx.sbs - -capitaltop-finprimeq.site - -capitaltop-finriseai.sbs - -capitaltop-finriseedge.sbs - -capitaltop-finriseio.sbs - -capitaltop-finriseprox.sbs - -capitaltop-finshiftio.sbs - -capitaltop-finstreamai.sbs - -capitaltop-fintracehub.sbs -! -capitaltop-finvisionpro.sbs - -capitaltop-fundburst.sbs - -capitaltop-fundburst.site -# -capitaltop-fundcompasslab.sbs - -capitaltop-funddeckq.sbs - -capitaltop-funddeckq.site - -capitaltop-fundflow.sbs - -capitaltop-fundflow.site -! -capitaltop-fundfocuspro.sbs - -capitaltop-fundfusion.sbs - -capitaltop-fundfusion.site - -capitaltop-fundgenix.sbs - -capitaltop-fundgenix.site - -capitaltop-fundhyper.sbs - -capitaltop-fundhyper.site - -capitaltop-fundlogicq.sbs - -capitaltop-fundnovaai.sbs - -capitaltop-fundnovay.sbs - -capitaltop-fundnovay.site - -capitaltop-fundorbitai.sbs -" -capitaltop-fundprimeedge.sbs - -capitaltop-fundscopeio.sbs - -capitaltop-fundscopex.sbs -" -capitaltop-fundsparkedge.sbs - -capitaltop-fundtracez.sbs - -capitaltop-fundtracez.site -! -capitaltop-fundtrackpro.sbs - -capitaltop-fundzonehub.sbs - -capitaltop-fundzonepro.sbs - -capitaltop-lendboostio.sbs - -capitaltop-lendflow.sbs - -capitaltop-lendflow.site - -capitaltop-lendfluxpro.sbs - -capitaltop-lendhyper.sbs - -capitaltop-lendhyper.site -" -capitaltop-lendmatrixhub.sbs -" -capitaltop-lendoriginlab.sbs -" -capitaltop-lendprimeedge.sbs -" -capitaltop-lendprimezone.sbs -! -capitaltop-lendriseedge.sbs - -capitaltop-lendscopeio.sbs - -capitaltop-lendsnapio.sbs -" -capitaltop-lendvisionhub.sbs -" -capitaltop-lendvisionpro.sbs - -capitaltop-loan.sbs - -capitaltop-loan.site -! -capitaltop-loanboostlab.sbs - -capitaltop-loanboostx.sbs - -capitaltop-loanedgex.sbs - -capitaltop-loanedgex.site - -capitaltop-loanflowlab.sbs - -capitaltop-loanhyper.sbs - -capitaltop-loanhyper.site - -capitaltop-loanliftx.sbs - -capitaltop-loanlink.sbs - -capitaltop-loanlink.site - -capitaltop-loanmapio.sbs - -capitaltop-loannextai.sbs - -capitaltop-loannextgen.sbs -! -capitaltop-loannovaedge.sbs -! -capitaltop-loanorbitpro.sbs - -capitaltop-loanorbitq.sbs - -capitaltop-loanorbitq.site -" -capitaltop-loanoriginlab.sbs -" -capitaltop-loanoriginpro.sbs - -capitaltop-loanpathhub.sbs -" -capitaltop-loanprimepath.sbs - -capitaltop-loanpulsex.sbs - -capitaltop-loanpulsex.site - -capitaltop-loanshiftq.sbs - -capitaltop-loanshiftq.site - -capitaltop-loantrackr.sbs - -capitaltop-loantrackr.site -" -capitaltop-loanvaultedge.sbs -" -capitaltop-loanvisionhub.sbs -" -capitaltop-loanvisionpro.sbs - -capitaltop-paycorehub.sbs - -capitaltop-paydashx.sbs - -capitaltop-payfluxio.sbs - -capitaltop-payfocushub.sbs - -capitaltop-payfocuslab.sbs -! -capitaltop-paymatrixlab.sbs - -capitaltop-paymatrixx.sbs - -capitaltop-paynovaedge.sbs - -capitaltop-paystreamai.sbs - -capitaltop-paytrackpro.sbs - -capitaltop-payzoneedge.sbs - -capitaltop-payzoneio.sbs - -captcartoon.com - -captchaless.top - - carcred.store - - carddd.shop - -cardiaccoogs.com - -carlcloudforge.work - -casasjuntoalmar.com - - casedaze.com - -cashmoneyplace.com - -cashsearchfive.com - -cashsearchthree.com - -cassinolandia.com - -catchurlove.com - - -cation.icu -# -cavespringmotorcyclerally.com - - ccard.store - - ccards.space - -ccredrock.site - - -cdsyjt.com - -cefectiptypts.com - - centranow.com - -cestagrande.com - -cfrdgrowwisek.today - -cfrdmaxlevelk.today - -cfrdnewstartk.today - -cfrdnextstepk.today - -cfrdstreamitk.today - -cfrdstrongerk.today - -cfrdworkmatek.today - -cfrgbestplank.today - -cfrggrowwisek.today - -cfrginsightxk.today - -cfrglearningk.today - -cfrgmodernxk.today - -cfrgopenmindk.today - -cfrgskyhighxk.today - -cfrgworkplank.today - -cfrpboostwayk.today - -cfrpbrightiqk.today - -cfrpclarifyxk.today - -cfrpconnectzk.today - -cfrpdatapathk.today - -cfrpdriveupk.today - -cfrpeasyworkk.today - -cfrpflowpathk.today - -cfrpgogetterk.today - -cfrpimpactzk.today - -cfrpinsightxk.today - -cfrplearningk.today - -cfrplevelupxk.today - -cfrpmaxlevelk.today - -cfrpmaxscalek.today - -cfrpmodernxk.today - -cfrpnetworkk.today - -cfrpnewslinek.today - -cfrpnextstepk.today - -cfrpoptimizek.today - -cfrpplanwisek.today - -cfrpresultxk.today - -cfrpsearchitk.today - -cfrpskyhighxk.today - -cfrpstreamitk.today - -cfrpsuccessk.today - -cfrpteamcorek.today - -cfrpvisionfxk.today - - chaotick.com - -charlestrodet.com - -chase-date.com - -chatwith-babe.com - -cheapperfume.shop - -chgutscheinseite.com - -chicks2sex.com - - chigger.top - -chinangling.com - - chmatric.com - -choicegoldcard.com - -cholymascry.com - -chriscyberchef.work - -churchbelfry.cfd - -churchbell.cfd - -churchhandbook.com - -chwiristal.com - -cladjurobite.com - -claimcompensation.top - - clappers.cfd - -classifiedsflow.com - -classroomchampion.com - -claudejacques.com - -clean-find.com - -clean-join.com - -clearsolutionsbathware.com - - clf-law.com - -click-circle.com - -click-vibes.com - -click2win4life.com - - click4fun.xyz - -click4riches.info - -clickandblush.xyz - -clickdealsess.space - -clicknkisses.xyz - -clieleistace.com -" -cliffordchronobiologist.work - -cloud01fastload.live - -clumbicomas.com - - cochz.com - -codesequencing.com - - coindhaba.com -! -colegioinstitutoamerica.com - -coloradostonecompany.com - -commitmate.xyz - -communicatv.icu -! -comovalomionacionalidad.com - -compathetic.sbs - -comunamea.info - - conecar.store - - conewws.com - -connect-corner.com - -connectivity.cfd - -connorcarpenter.work - - conomyand.icu - - cononmy.icu - -constrcreatlon.cc - -contactosrapidos.com - -containsnature.cfd - -copywritingthe.cfd - - cording.icu - -corenetbridge.com - -cormaccartographer.work - - cosclours.com - -cosmically.sbs - -cosmicrocosmos.sbs - -cosplay-lust.com - - cosulten.com - -cottonalleycafe.com - - couplevo.com - -cozybond-club.com - - cravehook.org - -credicredit.shop - - credione.shop - -credione.store - -credisole.online - - creditwo.shop - -creditwo.store - - credoi.online - - creedi.shop - - creedi.site - - creedi.store - -creeditech.online - -creeditech.site - -creeptemplate.com - -cricaticid.com - -crush-circle.com - -crush-meet.com - -crush-place.com - -crush-portal.com - -crush-vibes.com - -crush-wave.com - -crypticoins.com - -cryptohoppoer.com - - ctmmataro.com - -cupcupmrkt.xyz - - cupidabo.com - -cupidsting.com - -curious-match.com - -custodyaid.com - -cyberbrigada.com - -cz-infoliveinfo.org - - d-goodfin.xyz - -dafrpoptimizek.today - -dafrptechcorek.today - -dailysurgenow.com - -damiendesign.work - -danceenglist.cfd - -dandatadoc.work - -dante3dartist.work - -dapurpasaran.info - - darlinghq.com - -databasets.cfd - -datatistical.cfd - - date-flow.com - -date-inyourarea.com - - date-lane.com - - dateable.sbs - -datearoundyou.com - - dateaura.xyz - -datecrushhub.com - -datecurrent.com - -dateexotics.com - - dateluxe.xyz - - datelyst.xyz - -datenow.online - -datesandads.top - - dateshop.biz - -datesphere.monster - -dateygo.monster - -dateyourheart.net - -dating-sweeties.com - -dating2locals.com - - datingeo.com - -datingihun5.xyz - - datingles.sbs - -datingpoint.top - -datingswipe.monster - - datingthe.icu - -datlngplace.com - -dax1.com - -dazzlecase.com - - ddcard.shop - - ddcard.site - - ddcard.space - - ddcard.store - - ddcards.space - - ddcred.shop - - ddcred.site - - ddcred.space - - ddcred.store - - ddcrosy.site - - ddcrosy.space - -debilosempire.org - - debsedfut.com - -deconterpret.cfd - -decordesignsdecals.com - -deepchronemata.com - -deepsyllogramica.com - -deepxenometronica.com - -degenerateallstars.xyz - -deinimmochcheck.com -$ -delivery-associates-online.org - -demailmarkting.cfd - -demerixidan.com - - -demoes.cfd - - demotape.cfd - -desireflare.com - -destinybond.xyz - - deterpret.cfd - - devdhaba.com - -devicesafemode.com - - devotaf.xyz - -dewabola88.top - - dezis.sbs - -dgtjp6mh526g.today - -dianadendrologist.work - -dichdosism.com - -digitalised.cfd - - digitally.cfd - - -dinged.cfd - - dinging.cfd - - -dir50.club - -directandroidtips.com - -directupdater.com - -directviruses.com - - dirtyping.org - -discoverresultsfast.com - -discreet-transfer.com - -diversitydialogues.org - -dividendbooks.com - -dizzyhearts.com - - -dmobbs.xyz - - dock-bar.com - -dollar-gamble.xyz - - domaciweb.com - -domaindhaba.com - - dooperrr.sbs - -doradecologist.work - - dosaexpnj.com - -download4you.info - -draceneucost.com - -dreambikeuk.com - -drive-upgrade.top - -drovesafelyinnassau.com - -drpspeedway.today - - dwc-dates.com - - dwcflirt.com - -dymsw98d9xl6.today - - -dyttxm.com - - earmass.com - -easyflirts.xyz - -ecconomics.icu - - -ecnomy.icu - -ecocitybmt.com - -ecolabeliness.cfd - -ecolabelline.cfd - - ecomomics.icu - -ecomomythe.icu - -econonmics.icu - -edmondeditor.work - -efficiences.cfd - - -egopai.pro - -ehlkelawoffices.com - -ej79ajbssndb.today - -eli3dprint.work - -elitecardapproval.com - -elitedatehub.com - -elliotengineer.work - -emailmarcomms.cfd - -emilioecologist.work - -emotionlessart.com - - emphemice.com - - emutasa.xyz - - -enburu.pro - - eness.cfd - - -enofil.pro - - -enogup.pro - -ensreisted.com - -enthusiastpc.net - -environmential.cfd - -epharpunderae.com - - -epixot.com - - ercsubmit.com - - erenpit.top - -ernestoentomologist.work - -eroticflame1.com - -eroticpulse1.com - - eslwatch.info - -esnjdgm8qbt7.today - -essentialoilblogging.com - - esslymph.com - - estuaye.xyz - - etotave.sbs - -etworksthe.cfd - -etxcarrentals.com - -euctimakate.com - -eustaceembalmer.work - -everviveres.com - -evista-ehs.com - -exceptionaldates.net - - exigidos.com - - exikakid.sbs - -exoplanetoids.cfd - - -expend.icu - -expergomarketing.com - -expertjobmatch.com - -exploravip.com - -explorethebesttoday.com - -exploretoday.co - -exproscropure.com - - -extras.cfd - - eyakikoke.sbs - - eyiweqivi.sbs - - ez-dates.com - -f1l2oxplnogp.today - - faboxiy.sbs - -factory4love.com - -farmersmulchandrock.com - -fashionaqjkblogspot.com - -fastdatingero.com - -fastflirtz.xyz - - feihudy.com - -fewer-jumps.com - -ffbli8kxjvhp.today - - filicaces.com - -filmcratez.com - -financialassistance4me.com - - find-hot.date - - find-line.com - -find-me-a.date - -find-me-my.date - -find-singles-online.com - -find-your.date - -findresourcesusa.com - -findshortsmall.com - -fionafolklorist.work - -first1promo.site -" -firsthealthmedicalclinic.com - -fitbitonline.com - -fitconnect.monster - -fitnesalasinia.com - -fitwu1ct2ke7.today - -fitzgeraldfarrier.work - - fixarion.sbs - - fixsolo.com - - flamewild.com - - flaredup.xyz - - flaretech.net - -flaviofungiculturist.work - -flavorfulkitchenideas.com - -flfdatings.com - - flfmatch.com - - flfmeets.com - -flightsearchdirect.com - - -flirra.org - -flirt-avenue.com - -flirt-corner.com - -flirt-hive.xyz - -flirt-line.com - -flirt-ring.com - -flirtandlucky.com - -flirtbase-time.com - - flirties.live - -flirtifydday.xyz - -flirtislive.pro - - flirtlane.pro - -flirtquickly.pro - -flirtwithcrush.com - -flirty-circle.com - -flirtychat.online - -flirtyfate.xyz - -flirtypussies.com - - -flirvo.org - -flowtubelive.com - -fluxmatic.space - - flyrona.org - -fnewcreditforyoucom.com - -fomoneatorican.com - -foodstampsupport.net - - fordletze.com - -forevertwo.xyz - -forkitdiet.com - -formanceof.cfd - -forthwanderers.com - -fotofilminfinity.com - -fotto-cubi.com - -foundmoneyguide.com - -fraindaphia.com - -frankfuturefarmer.work - -frcforparients.com - -frcfprpatients.com - -frdspeedprok.today - -freedomlender.co - -fresh-thrill-date.com - -frgstrategyk.today - -frgsuccessk.today - -frontlineselling.net - -frostpassion.com - -frplogicnetk.today - -frpmarketupk.today - -frpmaxpowerk.today - -frpmodernxk.today - -frpskyhighxk.today - -frpsmartnetk.today - -frpsuccessk.today - -frpupscalek.today - -fruitful-connections.com - - fuck-more.com - -fundatesonly.xyz - -funky-minglers.com - -funsoplous.com - -fusiondates.com - - fuxxx.com - - -fynweb.com - -gabriellegemologist.work - -gamecreditfreenoshare.com - - gamekaifa.com - -gamingtruco.top - - gampheste.com - -garygamewarden.work - - gathetize.cfd - -gbmtfasteners.com - - gbrownie.com - - gecupot.sbs - -gehealthbenfits.com - -gehealthbenifits.com - - genyonred.com - -geomarketer.cfd - -georgiaendocrine.com - - german0.xyz - -geryshairdesign.com - -get-a-hot.date - -get-me-my.date - -getlaid-snaphookupna.com - - getlaidx.com - - getlaidxx.com - -getlaidxxx.com - -getlastnews.com - -getmore-only.today - -getmypriz.store - -getnggoods.com - -getngooods.com - -getnngoods.com - - getschema.org - - gettranny.com - -getyoudate.com - - gfx-tools.com - - ghykjou.com -- -'giftaskyworsadioldsigngoogeljano.online - - giftpark.shop -# -giftywordsigngoogeljano.store - -giftyworlds.online - - gioecav.xyz - -girl-4love.com - -girls-nearby.com - -girls4date.com - -girlsnearby.xyz - - girlsthey.sbs - -girlzsearch.com - - givelovr.com - -givestimnow.com - -glance-hub.com - -glimpse-vibe.com - -glistenfit.com - -globalestuariesforum.com - -globalvalueconnect.com - -glowmatches.xyz - -go2partnerprograms.com - - go4catnip.com - -go4topsecret.com - -goflirties69.com - - goflirtly.xyz - - goflirts.com - - gogagok.sbs - - gomusic.info - -goodgal-mansion.com - -goodslooke.net - - goodsnget.com - -goviralloop.org - -goyouniight.com - -grandcanyontu.org - -grantmethisgrantpls.com - - gray2day.site - -greatselections.co - -greenfruition.com - -gregorygeophysicist.work - -gruposerhumano.com - -gsd-adguard.pro - -gsx-adguard.pro - - -guital.sbs - - gutendia.com - - -gvrycb.top - -gxgr0l2fh6kd.today - - gxyichun.com - -h2noturfutah.com - - hadesex.com - -handcraftedartstudio.com - -handy-women.com - -hankhoverhunter.work - -hansendamhorsecenter.com - -harlandhunter.work - -harrisonholographer.work - -harryholotech.work - -harryhorticulturist.work - - -hbhgaj.com - - -hclips.com - - hdzog.com - -healthplanscouts.com - -heartclique.com - - heat-core.com - -heataddicts.com - - heatspree.com - - heiyatu.com - -hellutolow.com - - helperi.com - - hempcans.com - - herfinder.com - - hetnu.com - - hewunimo.sbs - -hexpuntrack.com - -hgetechnologyec.com - - -hgpt4u.com - - hgtmatch.com - - hhgjs99.com - - hicatalic.com - -highonpoems.com -! -hildegunnhawleyantiques.com - - hinasokyo.xyz - -historydatings.icu - - hjtts.co.in - -hoaglanetx.cfd - -hollyshirley.com - -homeandgardenideas.com - -homedecort.com - - -homets.cfd - - hookupers.com - - hoolca.rest - -horatiohorticulturist.work - -horizonharpist.work - - hornywish.com - -hostingrivieramaya.com - -hotbbavepa.today - -hotbbaxidi.today - -hotbbezaho.today - -hotbbihedu.today - -hotbbirixa.today - -hotbbitigi.today - -hotbbiyino.today - -hotbbizege.today - -hotbbodihu.today - -hotbbofoya.today - -hotbbofoye.today - -hotbbolemu.today - -hotbbupise.today - -hotbburise.today - - hotbcadayu.cc - -hotbcaseso.today - -hotbceguca.today - -hotbcehaze.today - -hotbcijuso.today - -hotbciwexi.today - -hotbcotajo.today - -hotbcuferu.today - -hotbcuforo.today - -hotbcunese.today - -hotbcusavu.today - -hotbdadehi.today - -hotbdakatu.today - -hotbdaluju.today - -hotbdavajo.today - - hotbdawogo.cc - -hotbdaxeyi.today - -hotbdetupi.today - -hotbdewecu.today - -hotbdivuyi.today - -hotbdokede.today - -hotbdonimi.today - -hotbdoxade.today - -hotbdoyega.today - -hotbduduya.today - -hotbdujixu.today - -hotbduyehi.today - -hotbfagopa.today - -hotbfanagi.today - -hotbfapoja.today - -hotbfasiyi.today - - hotbfenasu.cc - -hotbfevoda.today - -hotbfewapu.today - -hotbfidufi.today - -hotbfihime.today - -hotbfisopa.today - -hotbforusu.today - -hotbfotaji.today - -hotbfudupi.today - -hotbfuwame.today - -hotbgacada.today - -hotbgaguda.today - -hotbgajipa.today - -hotbgedaho.today - -hotbgenuba.today - -hotbgesaye.today - - hotbginoro.cc - -hotbgirayu.today - -hotbgobari.today - -hotbgobena.today - -hotbgososu.today - -hotbgovuba.today - - hotbgozijo.cc - -hotbgukaha.today - -hotbguwimo.today - -hotbguyehe.today - -hotbhatihu.today - -hotbhavovo.today - -hotbhemamo.today - -hotbhenimu.today - -hotbhesaju.today - -hotbhifega.today - -hotbhizigi.today - -hotbhobime.today - -hotbhofisu.today - -hotbholatu.today - -hotbhucito.today - -hotbhudavi.today - - hotbhunixi.cc - -hotbhuwuwi.today - -hotbigboobs.xyz - -hotbjacoyo.today - -hotbjaguri.today - -hotbjarama.today - -hotbjecagi.today - -hotbjejozo.today - -hotbjijane.today - -hotbjikiri.today - -hotbjimaji.today - -hotbjiputa.today - -hotbjofore.today - -hotbjofuko.today - - hotbjomeco.cc - -hotbjosiri.today - -hotbjuxonu.today - -hotbkacefa.today - -hotbkacevi.today - -hotbkakehi.today - - hotbkidoxa.cc - -hotbkivaxu.today - -hotbkiweji.today - -hotbkomoye.today - -hotbkorage.today - -hotbkoweda.today - -hotbkoxete.today - -hotbkuhexi.today - -hotbkupina.today - -hotblafejo.today - -hotblagawu.today - -hotblakome.today - -hotblayage.today - -hotblehecu.today - -hotblemoge.today - -hotblenulo.today - -hotblicife.today - -hotbliguti.today - -hotblimogo.today - -hotbliroxi.today - -hotblubehe.today - - hotbluviya.cc - -hotbluyami.today - -hotbmakoto.today - -hotbmarehu.today - -hotbmazemo.today - -hotbmazubu.today - -hotbmehaxa.today - -hotbmihaco.today - -hotbminuca.today - - hotbmizebu.cc - -hotbmizofa.today - -hotbmovuvu.today - - hotbmujusa.cc - -hotbmupigi.today - -hotbnafisu.today - -hotbnapami.today - -hotbnefuva.today - -hotbnemili.today - -hotbneyiho.today - -hotbniwula.today - -hotbnofake.today - -hotbnopuno.today - -hotbnosaka.today - -hotbnovoce.today - -hotbnuvope.today - -hotbpacoxa.today - -hotbpapela.today - - hotbpasixo.cc - -hotbpeguyu.today - - hotbpetuju.cc - -hotbpinaje.today - -hotbpixawo.today - -hotbpokuza.today - -hotbpoyoso.today - -hotbpugise.today - - hotbpuhipe.cc - -hotbpuholi.today - -hotbpujobo.today - -hotbpusewu.today - -hotbradani.today - -hotbranevo.today - -hotbratohi.today - -hotbrawuvo.today - -hotbraxabe.today - -hotbredemi.today - -hotbrekoha.today - -hotbreziko.today - -hotbriguve.today - -hotbrikeju.today - -hotbromefo.today - -hotbrubuzu.today - -hotbruwefe.today - -hotbsecija.today - -hotbsideci.today - -hotbsisesi.today - -hotbsisulu.today - -hotbsiwexu.today - -hotbsogici.today - -hotbsogumo.today - -hotbsovemu.today - -hotbsovode.today - -hotbsulaya.today - -hotbsuzufo.cyou - -hotbtajoka.today - -hotbtapepu.today - -hotbtapiku.today - -hotbtelafu.today - -hotbteperi.today - -hotbtepuwe.today - -hotbtifebo.today - -hotbtiwori.today - -hotbtopoxo.today - -hotbtoyewu.today - -hotbtuciba.today - -hotbtukoze.today - -hotbturuhe.today - -hotbvevazo.today - -hotbvocome.today - -hotbvoyugu.today - -hotbvuropo.today - -hotbwazape.today - -hotbwegocu.today - -hotbwehira.today - -hotbwezafe.today - - hotbwigaba.cc - -hotbwinepe.today - -hotbwinere.today - -hotbwoguri.today - -hotbwowami.today - -hotbwumomi.today - -hotbwuyade.today - -hotbxajola.today - -hotbxavaba.today - -hotbxawohu.today - -hotbxedoxi.today - - hotbxefofa.cc - -hotbxegoyu.today - -hotbxekace.today - -hotbxelolo.today - -hotbxicope.today - -hotbxogali.today - -hotbxusani.today - -hotbyalacu.today - -hotbyaxono.today - -hotbyihoce.today - -hotbyiputo.today - - hotbyojiko.cc - -hotbyomaga.today - -hotbyupiza.today - -hotbzabeco.today - -hotbzaloyu.today - - hotbzasime.cc - -hotbzayaji.today - -hotbzedazi.today - -hotbzexipi.today - -hotbzezilu.today - -hotbzojeke.today - -hotbzomaga.today - -hotbzomeki.today - -hotbzovoro.today - -hotbzozeco.today - -hotbzupaca.today - -hotbzuzije.today - -hotdatespot.pro - -hotgirl4me.com - -hotintimacy1.com - -hotlocalnights.com - - hotmovs.com - -hotsexy-girl.com - -hotstreamz.com - - htfd6.org - - hubquake.com - -huissier-bonnamy.com - -hulondor.co.in - -humblelessons.com - -humblepuppy.info - -huntraticara.com - -hypermode4.cfd - -hypermodes.cfd - - hypotspa.com - -hysoctring.com - -i0kaqv48x1sv.today - -i5eyfi63xgcp.today - -ianinfinityfix.work - - idoariel.com - -ignite-bud.com - -ignite-flash.com - -igniteburst.com - -igniteurge.com - -igniteyourlust.com - -ii41.com - - iiqlmiu.com - -iitopgirll.net - - ikoce.com - - -ikunod.sbs - -ilovethisdance.com - -imilroshoors.com - -imp2.com - -incrackledleather.net - -indoredelhigawa.sbs - -industryfairhms.com - -inezichnologist.work - -info-feed.info - -info-mymilfs.com - - infooks.xyz - -informationvine.com - -infotodiscover.com - -infotofind.com - - ingeeric.com - -inigrattic.com - -inkjetbuzz.com - -inkomed-yar.com - -innovatrax.co.in - - -inporn.com - -installplay.online - -instant-bond.com - -instant-chatting.com - -instantmoan.com - -instantmoviezone.com - -insultingsign.com -" -integratedcaremanagement.com - - intellgen.sbs - -interenetcon.cfd -! -internationalfamilylaw.info - -internetcorkboard.com - - -intral.cfd - -intristient.com - -investigather.cfd - - ioslover.com - -irvinginventor.work - -isidoreichthyologist.work - -it-geniuses.com - -itagrosevroy.pro - -itamserovey.pro - - iteya.sbs - - iveqa.sbs - - ivoqasos.sbs - - ixanosoxe.sbs - -jadejuggler.work - - javbd38.com - - jawoopst.com - -jcxu4piyw47j.today - - jdating.icu - -jectuphyth.com - -jeffersoncopacourt.org - -jeffjovialjanitor.work - -jeffjunkjet.work - -jeromejurisprudent.work - -jeuxtriche.top - -jg64helygpfe.today - - jijuijopw.cc - - jirte.com - -jobdiagnosis.com - -joesrentals.com - -join-corner.com - - jonjmorin.com - -joshuajeweler.work - -joutersism.com - - -jownel.top - - joydate.xyz - -joyful-ride.com - - -joymii.org - - joyroad.shop - -jtzsjzk1by9b.today - -juanjuankan.com - - jubsaugn.com - - judynjeri.com - -juliannejournalist.work - - jumploom.shop - -jundaloges.com - - jupiters.cfd - - juriomely.com - -justindellojoio.com - -justtyoume.com - - -jxneyl.com - -jzxnxtyc0qkm.today - -k-advantage.com - - k-goodfin.xyz - -k1j3i00yvj7y.today - - kaaptai.com - - kalxm.sbs - - kbnnjsss.xyz - - kddatings.com - - kendall.vip - -kendrickkartographer.work - -kendrickkennel.work - -kenknowledgemaster.work - -kennethkitemaker.work - - -kenofa.xyz - - kewoqev.sbs - - kewusowe.sbs - - -kfboon.xyz - -kickfitday.com - -kiefsvestajavel.space - -kierankyakguide.work -% -kimladesignsphotographyblog.com - -kinganlind.com - -kingfisherkeeper.work - - kinkblitz.com - - kiss-flow.com - -kiss-portal.com - -kisscurrent.com - -kissignite.com - - kissnraw.com - -kizanmitra.com - - kkjam.com - -knelltower.cfd - -kohlsfeednack.com - -kok33lazbauf.today - -kokodekchicken.com - - -kole.click - -kometkomedi.com - -kpriscilla.com - -kq9e.com - -kristal-ruse.com - -ktrackgg.store - - -kuikee.com - -kulinermania.com - -kunskapsaker.com - -kvarcureeny.com - -kylekaleidocraft.work - - l-goodfin.xyz - - labedafaok.ru - - ladyinhot.com - -laermorous.com - -lamesaspinecenter.com - -languayveness.sbs - - lanonhal.com - - lates.icu - -latina-match.com - -lawrencelocksmith.work - -lawsections.com - -learnhowtotradebitcoin.com - - -lediko.sbs - -leilalepidopterist.work - -lekkertech.com - -lemax-spookytown.com - -leolightsculptor.work - -leonardluthier.work - -leportail.info - -lessdateads.top - - letme-now.com - -lfi4qd5i7k62.today - -licktaughigme.com - -likeamermaidhair.com - - limelegab.sbs - -lineuppussy.com - -link-corner.com - -link-vibes.com - -lintasline.com - - linuxchef.com - -lipsdating.com - -lisorknopainn.com - -list4trades.com - - livabilty.cfd - -livelaughlearnlove.com - -livequizwithu.com - -ljdlf05wsw9k.today - - ljkkboo.xyz - - -lnoool.xyz - -localbotrepair.com - -localcrush.xyz - -localhotdesire.com - -localjobmatching.com - -localpinkflirt.com - -locals-aggregate.com - -locatiseld.com - -lonciallycea.com - -lonlyandhorny.com - -lonneke-maarten.com - -look-for-fun.com - - look4here.xyz - -loooooooooove.sbs - -looooooveee.sbs - -looooveeeeeee.sbs - -love-bridges.com - -loveawaitsyou.com - -lovebuzz-time.com - - -loveee.icu - - lovefers.biz - -lovefusion.org - -loveleyla.site - -lovemagnet.monster - -lovemakingx.com - - lovembo.com - -lovevibehub.xyz - -lovevibeshub.xyz - -lsfeylwfql2l.today - -ltrackgg.store - -lucenwstnyjete.click - -lucianolimnologist.work - -lucky-findings.com - -luckyshark.boats - -luckyshark.fun - -lustcanyon.com - -lustexplosion.com - -lustflarezone.com - -lustflicker.com - -lustflicks.com - -lustfulheat.com - -lustpiston.com - -lustywaves.live - -luvnsearch.site - - -luvyoo.org - -luxdatingclub.com - -luxeromance.xyz - -luxmatchelite.com - -lvyou100fen.com - -machineism.com - -macroeconmy.icu - -macroeconony.icu - - -madepu.pro - - -magoru.pro - - maichne.com - -mainspired.icu - -make-some-love.com - -maketthese.cfd - - -mances.cfd - -mantagraphics.com - - mapmymilf.com - -maral25.beauty - -marcosmeteorologist.work - -marketthis.cfd - -martialartsflorida.com - -massivewealthtosuccess.com - -massmediai.cfd - -masspagemoney.com - -matchmakerlink.xyz - -matchmaking.icu - -matchmate-world.com - -matchskill.org - - matchvibe.xyz - -matchy-corner.com - -matchywave.com - - matewhirl.com - -maticarced.com - -mattmemechanic.work - -maycongtrinhtrungquoc.com - -mcocodshowerdoor.com - - mediain.cfd - - meecn.click - - meet-dash.com - -meetchonky.com - -meetintonight.com - -meetlocalsnowsx.site - -meetmehorny.com - - meetnaked.com - -meets-dating.com - -meetsforyou.space - -meetsinglemates.com - -meetyourone.com - -megalovr.store - -memoryanalyzer.com - - -meneus.xyz - -menitionitionas.com - -menpowermedia.com - - -mepe.click - - merogent.com - -messienavunkvka.click - -metalduplicator.com - -metohmenion.com - - metry.cfd - - mifii.cfd - - migeeles.com - -milagrokitchen.com - -milakubaro.com - -milf-radar.com - -milfforyou.org - - milfswantu.co - -mimisadwormy.com - -mingle-space.com - -mingle-spot.com - -mingle-wave.com - -minocapolle.com - -minotebuyer.com - -mirandamyrmecologist.work - - miss4man.com - -mmkxhoodnsth.today - - moanblast.com - -moanbridge.com - -moanstream.com - - mobcpaps.sbs - - mobdepd.sbs - - mobidefsp.cfd - -mobileninjaapp.com - -mocaverity.com - - mode4.cfd - - modehow.cfd - - modei.cfd - - modelhive.ink - -modernbritishclassic.com - -mojomediamasters.com - -moldcellcom.icu - -money-pilot-card.store - -money-pilot-cash.store - -money-pilot-core.space - -money-pilot-crew.art - -money-pilot-deals.store - -money-pilot-design.art - -money-pilot-hq.online - -money-pilot-icon.art - -money-pilot-new.site - -money-pilot-next.live - -money-pilot-next.site - -money-pilot-next.space - -money-pilot-nova.cc - -money-pilot-one.cloud - -money-pilot-point.cloud - -money-pilot-point.store - -money-pilot-power.live - -money-pilot-prime.xyz - -money-pilot-pro.art - -money-pilot-pro.cc - -money-pilot-pro.site - -money-pilot-pro.space - -money-pilot-pro.store - -money-pilot-star.space - -money-pilot-sync.cloud - -money-pilot-vision.site - -money-pilot-zone.cc - -money-pilot-zone.space - -moneyhub-academy.click - -moneyhub-actuary.sbs - -moneyhub-advisors.click - -moneyhub-advisors.sbs - -moneyhub-alpha.sbs - -moneyhub-altcoin.click - -moneyhub-altcoin.sbs - -moneyhub-assetspro.sbs - -moneyhub-audit.sbs - -moneyhub-automation.click - -moneyhub-automation.sbs - -moneyhub-balance.sbs - -moneyhub-balancepro.click - -moneyhub-balancepro.sbs - -moneyhub-bankingpro.sbs - -moneyhub-benchmark.click - -moneyhub-benchmark.sbs - -moneyhub-beta.sbs - -moneyhub-billing.click - -moneyhub-billing.sbs - -moneyhub-blog.click - -moneyhub-blog.sbs - -moneyhub-books.click - -moneyhub-budget.click - -moneyhub-budgeting.click - -moneyhub-budgeting.sbs - -moneyhub-capitalpro.sbs - -moneyhub-certify.click - -moneyhub-chart.click - -moneyhub-chart.sbs - -moneyhub-class.click - -moneyhub-cloud.click - -moneyhub-cloud.sbs - -moneyhub-coders.click - -moneyhub-coders.sbs - -moneyhub-collect.click - -moneyhub-collect.sbs - -moneyhub-consulting.click - -moneyhub-consulting.sbs - -moneyhub-contactless.click - -moneyhub-contactless.sbs - -moneyhub-control.click - -moneyhub-control.sbs - -moneyhub-corporate.click - -moneyhub-corporate.sbs - -moneyhub-course.click - -moneyhub-creditcard.sbs - -moneyhub-currency.click - -moneyhub-dashboard.click - -moneyhub-dashboard.sbs - -moneyhub-data.click - -moneyhub-data.sbs - -moneyhub-datacenter.click - -moneyhub-datacenter.sbs - -moneyhub-defi.click - -moneyhub-defi.sbs - -moneyhub-degree.click - -moneyhub-dev.click - -moneyhub-dev.sbs - -moneyhub-digitalpay.click - -moneyhub-digitalpay.sbs - -moneyhub-diversify.click - -moneyhub-diversify.sbs - -moneyhub-due.click - -moneyhub-due.sbs - -moneyhub-earnings.click - -moneyhub-earnings.sbs - -moneyhub-ebooks.click - -moneyhub-economy.click - -moneyhub-edu.click - -moneyhub-ewallets.click - -moneyhub-ewallets.sbs - -moneyhub-exam.click - -moneyhub-exchange.click - -moneyhub-exchanges.click - -moneyhub-exchanges.sbs - -moneyhub-expense.click - -moneyhub-expenses.sbs - -moneyhub-factory.click - -moneyhub-factory.sbs - -moneyhub-fintech.sbs - -moneyhub-forecast.click - -moneyhub-forecast.sbs - -moneyhub-futures.click - -moneyhub-gains.click - -moneyhub-gains.sbs - -moneyhub-global.sbs - -moneyhub-globalpay.sbs - -moneyhub-gold.sbs - -moneyhub-growthai.click - -moneyhub-growthai.sbs - -moneyhub-guide.click - -moneyhub-guide.sbs - -moneyhub-holdings.click - -moneyhub-holdings.sbs - -moneyhub-ico.click - -moneyhub-ico.sbs - -moneyhub-idopro.click - -moneyhub-idopro.sbs - -moneyhub-index.sbs - -moneyhub-insurance.sbs - -moneyhub-investplus.click - -moneyhub-investplus.sbs - -moneyhub-invoice.click - -moneyhub-invoice.sbs - -moneyhub-ipo.click - -moneyhub-ipo.sbs - -moneyhub-journal.click - -moneyhub-journal.sbs - -moneyhub-labs.click - -moneyhub-labs.sbs - -moneyhub-learn.click - -moneyhub-ledger.sbs - -moneyhub-lessons.click - -moneyhub-liquidity.sbs - -moneyhub-listing.click - -moneyhub-listing.sbs - -moneyhub-magazine.click - -moneyhub-magazine.sbs - -moneyhub-marketcap.click - -moneyhub-marketcap.sbs - -moneyhub-media.click - -moneyhub-media.sbs - -moneyhub-membership.click - -moneyhub-membership.sbs - -moneyhub-merchant.click - -moneyhub-merchant.sbs - -moneyhub-mining.click - -moneyhub-ml.click - -moneyhub-ml.sbs - -moneyhub-mortgage.sbs - -moneyhub-mortgages.sbs - -moneyhub-news.click - -moneyhub-news.sbs - -moneyhub-nft.click - -moneyhub-nft.sbs - -moneyhub-p2p.click - -moneyhub-p2p.sbs - -moneyhub-papers.click - -moneyhub-pay.click - -moneyhub-payout.click - -moneyhub-payouts.click - -moneyhub-payouts.sbs - -moneyhub-pension.sbs - -moneyhub-portal.click - -moneyhub-portal.sbs - -moneyhub-practice.click - -moneyhub-profitmax.sbs - -moneyhub-quiz.click - -moneyhub-ranking.click - -moneyhub-ranking.sbs - -moneyhub-receipts.sbs - -moneyhub-remittance.click - -moneyhub-remittance.sbs - -moneyhub-report.click - -moneyhub-report.sbs - -moneyhub-reports.click - -moneyhub-reports.sbs - -moneyhub-research.click - -moneyhub-research.sbs - -moneyhub-retirement.click - -moneyhub-retirement.sbs - -moneyhub-return.click - -moneyhub-return.sbs - -moneyhub-returns.sbs - -moneyhub-revenue.click - -moneyhub-revenueplus.click - -moneyhub-revenueplus.sbs - -moneyhub-roboadvisor.click - -moneyhub-roboadvisor.sbs - -moneyhub-roi.sbs - -moneyhub-saas.click - -moneyhub-saas.sbs - -moneyhub-safe.sbs - -moneyhub-savingsplus.sbs - -moneyhub-scale.click - -moneyhub-scale.sbs - -moneyhub-scaleup.click - -moneyhub-scaleup.sbs - -moneyhub-scheduler.click - -moneyhub-scheduler.sbs - -moneyhub-school.click - -moneyhub-seed.click - -moneyhub-seed.sbs - -moneyhub-seriesa.click - -moneyhub-seriesa.sbs - -moneyhub-server.click - -moneyhub-server.sbs - -moneyhub-services.click - -moneyhub-services.sbs -# -moneyhub-smartcontracts.click -! -moneyhub-smartcontracts.sbs - -moneyhub-stablecoin.click - -moneyhub-stablecoin.sbs - -moneyhub-stats.click - -moneyhub-stats.sbs - -moneyhub-stocksplus.sbs - -moneyhub-study.click - -moneyhub-subscribe.click - -moneyhub-subscribe.sbs - -moneyhub-systems.click - -moneyhub-systems.sbs - -moneyhub-tax.sbs - -moneyhub-tests.click - -moneyhub-trade.click - -moneyhub-training.click - -moneyhub-transfer.sbs - -moneyhub-trend.click - -moneyhub-trend.sbs - -moneyhub-trust.click - -moneyhub-trust.sbs - -moneyhub-tutorial.click - -moneyhub-tutorial.sbs - -moneyhub-unicorn.click - -moneyhub-unicorn.sbs - -moneyhub-university.click - -moneyhub-valuation.click - -moneyhub-valuation.sbs - -moneyhub-value.click - -moneyhub-value.sbs - -moneyhub-venture.click - -moneyhub-venture.sbs - -moneyhub-ventures.click - -moneyhub-ventures.sbs - -moneyhub-wallets.click - -moneyhub-wealthpro.sbs - -moneyhub-withdraw.sbs - -morsecives.com - -motoalianzamedellin.com - -motstacticret.com - -movies123-uk.click - - -mpw002.com - - -mrgay.tube - - mrmnc.com - -mtzuc3z9ws2a.today - - mujah.sbs - -multiversei.sbs - - mumoro.click - -murals.gallery - -murphymcmahonjewelers.com - -mv8orpfvrgbq.today - -my-prize-search.com - -my-prizes-search.com - -mycrushdates.com - -mydailysurges.com - - mydatess.com - -mydlysurge.com - -mydogplace.com - -mydollarwinner.com - -myfantasyss.com - -myfoodhelper.net - -mygiftcollection.com - - mylovemod.com - -myopinionpayout.com - -myositentrous.com - -myprizesearch-1.com - -mysampleshub.com - -mysocialsix.com - - myspaoa.org - -mytopclicks.club - -myydailysurge.com - -n6tbijt9lmha.today - -naaqzb875yf9.today - -nackedchiks.org - -nadianumismatist.work - -nakedpassionhub.com - -namoradasfofos.com - -nathanaeldan.pro - -nathananonanny.work - -naughtinghill.com - -naughtydatespot.xyz - -navixzuro.co.in - -needtowinbig.com - -needureyes.com - - nekih.sbs - -neonnomad.work - -neosclocktower.fans - -netconnection.cfd - -netofflove.com - -nettieneonartist.work - -newer-horizons.com - - -news09.biz - -newsreporthy.icu - -newsreports.icu - - newssip.icu - - newssitem.icu - -newtinder.dating - - -newtor.cfd - -newworldradionetwork.com - -nexaflow.co.in - -nexoraedge.co.in - - nextonill.com - -nightglowlove.com - -nightlane-vibes.com - -nightshadenavigator.work - -nighttalk-link.com - - nineartz.com - - -nito.today - -nk72e5sr9zhm.today - - nlperfolg.com - -nolanneurologist.work - -nonchopeth.com - - nontivene.com - -normoniartal.com - -nortonfirewall.com - -notadslife.com - - nothectic.com - - notiffit.com - -notifinfoback.com - - notifstar.com - -notiftravel.com - -notify-time.com - - notimoti.com - - notioname.com - - now-bdy.click - - now-bqp.click - -ntamdq80qep2.today - - nuceefal.com - -nuestrasmadres.com - - nutravibe.pw - -nuviasmilesmail.com - -nwstyneabsltne.click - -nyfitnesskeen.com - - nylon24.com - -oar2.com - -ob4lx2qsv2xc.today - - obracted.com - - occumous.com - - odiqeqoto.sbs - -offergarden.shop - -offertime.shop - - officexls.com - -oficialpaulabrandao.com - -ofperformace.cfd - - ohebefiy.sbs - - ohebenu.sbs - -ohmboyiran.com - -okmplijnuh.info - -oliveroptician.work - -omgsweeps.info - -onbypnaaxm8c.today - -oneawaytogypt.org - - onecrd.site - -onecredit.space - -onecredit.store - -onehooponelove.com - -onelollipop4two22.xyz - -onenightromance.club - -onesteptomeet.com - -onlinedatingthe.icu -& - onlinefitnesstransformations.com - -onlinesafetycontrol.com - - onlylove.is - - onlyron.com - -onlysinglesonline.com - - onlytik.com - - ooxxx.com - -opalphthalmologist.work - - opended.sbs - -orangoose.shop - - orent-a.xyz - - orilasma.com - - -ormace.cfd - - -ormanc.cfd - -oscaropticsmith.work - -oshirisuki.com - - oshoz.com -% -otherworldlyfootballmachine.org - - ovaxu.sbs - -oveldibly.co.in - -oveletarratly.com - -ovettablitte.com - - ovvroptyo.xyz - -owhp6x6499lu.today - -ozidumplingsbk.com - - ozudune.sbs - -paidviewpanel.com - - pair-club.com - -pair-corner.com - - pair-dash.com - - pair-flow.com - - pair-lane.com - -pair-vibes.com - - pair-wave.com - -palmenonurm.com - -para---isso.com - -parleteedial.com - - parosex.com - -passionatekiss1.com - -passionatemeet.com - -passionbed.com - -passionburstnow.com - -pastdesigners.com - - pathogrot.com - -paulparadoxsolver.work - -pck2c61xle03.today - -pcp5979pmgrp.today - - -pefoil.pro - - peformanc.cfd - -peformancethis.cfd - - -pegutu.pro - -pellucidpiffle.com - -people-wet.com - -peroformance.cfd - - -pesety.pro - -petalumagun.com - -peterpixelpro.work - - petrtv.info - -phatgiaoxuyenmoc.org - - phelitic.com - - philip25.xyz - -philosophyi.site - -phimsexvn1.com - - phonest.blog - - phosolica.com - - photo4u.org - -physicaltherapyjoint.com - - pickupher.com - - -picme.name - -picmymatch.xyz - -picturethesell.com - -picurgobiviated.com - - pillsen.info - -pindarotsjes.com - - pipairs.com - -pipchanges.com - - piplarge.com - -pipoperate.com - - pippots.com - - -pivake.xyz - -plastidip-gmbh.info - -playandmoan.com - -playgetngoods.com - -pleasureinflame.com - -pleasuremap.live - -plesuarpussy.xyz - -plus-ehl.click - -plus-skd.click - - plus-sv.info - -pogicalein.com - - pointdate.top - - polta.xyz - -pongpong-castella.com - -pontinalitced.com - - pornchita.com - - pornhits.com - - pornl.com - -pornobuceta.net - - pornoweb.win - - porntop.com - - portalclt.com - - -posaid.pro - -posarucurfer.com - - -posedy.pro - -postdigtial.cfd - -postmasters.icu - - -potute.pro - -powerfulhealth.pw - - -ppgopp.com - -practiceasdsa.com - - praelant.com - -prapunmial.com - - pravonexa.com - -preferpren.com - -preformancein.cfd - -premamaingon.com - - -preoin.top - -prescientfoundation.org - -preteenages.sbs - -primeiro-livro.com - -primerewardstop.com - -private-salon.com - -priventess.com - -prize-oceans.online - -prizesearchfive.com - -prizesearchfour.com - -prizesearchone.com - -prizesearchseven.com - -prizesearchsix.com - -prizesearchtwo.com - -prizestash.com - -productdesc.com - -productreviewjobs.com - -profrmances.cfd - - protoions.com - -protoplanetoids.cfd - -protostarsas.cfd - - prtovtem.xyz - -ptaimpeerte.com - - pudongyy.com - -pulmogenomics.org - -purelypairs.xyz - - purlewind.org - -pushdomainm.com - -pushdomainp.com - -pushdomainq.com - -pushdomainr.com - -pushdomainu.com - -pushdomainv.com - -pushnextpropeller.ink - -pushnextsharing.ink - -pushnextsteller.ink - -pushnextunity.ink - -pushpropeller.ink - -pushsharing.ink - -pushsteller.ink - - pushunity.ink - -pussy-airlines.com - - pussyfuck.win - - putit.world - - puzicug.sbs - - -pzdate.com - - qchtrip.com - - qerakin.xyz - -qo4dmb5hio3e.today - - qotawijo.sbs - -quanghuyland.com - -quick-distractions.com - -quickest-encounters.com - -quickest-matches.com - -quickformpayout.com - -quicklusts.com - -quicknready.com - -quickplayer.cfd - -quickytalks.com - - quietdate.com -! -quincyquiltconservator.work - -quinnquantumphysicist.work - -quinquantumcook.work - -quinquantumquilt.work - -quioneroudle.com - -quirkeynet.shop - - qunguoguo.com - - -qzkjsh.com - -r-qqdatesapp.com - -r3jv1soeku96.today - -r6p5yb0lkzjk.today - -rabbitlittle.com - - radhika.xyz - -radiationcrypto.com - -radioxoriyo.com -# -railroadinjuryinformation.com - - ramedos.xyz - -randalradiologist.work - -random-strangers.com - - -rative.sbs - -rawdesires.live - - rawflirt.live - - rawtempt.com - -raymond-jones.com - -reanalyzed.cfd - -receivelove.click - -reckless-lust.com - -reconomising.icu - - rededepla.com - -redgroundstar.shop - -redsevenlinux.com - -reelbeamspace.com - -remyisgettingstrong.com - -rentaldumpstersva.com - -respectourrights.com - -retreatfullofjoy.com - -revideomarafiki.cfd - -revistamuchomas.com - - rew76.com - -rewardbase.shop - -rewardcenter.shop - -rewardcity.shop - -rewardclickspanel.com - -rewardcloud.shop - -rewardgate.shop - -rewardmarket.shop - -rewardstreet.shop - - rewwzinga.com - -rf8iqzuz01no.today - - rgzdjxc.com - -ri25.com - -rickroborepair.work - - -ringin.cfd - - -ringle.cfd - - rininger.cfd - -rldistributors.com - -ro-hubtodayline.org - -roisx9avec0r.today - -romance-meet.com - -romanceconnecter.xyz - -romanceflare.xyz - -romanceroute.click - -romancezoneer.xyz - -romanti2moment.net - -romantiidate.com - -ronretrofixer.work - -roofcorpla.com - - ropedric.com - -rosalindradiographer.work - -roughplunge.com -! -rowenarollerderbycoach.work - - rudecepal.com - - rudemoan.com - -rufreecup.tech - - -ruki.click - -ruleabsent.com - -rulechickens.com - - rulefaded.com - -ruleincandescent.com - - -rutulu.com - -rv7ilky90sc7.today - -s0g45374rtsl.today - -s17juymrmx4s.today - -saangioclub.com - -salon-pros.com - - samarito.sbs - - samavet.xyz - -sample-buddy.com - -samplesfinderpro.com - -samplesflash.com - -sampleshunterusa.com - -samsolarbarber.work - -samyangles.icu - -sandsmodelsshop.com - -sangiorgiosnc.com - -sanmiguell.com - -santiscias.com - - sarahkuck.xyz - - sashalabs.com - -sattelelvision.icu - -sattelephotos.icu - -saucyangel.xyz - - savefrom.net - - saverting.icu - - savesting.icu - - savetown.shop - -savorykitchenart.com - - scanhn.click - -schiditypookyph.com - - schroffus.com - - -scient.sbs - - scoraism.com - -scored-ittt.com - -scrimprotecting.icu - - sdiretail.com - -sdn-defender.pro - -seakearbionsin.com - - sealingjx.com - -search-for.date - -search-now.date - -sebastianshipwright.work - -secmobidapp.cfd - -secrurespend.com - -seekinfofast.com - -seeyourtoday.click - -sekseebounty.com - -selfawarecultures.com - - selitioge.com - - -seno.click - -sensualdrive.live - -sensualkiss1.com - -sententias.org - - sentivin.com - - senzuri.tube - - seoproff.info - -serenasoundengineer.work - -serialbrokeboy.com - -seriouslyidentity.com - -setouchi-hd.com - -sex-friend-finder.com - -sexdatable.icu - -sexoaovivo.org - -sexxfun4you.com - -sexybounty.com - -sexyddates.com - -sexyfinders.com - - seyoh.com - -sfgermanmotors.com - -shadow-meets.com - -shaky-jugs.com - -shanesservices.com - -sharedculture.org - -shootpostwatch.org - - shopju.click - - shopping.net - -shopv1sion.com - -shybeautgirls.com - -sicherheitskultur.org - - sickofsam.com - -siftforanswers.com - -silkpleasure1.com - - sitanity.com - - -sjgt88.com - -skygoatdigital.com - - skysound7.com - -sleepingsmart.org - -slivki1onlineshop.live - - -slovax.com - - -slpose.com - -slut-radar.com - -slutybabes.com - -slutymilfs.com - -slutytalks.com - - smardis.xyz - -smartlifestyletrends.com - -smartresultsnow.net - -smoochyboo.com - -snagyoursamples.com - -snapboostr.site - -snapromance.xyz - -snapsnooze.com - - sneakyow.com - -snuggloria.com - -sociologistsamuel.work - - soguqeci.sbs - -solid-sales.com - -sontayamusic.com - -sophiaspeechtherapist.work - - sophistic.sbs - -sophisticacious.sbs - -soulbonded.xyz - -soulclick-lane.com - -soulmatehub.xyz - -southsidealliance.org - - -sowin.info - -spanchainhub.com - -spark-corner.com - -spark-dash.com - -spark-place.com - -sparkforge.click - - sparkmate.xyz - - speak4sex.cam - -spectrtriee.cc - - spendeum.com - -speraspace.com - -sphiphtlisbels.com - -spintreasure.shop - -spinyourchance.xyz - -spodocylversh.com - -sposynalies.com - -spurtdaddy.com - -squilogibler.com - - srbazan.xyz - -staffservice.cfd - - starbbqoc.com - -starlightsculptor.work - -starlightsingles.monster - - -starry.cfd - -starstarsand.cfd - -stevesautorepairkc.com - -stevesolarsailor.work - -stewartspawn.com - - stified.sbs - -stimprograms.com - - stlyz.com - - stormpop.fun - -strachannels.sbs - -streamshiftplay.com - -streamwatcharena.com - - strious.sbs - -strykersvilletire.com - -studionomatik.com - - submode2.cfd - - submode3.cfd - -submodethe.cfd - -subnetworkin.cfd - -subnetworksthe.cfd - -suchefinde.net - -sudhangurung.com - -sultrydates.com - -sunsephoracle.sbs - -sunset-pair.com - -sunsetinyourpocket.org - -superjobsishere.com - -superspicacious.sbs - -supersweepstotherescue.com - - superter.icu - - supezuj.sbs - - supoortto.cfd - - -supors.cfd - - supppors.cfd - -surgicalent.com - -surveyearningsnow.com - -sv-programs.com - -swahiligrill.com - -sweepscentreusa.com - -sweetkisss.net - -sweetmeetnow.xyz - - swiftbond.xyz - -swiftgear.autos - -swipeharmony.org - - swissweb3.com - - sylaixin.com - -sylaribacty.com -! -sylvestersoundsculptor.work - -syncdate.monster - -syrianarestaurante.com - -systemplayerhub.com - - szdeston.com - -t5i5wr7kckld.today - -tabithataxonomist.work - -taekwondoflorida.com - -talk-circle.com - - talk-line.com - - talk-wave.com - -talkdoor-club.com - -talkingirls.com - -tansytechnician.work - -taptowin.website - -tasmanconcept.com - -technologyup.date - - techup.date - - tel1mekar.com - -telesystem.icu - - tellgents.sbs - - -tengsu.org - - teonite.xyz - - testdr50.com - -tf18drgx6re0.today - - thaddaeus.xyz - - -thdigi.net - -theamericancareerguide.com - -theamericansurvey.com - -thebestcostumer.com - -thebestlover.com - -thebethleheminn.com - -thebfirearmblog.com - -theclassactionguide.com - -thefreedailyraffle.com - -thefreesamplesguide.com - -thefreesampleshelper.com - - -thegay.com - -thehatefulsociety.com - -thehemoinano.com - - theieumch.com - -thelover.online - - themedias.cfd - -themoneyhackers.com - -themoneyminutes.com - -themoneypower-account.sbs - -themoneypower-ai.sbs -" -themoneypower-alliance.store - -themoneypower-alt.best - -themoneypower-app.lol - -themoneypower-axis.best - -themoneypower-balance.xyz - -themoneypower-bank.co - -themoneypower-banking.xyz - -themoneypower-boost.click - -themoneypower-capital.bond - -themoneypower-club.lat - -themoneypower-club.store - -themoneypower-core.cloud - -themoneypower-core.site - -themoneypower-creditos.lat - -themoneypower-crown.cloud - -themoneypower-dx.space - -themoneypower-elite.live - -themoneypower-elite.sbs - -themoneypower-elite.site -" -themoneypower-fastpay.online - -themoneypower-focus.best - -themoneypower-funds.online - -themoneypower-future.live - -themoneypower-fx.art -! -themoneypower-global.online - -themoneypower-go.lol - -themoneypower-go.space - -themoneypower-go.store - -themoneypower-guard.space - -themoneypower-horizon.site - -themoneypower-hq.sbs - -themoneypower-hq.site - -themoneypower-hub.cc - -themoneypower-io.live - -themoneypower-iron.art - -themoneypower-key.best - -themoneypower-key.space - -themoneypower-lab.online - -themoneypower-lab.site -! -themoneypower-legend.online - -themoneypower-limit.xyz - -themoneypower-max.online - -themoneypower-neo.space - -themoneypower-net.online - -themoneypower-next.cc - -themoneypower-next.xyz - -themoneypower-nxt.xyz - -themoneypower-one.sbs - -themoneypower-one.xyz - -themoneypower-orbit.art - -themoneypower-peak.store - -themoneypower-planet.space - -themoneypower-plus.cfd - -themoneypower-premium.xyz - -themoneypower-prime.art - -themoneypower-prime.live - -themoneypower-pro.cc - -themoneypower-pro.co - -themoneypower-pro.live - -themoneypower-profit.co - -themoneypower-rise.site - -themoneypower-royal.cc - -themoneypower-safe.live - -themoneypower-saldo.co - -themoneypower-spot.cc - -themoneypower-strong.cloud - -themoneypower-true.online - -themoneypower-truepath.xyz - -themoneypower-union.cloud - -themoneypower-vip.bond - -themoneypower-vision.xyz - -themoneypower-vr.live -+ -%thenonalcoholicfattyliverstrategy.com - -theodoratoxicologist.work - -theodoretechtester.work -# -thepersonalfinancialguide.com - -theproteinshowcase.com - -thepurpleharpsichord.com - -thequicklusts.com - -thesanfranciscomovers.com - -thestudiosublime.com -& - theunemploymentbenefitsguide.com - -theunivers.sbs - -thewhitenile.com - -thickcrave.com - -thickflare.com - -thisissmoth.com - -thisstream.com - - tholove.org - -thomasfixed.com - -threecredit.shop - -threecredit.site - -threecredit.store - -threeloan.site - -threesixtykids.com - - tml-xerox.com - -tobiastelemetrytech.work - - todoroms.com - - tokinon.icu - -tominchies.com - -top-live-show.com - - top2days.life - -topjobmarket.net - -topmusicfactory.com - -topsurveyspot.com - - torringla.com - -tothedegrees.com - -tradebitcoinaustralia.com - -tradecupertfarm.com - -tradeschoolloans.com - - transmit7.com - -trapicheo-bot.com - -treatline.shop - -treatmall.shop - -treatpark.shop - -treffen2sex.com - -trendingstoriesforyou.com - -trendndailyamerica.com - -trendndailycentral.com - -trendndailyclub.com - -trendndailydeals.com - -trendndailyinsider.com - -trendndailyofficial.com - -trendndailytoday.com - -trendndailyus.com - -trendreport.pw - -trentimetinker.work - -trickorkiss.org - -trinellaneta.cfd - -trittrousenes.com - -tropicalconcrete.com -! -trucchiperigiochimobile.com - -truematch.click - - truematch.xyz - -trueties.click - -trustydevicehub.com - - try2day.life - - try2day.live - -try2days.today - - tryconv.com - - -trymsg.com - - trynoti.com - - tryregme.com - - ttstation.com - -tubepornclassic.com - -tumejornoticia.com - -tumtumtumclicke.online - -turbodomain37.online - -tuskmacrographica.sbs - -tuskmicrochronica.com - -tweadsialing.com - -twocredit.shop - -twocredit.site - -twocredit.store - -twocredits.store - -txxx.com - - ucompany.icu - - -udekeh.sbs - -ulrikaumbrellamaker.work - -ultimatest.cfd - -ultraplinko.com - - ulumexo.sbs - -ulyssesupcyclist.work - -ulyssesutopiamech.work - -umbrellautopia.work - - unabledgy.com - - undating.sbs - -undercutmusic.com - -undicantertic.com - - undiness.com - - undlible.com - - unfornee.com - -uninteligent.sbs - - univers.sbs - -universales.sbs - - universei.sbs - -universethey.sbs - -unlimdates.com - -unlimflirt.com - -unmarkting.cfd - -unsustainabilty.cfd - -unturnnostone.com - -upgradedrive.top - - upornia.com - - upportfor.cfd - - upporting.cfd - - uppport.cfd - - uqfojkbh.com - - uqicevo.sbs - -urbanunderwaterwelder.work - - ureha.sbs - -urenculusetrate.com - - urese.sbs - -ursulaupholsterer.work - - -urvery.cfd - - useconmy.icu - -useconomyand.icu - - usose.com - - uveje.sbs - - uxavi.sbs - - uxofona.sbs - - uytrlab.com - - uzakuvi.sbs - - v400gsm.cfd - -vackatliged.com - - vacopers.com - -valdebarasota.cfd - -valuemailpush.com - -valuepoint.shop - - valueway.shop - - vatinad.xyz - - -vcfare.com - - veciguk.sbs - - velonexa.com - - velvetsin.xyz - -velvetvows.click - - verifynio.com - - verromon.com - - vibe-lane.com - -vibe-portal.com - - vibe-time.com - -vibe-vibes.com - - vibeamour.xyz - -vibepark-space.com - -videocloudhub.com - - videokbs.com - - vip-dates.com - - virtydate.com - -visishized.com - -vitalvisualsmarketing.com - -viva99-linkgacor.info - -vividlovestories.com - -viviennevirologist.work - - vivimodas.com - -vjav.com - -vnz1m8fwp9io.today - - vobvroles.xyz - -voltaiccrypto.com - - voyegover.com - - vuzeindia.in - -vxxx.com - - -w-news.biz - -waisontyresty.com - -waldhotel-ilsenburg.com - -waliscoritingy.com - -walkthreenshare.cfd - -waltonfish.com - -wantyouhot.com - - -wara64.org - -watchcinemavibe.com - -watchdropzone.org - - watchest.info - -watchitclick.com - -watchmeflick.com - -wazambabonus.com - -wazambakasino.com - - wcnhack.com - - webdating.icu - - webrada.com - -webventure.co.in - -wefindanswers.co - - welligent.sbs - - weltchor.com - -westhillslodge.com - - wet-whirl.com - -wetdesire1.com - - wetflash.com - - wetforu.com - -wetpassionhub.com - - -wettxt.com - -wheathtips.com - -whirlwindweaver.work - -whiskerpickles.com - - -whyggg.com - - widudedas.sbs - -wildandffun.com - - winalert.net - -winifredwatercolorist.work - - winprz.online - -wintheprize.xyz - - wirlesss.cfd - -wishaffair.com - -withcatalonia.org - - wmtmbfun.com - -wolfscatchers.com - -wonderwomenpolestudio.com - - woriusly.com - -worldoftatapam.com - -worldonlinegamez.com - -worsherise.com - - worthyrid.com - -wp9v8b47ejez.today - -wrenuorfa.info - -wrestlingforlovers.com - - wuwzh.com - - -wxsjzl.com - -wykcteqzx7me.today - - xajydhg.com - - -xasiat.com - -xavieraxtherapist.work - -xavierxraytech.work - -xavierxylographer.work - - xbd5o.com - -xenonxylophonist.work - -xfinityfraud.com - - xmilf.com - -xn----ztbcbceder.net - - -xnbtsz.com - -xniteproductions.com - - xnxx-porn.win - -xnxx-porno.win - - xohahoroz.sbs - - xrcut.com - - xrfarming.com - - xtxx.online - - -xucha.site - - xxxi.porn - -yamahamusicindonesia.com - -yancyyachtmaster.work - - yasitons.com - -ye5z2ancv4bx.today - - yes1time.life - - yes2dates.com - -yinfangift.com - - yisen99.com - -ynfvg239rlnc.today - - yogazio.com - -yourcybersecurewave.com - -yourfantasys.com - -yourflirtygirl.pro - -yournaughtyneighbor.com - -yoursexdesire.com - -youthcarebeauty.com - -youtubicaact.shop - -youtubicacau.shop - -youtubicaccu.shop - -youtubicacti.shop - -youtubicacyi.shop - -youtubicaoit.shop - -youtubicatoy.shop - -youtubicayii.shop - -yt1k8csmbmnh.today - - ytwy360.com - -yuncheng315.com - -yusufyogacoach.work - -zacharyzoologist.work - -zachzerohero.work - -zasverlopit.ru - - zingaflow.com - - zmsub.com - - zoemaka.xyz - - -zone9a.com - - zreaxs.site - - zyntrak.click - -zyronex.online \ No newline at end of file diff --git a/library/jcef/cache/Crowd Deny/2025.10.6.61/_metadata/verified_contents.json b/library/jcef/cache/Crowd Deny/2025.10.6.61/_metadata/verified_contents.json deleted file mode 100644 index c4b4c05..0000000 --- a/library/jcef/cache/Crowd Deny/2025.10.6.61/_metadata/verified_contents.json +++ /dev/null @@ -1 +0,0 @@ -[{"description":"treehash per file","signed_content":{"payload":"eyJjb250ZW50X2hhc2hlcyI6W3siYmxvY2tfc2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2IiwiZmlsZXMiOlt7InBhdGgiOiJQcmVsb2FkIERhdGEiLCJyb290X2hhc2giOiIySkY0bjlJektLVmJCMExMQ2VRMXd0N09LN1Nlem1oaV9MenBEVjhnWEdnIn0seyJwYXRoIjoibWFuaWZlc3QuanNvbiIsInJvb3RfaGFzaCI6IjZ5RUlpWmVBWnNlM0g3bWNEYjg5N2hfcXFfWTUtOEo2WW1MNXpLdVJZLWcifV0sImZvcm1hdCI6InRyZWVoYXNoIiwiaGFzaF9ibG9ja19zaXplIjo0MDk2fV0sIml0ZW1faWQiOiJnZ2trZWhnYm5manBlZ2dmcGxlZWFrcGlkYmtpYmJtbiIsIml0ZW1fdmVyc2lvbiI6IjIwMjUuMTAuNi42MSIsInByb3RvY29sX3ZlcnNpb24iOjF9","signatures":[{"header":{"kid":"publisher"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"MXBnWArmfHUkDQ_zR8fPtKClY4Z6Meb5w8vBR2s1tT8kDF9VXr45al_o5klckYyEBHEEHtyG5TTUQKi6zMHNvjKF8dhVsgqNDoVQUKeLMqsWcBW3gEw3gQiFLlwloO1_UCPWNmp7nZe1WjMSL1Wia60550kjAafsYveG1rqAJOmD6pl-WvW5JKWeqiUjN0jNG2gPkIzxv143Sk4pfchvZUBKxVW9YR61lW9CiKmlguEbiOnhY2BHj3PYUdQUMefbJCVJKI4TpOe67AjKpynoBTaKZLuW21PAUHMiWPaBR_tOTjDB9pbq3AesUh_U5OnRA8lF_9nwX9cfNg-zRoHR2NMtmhVlUXrIsDWwh-FNA32LnwOzbNH_4FmuWPWXzu6IjNSLGxpda5GUg65rhMWgCW2-YJVjIbO0p-Y27eU7JDVeIOmodZ358uTDCFz-0ChMUoffA1ej8flGyX_HXqUdYZRzuaJosqwUP9mPXErIgUJbgHNlgO1dRvNyGGTsCe0Xn7UPVfCeuPYoX3yZekKeqlOGcTcXu5mqsg8icr6YP-H_vZR0tBjPmcnmM-Dp08Ix0AY1Pm7wK-7F8sExn7sWTNf3XMQzF1QHIfYLHjnHTmg78fF2kjQxhHd8zqZ121J2o2dhslZdIsH3F44EuEIFQfYHFbKhr93ODq93p2L0cEM"},{"header":{"kid":"webstore"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"i9XSMHRDp-syAOvlFsrJ4swarl0gHdQH5xdhimD2bsj0dxnyNXUZszt97BJfZKmHqPvzJENKAnakE0Q6b_AB2W50xjGfuRZbMJAMdjp8nB_rftgth_KwkO9mbhaBXC_jzP4QYUUFOrwqTTTmNU33wTF9XcjAZ8W8nd7D_-tocxjrT56FpvbKx34Prha1u8VtwGt1raCCI73B_mIXHy9O1PQN6l8umNo12KcTcGt1R2htB-BxWpHLVLHkir4o4ABzjpXfbK3ecEQNTFNZYJs3dvDvmqkEBWCOL17GWn37_GFV78abmUdM3lAcQp1lzavg5VZSBcIcmsoCO4-XUL63Jw"}]}}] \ No newline at end of file diff --git a/library/jcef/cache/Crowd Deny/2025.10.6.61/manifest.fingerprint b/library/jcef/cache/Crowd Deny/2025.10.6.61/manifest.fingerprint deleted file mode 100644 index 141583e..0000000 --- a/library/jcef/cache/Crowd Deny/2025.10.6.61/manifest.fingerprint +++ /dev/null @@ -1 +0,0 @@ -1.0bd17169e41bf80771e71e625ed9469c4006d08a33caa457e184caa55174f67b \ No newline at end of file diff --git a/library/jcef/cache/Crowd Deny/2025.10.6.61/manifest.json b/library/jcef/cache/Crowd Deny/2025.10.6.61/manifest.json deleted file mode 100644 index a152411..0000000 --- a/library/jcef/cache/Crowd Deny/2025.10.6.61/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "manifest_version": 2, - "name": "Crowd Deny", - "preload_data_format": 1, - "version": "2025.10.6.61" -} \ No newline at end of file diff --git a/library/jcef/cache/Default/BrowsingTopicsState b/library/jcef/cache/Default/BrowsingTopicsState index f6f2739..fa5aabd 100644 --- a/library/jcef/cache/Default/BrowsingTopicsState +++ b/library/jcef/cache/Default/BrowsingTopicsState @@ -1,6 +1,20 @@ { "epochs": [ { - "calculation_time": "13407745316672915", + "calculation_time": "13409970184368503", + "config_version": 0, + "model_version": "0", + "padded_top_topics_start_index": 0, + "taxonomy_version": 0, + "top_topics_and_observing_domains": [ ] + }, { + "calculation_time": "13410596642026995", + "config_version": 0, + "model_version": "0", + "padded_top_topics_start_index": 0, + "taxonomy_version": 0, + "top_topics_and_observing_domains": [ ] + }, { + "calculation_time": "13411376394291911", "config_version": 0, "model_version": "0", "padded_top_topics_start_index": 0, @@ -8,5 +22,5 @@ "top_topics_and_observing_domains": [ ] } ], "hex_encoded_hmac_key": "4E642EB949A7AB49761C11DC02ABA0FB1E46EECC80D17CFFE210EF757D5AF3DF", - "next_scheduled_calculation_time": "13408350116672979" + "next_scheduled_calculation_time": "13411981194292267" } diff --git a/library/jcef/cache/Default/Cache/Cache_Data/data_0 b/library/jcef/cache/Default/Cache/Cache_Data/data_0 index 34de4ba..1f65833 100644 Binary files a/library/jcef/cache/Default/Cache/Cache_Data/data_0 and b/library/jcef/cache/Default/Cache/Cache_Data/data_0 differ diff --git a/library/jcef/cache/Default/Cache/Cache_Data/data_1 b/library/jcef/cache/Default/Cache/Cache_Data/data_1 index 916f771..44c74d5 100644 Binary files a/library/jcef/cache/Default/Cache/Cache_Data/data_1 and b/library/jcef/cache/Default/Cache/Cache_Data/data_1 differ diff --git a/library/jcef/cache/Default/Cache/Cache_Data/data_2 b/library/jcef/cache/Default/Cache/Cache_Data/data_2 index a384a84..39a9ec5 100644 Binary files a/library/jcef/cache/Default/Cache/Cache_Data/data_2 and b/library/jcef/cache/Default/Cache/Cache_Data/data_2 differ diff --git a/library/jcef/cache/Default/Cache/Cache_Data/data_3 b/library/jcef/cache/Default/Cache/Cache_Data/data_3 index acea218..17b541b 100644 Binary files a/library/jcef/cache/Default/Cache/Cache_Data/data_3 and b/library/jcef/cache/Default/Cache/Cache_Data/data_3 differ diff --git a/library/jcef/cache/Default/DIPS b/library/jcef/cache/Default/DIPS index 5306125..04a9131 100644 Binary files a/library/jcef/cache/Default/DIPS and b/library/jcef/cache/Default/DIPS differ diff --git a/library/jcef/cache/Default/DawnGraphiteCache/data_1 b/library/jcef/cache/Default/DawnGraphiteCache/data_1 index 41880fa..3965a86 100644 Binary files a/library/jcef/cache/Default/DawnGraphiteCache/data_1 and b/library/jcef/cache/Default/DawnGraphiteCache/data_1 differ diff --git a/library/jcef/cache/Default/DawnWebGPUCache/data_1 b/library/jcef/cache/Default/DawnWebGPUCache/data_1 index d49ed56..3bfbdbb 100644 Binary files a/library/jcef/cache/Default/DawnWebGPUCache/data_1 and b/library/jcef/cache/Default/DawnWebGPUCache/data_1 differ diff --git a/library/jcef/cache/Default/Extension State/LOG b/library/jcef/cache/Default/Extension State/LOG index cb1567c..1bc14f0 100644 --- a/library/jcef/cache/Default/Extension State/LOG +++ b/library/jcef/cache/Default/Extension State/LOG @@ -1,3 +1,3 @@ -2025/11/16-15:36:13.162 1b78 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Extension State/MANIFEST-000001 -2025/11/16-15:36:13.163 1b78 Recovering log #3 -2025/11/16-15:36:13.163 1b78 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Extension State/000003.log +2025/12/28-14:50:54.309 d58 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Extension State/MANIFEST-000001 +2025/12/28-14:50:54.309 d58 Recovering log #3 +2025/12/28-14:50:54.312 d58 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Extension State/000003.log diff --git a/library/jcef/cache/Default/Extension State/LOG.old b/library/jcef/cache/Default/Extension State/LOG.old index ed6baed..aa8541e 100644 --- a/library/jcef/cache/Default/Extension State/LOG.old +++ b/library/jcef/cache/Default/Extension State/LOG.old @@ -1,3 +1,3 @@ -2025/11/16-14:14:54.664 ec0 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Extension State/MANIFEST-000001 -2025/11/16-14:14:54.667 ec0 Recovering log #3 -2025/11/16-14:14:54.668 ec0 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Extension State/000003.log +2025/12/28-14:19:49.868 bcc Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Extension State/MANIFEST-000001 +2025/12/28-14:19:49.869 bcc Recovering log #3 +2025/12/28-14:19:49.881 bcc Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Extension State/000003.log diff --git a/library/jcef/cache/Default/Favicons b/library/jcef/cache/Default/Favicons index 4234df4..2365216 100644 Binary files a/library/jcef/cache/Default/Favicons and b/library/jcef/cache/Default/Favicons differ diff --git a/library/jcef/cache/Default/Favicons-journal b/library/jcef/cache/Default/Favicons-journal index f28a055..e69de29 100644 Binary files a/library/jcef/cache/Default/Favicons-journal and b/library/jcef/cache/Default/Favicons-journal differ diff --git a/library/jcef/cache/Default/GCM Store/Encryption/LOG b/library/jcef/cache/Default/GCM Store/Encryption/LOG index 88d8bba..4c3f223 100644 --- a/library/jcef/cache/Default/GCM Store/Encryption/LOG +++ b/library/jcef/cache/Default/GCM Store/Encryption/LOG @@ -1,3 +1,3 @@ -2025/11/16-14:12:44.573 94 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\GCM Store\Encryption/MANIFEST-000001 -2025/11/16-14:12:44.574 94 Recovering log #3 -2025/11/16-14:12:44.574 94 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\GCM Store\Encryption/000003.log +2025/12/28-14:50:57.787 24fc Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\GCM Store\Encryption/MANIFEST-000001 +2025/12/28-14:50:57.787 24fc Recovering log #3 +2025/12/28-14:50:57.788 24fc Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\GCM Store\Encryption/000003.log diff --git a/library/jcef/cache/Default/GCM Store/Encryption/LOG.old b/library/jcef/cache/Default/GCM Store/Encryption/LOG.old index 365b7c3..2c8e05e 100644 --- a/library/jcef/cache/Default/GCM Store/Encryption/LOG.old +++ b/library/jcef/cache/Default/GCM Store/Encryption/LOG.old @@ -1,3 +1,3 @@ -2025/11/16-14:11:32.786 6108 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\GCM Store\Encryption/MANIFEST-000001 -2025/11/16-14:11:32.787 6108 Recovering log #3 -2025/11/16-14:11:32.787 6108 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\GCM Store\Encryption/000003.log +2025/12/28-14:19:54.454 4760 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\GCM Store\Encryption/MANIFEST-000001 +2025/12/28-14:19:54.457 4760 Recovering log #3 +2025/12/28-14:19:54.458 4760 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\GCM Store\Encryption/000003.log diff --git a/library/jcef/cache/Default/GPUCache/data_1 b/library/jcef/cache/Default/GPUCache/data_1 index 2909d22..b013672 100644 Binary files a/library/jcef/cache/Default/GPUCache/data_1 and b/library/jcef/cache/Default/GPUCache/data_1 differ diff --git a/library/jcef/cache/Default/History b/library/jcef/cache/Default/History index 10e2dfd..a6308f5 100644 Binary files a/library/jcef/cache/Default/History and b/library/jcef/cache/Default/History differ diff --git a/library/jcef/cache/Default/Local Storage/leveldb/000044.log b/library/jcef/cache/Default/Local Storage/leveldb/000044.log index 6e58d53..304bdf0 100644 Binary files a/library/jcef/cache/Default/Local Storage/leveldb/000044.log and b/library/jcef/cache/Default/Local Storage/leveldb/000044.log differ diff --git a/library/jcef/cache/Default/Local Storage/leveldb/LOG b/library/jcef/cache/Default/Local Storage/leveldb/LOG index 32c82e0..cbf95d4 100644 --- a/library/jcef/cache/Default/Local Storage/leveldb/LOG +++ b/library/jcef/cache/Default/Local Storage/leveldb/LOG @@ -1,3 +1,3 @@ -2025/11/16-15:36:13.418 14a4 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Local Storage\leveldb/MANIFEST-000001 -2025/11/16-15:36:13.433 14a4 Recovering log #44 -2025/11/16-15:36:13.440 14a4 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Local Storage\leveldb/000044.log +2025/12/28-14:50:54.386 2b54 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Local Storage\leveldb/MANIFEST-000001 +2025/12/28-14:50:54.399 2b54 Recovering log #44 +2025/12/28-14:50:54.404 2b54 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Local Storage\leveldb/000044.log diff --git a/library/jcef/cache/Default/Local Storage/leveldb/LOG.old b/library/jcef/cache/Default/Local Storage/leveldb/LOG.old index a57996a..49f29dd 100644 --- a/library/jcef/cache/Default/Local Storage/leveldb/LOG.old +++ b/library/jcef/cache/Default/Local Storage/leveldb/LOG.old @@ -1,3 +1,3 @@ -2025/11/16-14:14:54.674 3450 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Local Storage\leveldb/MANIFEST-000001 -2025/11/16-14:14:54.685 3450 Recovering log #44 -2025/11/16-14:14:54.689 3450 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Local Storage\leveldb/000044.log +2025/12/28-14:19:49.965 2d90 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Local Storage\leveldb/MANIFEST-000001 +2025/12/28-14:19:49.981 2d90 Recovering log #44 +2025/12/28-14:19:49.986 2d90 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Local Storage\leveldb/000044.log diff --git a/library/jcef/cache/Default/Network/Cookies b/library/jcef/cache/Default/Network/Cookies index 6bb1401..d254051 100644 Binary files a/library/jcef/cache/Default/Network/Cookies and b/library/jcef/cache/Default/Network/Cookies differ diff --git a/library/jcef/cache/Default/Network/Network Persistent State b/library/jcef/cache/Default/Network/Network Persistent State index 4bb95cf..e616eb3 100644 --- a/library/jcef/cache/Default/Network/Network Persistent State +++ b/library/jcef/cache/Default/Network/Network Persistent State @@ -1 +1 @@ -{"net":{"http_server_properties":{"broken_alternative_services":[{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",false],"broken_count":1,"host":"www.googletagmanager.com","port":443,"protocol_str":"quic"},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",false],"broken_count":2,"host":"www.google-analytics.com","port":443,"protocol_str":"quic"},{"anonymization":["GAAAABIAAABodHRwczovL2xpbmdxaS52aXAAAA==",false],"broken_count":5,"host":"content-autofill.googleapis.com","port":443,"protocol_str":"quic"},{"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false],"broken_count":3,"host":"fonts.gstatic.com","port":443,"protocol_str":"quic"},{"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false],"broken_count":4,"host":"fonts.googleapis.com","port":443,"protocol_str":"quic"},{"anonymization":["JAAAAB0AAABodHRwczovL3VwZGF0ZS5nb29nbGVhcGlzLmNvbQAAAA==",false],"broken_count":18,"broken_until":"1763444643","host":"update.googleapis.com","port":443,"protocol_str":"quic"},{"anonymization":["GAAAABIAAABodHRwczovL2dvb2dsZS5jb20AAA==",false],"broken_count":4,"host":"accounts.google.com","port":443,"protocol_str":"quic"}],"servers":[{"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false],"server":"https://a.nel.cloudflare.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",true],"server":"https://cdn.casbin.org","supports_spdy":true},{"anonymization":["FAAAABAAAABodHRwczovL2JpbmcuY29t",false],"server":"https://ts3.tc.mm.bing.net","supports_spdy":true},{"anonymization":["FAAAABAAAABodHRwczovL2JpbmcuY29t",false],"server":"https://ts4.tc.mm.bing.net","supports_spdy":true},{"anonymization":["FAAAABAAAABodHRwczovL2JpbmcuY29t",true],"server":"https://login.microsoftonline.com","supports_spdy":true},{"anonymization":["FAAAABAAAABodHRwczovL2JpbmcuY29t",false],"server":"https://ts2.tc.mm.bing.net","supports_spdy":true},{"anonymization":["FAAAABAAAABodHRwczovL2JpbmcuY29t",false],"server":"https://ts1.tc.mm.bing.net","supports_spdy":true},{"anonymization":["FAAAABAAAABodHRwczovL2JpbmcuY29t",false],"server":"https://www.clarity.ms","supports_spdy":true},{"anonymization":["FAAAABAAAABodHRwczovL2JpbmcuY29t",false],"server":"https://scripts.clarity.ms","supports_spdy":true},{"anonymization":["FAAAABAAAABodHRwczovL2JpbmcuY29t",false],"server":"https://c.bing.com","supports_spdy":true},{"anonymization":["FAAAABAAAABodHRwczovL2JpbmcuY29t",false],"server":"https://c.clarity.ms","supports_spdy":true},{"anonymization":["FAAAABAAAABodHRwczovL2JpbmcuY29t",false],"server":"https://cn.bing.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2RlZXBzZWVrLmNvbQ==",false],"server":"https://static.deepseek.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2RlZXBzZWVrLmNvbQ==",false],"server":"https://tab.volces.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2RlZXBzZWVrLmNvbQ==",true],"server":"https://open.weixin.qq.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://activity.hdslb.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://backup.hdslb.com","supports_spdy":true},{"anonymization":["GAAAABEAAABodHRwczovL2hkc2xiLmNvbQAAAA==",false],"server":"https://dl.hdslb.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://i1.hdslb.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://i2.hdslb.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://le3-api.game.bilibili.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://unet.quark.cn","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://cid-click.smallfighter.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://api.live.bilibili.com","supports_spdy":true},{"anonymization":["FAAAAA8AAABodHRwczovL2pzLmNvb2wA",false],"server":"https://static.cloudflareinsights.com","supports_spdy":true},{"anonymization":["FAAAAA8AAABodHRwczovL2pzLmNvb2wA",false],"server":"https://cloudflareinsights.com","supports_spdy":true},{"anonymization":["FAAAAA8AAABodHRwczovL2pzLmNvb2wA",false],"server":"https://play.mc.js.cool","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://www.bilibili.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",true],"server":"https://s1.hdslb.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://s1.hdslb.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://i0.hdslb.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://api.bilibili.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://cm.bilibili.com","supports_spdy":true},{"anonymization":["GAAAABQAAABodHRwczovL2JpbGliaWxpLmNvbQ==",false],"server":"https://data.bilibili.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",true],"server":"https://vc.hotjar.io","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",false],"server":"https://vc.hotjar.io","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",false],"server":"https://cdn.casbin.org","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",false],"server":"https://www.googletagmanager.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",true],"server":"https://static.hotjar.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",false],"server":"https://static.hotjar.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",true],"server":"https://api.github.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",false],"server":"https://oa.casbin.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",false],"server":"https://script.hotjar.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",true],"server":"https://script.hotjar.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",true],"server":"https://content.hotjar.io","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",false],"server":"https://surveystats.hotjar.io","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",true],"server":"https://surveystats.hotjar.io","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",true],"server":"https://metrics.hotjar.io","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",false],"server":"https://www.google-analytics.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",true],"server":"https://door.casdoor.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",true],"server":"https://ghbtns.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",false],"server":"https://embed.tawk.to","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",true],"server":"https://a.nel.cloudflare.com","supports_spdy":true},{"anonymization":["GAAAABMAAABodHRwczovL2Nhc2Rvb3Iub3JnAA==",false],"server":"https://casdoor.org","supports_spdy":true},{"anonymization":["MAAAACwAAABodHRwczovL3Bhc3N3b3Jkc2xlYWtjaGVjay1wYS5nb29nbGVhcGlzLmNvbQ==",false],"server":"https://passwordsleakcheck-pa.googleapis.com","supports_spdy":true},{"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false],"server":"https://cdn.jsdelivr.net","supports_spdy":true},{"anonymization":["GAAAABIAAABodHRwczovL2xpbmdxaS52aXAAAA==",false],"server":"https://cdn.casbin.org","supports_spdy":true},{"anonymization":["GAAAABIAAABodHRwczovL2xpbmdxaS52aXAAAA==",false],"server":"https://content-autofill.googleapis.com","supports_spdy":true},{"anonymization":["GAAAABIAAABodHRwczovL2xpbmdxaS52aXAAAA==",false],"server":"https://casdoor.lingqi.vip","supports_spdy":true},{"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false],"server":"https://fonts.googleapis.com","supports_spdy":true},{"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false],"server":"https://fonts.gstatic.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13410337440195665","port":443,"protocol_str":"quic"}],"anonymization":["JAAAAB4AAABodHRwczovL3N0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AAA==",false],"server":"https://storage.googleapis.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13410339164949253","port":443,"protocol_str":"quic"}],"anonymization":["JAAAAB0AAABodHRwczovL3VwZGF0ZS5nb29nbGVhcGlzLmNvbQAAAA==",false],"server":"https://update.googleapis.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13407833541102793","port":443,"protocol_str":"quic"}],"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false],"network_stats":{"srtt":187959},"server":"https://unpkg.com","supports_spdy":true},{"anonymization":["GAAAABIAAABodHRwczovL2dvb2dsZS5jb20AAA==",false],"server":"https://accounts.google.com","supports_spdy":true}],"supports_quic":{"address":"192.168.2.29","used_quic":true},"version":5},"network_qualities":{"CAESABiAgICA+P////8B":"4G","CAYSABiAgICA+P////8B":"Offline"}}} \ No newline at end of file +{"net":{"http_server_properties":{"broken_alternative_services":[{"anonymization":["JAAAAB0AAABodHRwczovL3VwZGF0ZS5nb29nbGVhcGlzLmNvbQAAAA==",false],"broken_count":10,"broken_until":"1767056398","host":"update.googleapis.com","port":443,"protocol_str":"quic"},{"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false],"broken_count":1,"broken_until":"1766904958","host":"fonts.googleapis.com","port":443,"protocol_str":"quic"}],"servers":[{"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false],"server":"https://code.jquery.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13413970258167864","port":443,"protocol_str":"quic"}],"anonymization":["JAAAAB0AAABodHRwczovL3VwZGF0ZS5nb29nbGVhcGlzLmNvbQAAAA==",false],"server":"https://update.googleapis.com","supports_spdy":true},{"alternative_service":[{"advertised_alpns":["h3"],"expiration":"13413970255009851","port":443,"protocol_str":"quic"}],"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false],"server":"https://fonts.googleapis.com","supports_spdy":true},{"anonymization":["DAAAAAcAAABmaWxlOi8vAA==",false],"server":"https://cdn.jsdelivr.net","supports_spdy":true}],"version":5},"network_qualities":{"CAESABiAgICA+P////8B":"4G"}}} \ No newline at end of file diff --git a/library/jcef/cache/Default/Network/Reporting and NEL b/library/jcef/cache/Default/Network/Reporting and NEL index 18f2bb5..da40c06 100644 Binary files a/library/jcef/cache/Default/Network/Reporting and NEL and b/library/jcef/cache/Default/Network/Reporting and NEL differ diff --git a/library/jcef/cache/Default/Network/TransportSecurity b/library/jcef/cache/Default/Network/TransportSecurity index 6a991a9..0f48527 100644 --- a/library/jcef/cache/Default/Network/TransportSecurity +++ b/library/jcef/cache/Default/Network/TransportSecurity @@ -1 +1 @@ -{"sts":[{"expiry":1786970571.324373,"host":"CKgWF8Pt6+y0hclDrtEluIjL2FYgseRxvC7MGqrhWRI=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1755434571.324375},{"expiry":1786970748.345876,"host":"Fkw+OD9Mt6EPvdYMfEpOeOh5YtJfdmo+tIpYmrw/aHw=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1755434748.345878},{"expiry":1786952672.907422,"host":"F8CDsiT0h6lTN4Nqwoyb2wNyqqjWSTsRj/gzlYU3NfY=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755416672.907424},{"expiry":1787020707.436438,"host":"Ifty0kmGtBkqpzyRNEJ2iccvLpOffAEtEf72UjNCr0U=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755484707.43644},{"expiry":1787020704.592068,"host":"M4bfUnCmQAi4PNb3B8aI/2+SVJhHKsMfMMT7fzi6ij4=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755484704.592071},{"expiry":1771158958.946775,"host":"ODQpkumdnBklhBhFYjplmPIGUqnaPXWWo+CnFJ0/t6Q=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755434158.946777},{"expiry":1786952673.49577,"host":"PKqosHGXLFTwexcsjC+UXTkKV3GWWHwtzKz/ULb9ssM=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1755416673.495773},{"expiry":1787020905.689562,"host":"RzJ4U8oc5hFtdWWgqHrmedzdy4/1lFbezoh2vuM2sag=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755484905.689566},{"expiry":1786970158.787767,"host":"TYKBX4bj7STJmdsAJf/AXVV5wkijZuQeyADuWpRjtV8=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1755434158.787769},{"expiry":1770986558.135948,"host":"ZYjgsmqK1UttB4LVvFgUJHAviltZMIazfUdho0vgoHQ=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755434558.13595},{"expiry":1786970158.716881,"host":"cZAeyWA3fHA2TUtpNDfMvYMZuYmsyq5vod7XbsSSYrQ=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1755434158.716883},{"expiry":1794814574.257027,"host":"e3SziuwfuO2UvuBno+qkR1ObHAzZmSUoJhrc7dbP1Uo=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1763278574.257038},{"expiry":1786952041.049301,"host":"glefLLBZdcPMe88hNYSyH7izPjRoDmxZt9QhbJ5f3Yw=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755416041.049304},{"expiry":1791358496.65612,"host":"nAuqgR4iEWti7SOdT3UHPl6rmZU/DeaIm38P2O2OkgA=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1759822496.656122},{"expiry":1771158958.82246,"host":"n/JnTog5u4dqIfqFFLydo6nGO69ZdnM1JYifZdoi7ss=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755434158.822462},{"expiry":1789382403.778989,"host":"qaDeFdT1UTirY0OQe+c5LKw+zjx6vF/+3vFh7CgrAOY=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1757846403.778991},{"expiry":1786952671.323698,"host":"ybtrRgz/Tr6DpPFcMRWkxm21VXSNujk8GGrWr9ew7/g=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755416671.3237},{"expiry":1786970158.131997,"host":"5saCdK9MYjX/Itn8+aAtA/tRPNpaGBN3I3fjBzZOecQ=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755434158.132},{"expiry":1789952777.339869,"host":"8/RrMmQlCD2Gsp14wUCE1P8r7B2C5+yE0+g79IPyRsc=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1758416777.339871}],"version":2} \ No newline at end of file +{"sts":[{"expiry":1786970571.324373,"host":"CKgWF8Pt6+y0hclDrtEluIjL2FYgseRxvC7MGqrhWRI=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1755434571.324375},{"expiry":1781472734.002118,"host":"E10e7Gwg5+phsYD4E8qNYFsQySXnIHPAfo4zloUPESc=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1765692734.002119},{"expiry":1786970748.345876,"host":"Fkw+OD9Mt6EPvdYMfEpOeOh5YtJfdmo+tIpYmrw/aHw=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1755434748.345878},{"expiry":1786952672.907422,"host":"F8CDsiT0h6lTN4Nqwoyb2wNyqqjWSTsRj/gzlYU3NfY=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755416672.907424},{"expiry":1787020707.436438,"host":"Ifty0kmGtBkqpzyRNEJ2iccvLpOffAEtEf72UjNCr0U=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755484707.43644},{"expiry":1787020704.592068,"host":"M4bfUnCmQAi4PNb3B8aI/2+SVJhHKsMfMMT7fzi6ij4=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755484704.592071},{"expiry":1771158958.946775,"host":"ODQpkumdnBklhBhFYjplmPIGUqnaPXWWo+CnFJ0/t6Q=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755434158.946777},{"expiry":1786952673.49577,"host":"PKqosHGXLFTwexcsjC+UXTkKV3GWWHwtzKz/ULb9ssM=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1755416673.495773},{"expiry":1787020905.689562,"host":"RzJ4U8oc5hFtdWWgqHrmedzdy4/1lFbezoh2vuM2sag=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755484905.689566},{"expiry":1786970158.787767,"host":"TYKBX4bj7STJmdsAJf/AXVV5wkijZuQeyADuWpRjtV8=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1755434158.787769},{"expiry":1770986558.135948,"host":"ZYjgsmqK1UttB4LVvFgUJHAviltZMIazfUdho0vgoHQ=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755434558.13595},{"expiry":1786970158.716881,"host":"cZAeyWA3fHA2TUtpNDfMvYMZuYmsyq5vod7XbsSSYrQ=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1755434158.716883},{"expiry":1795327998.306247,"host":"e3SziuwfuO2UvuBno+qkR1ObHAzZmSUoJhrc7dbP1Uo=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1763791998.306249},{"expiry":1786952041.049301,"host":"glefLLBZdcPMe88hNYSyH7izPjRoDmxZt9QhbJ5f3Yw=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755416041.049304},{"expiry":1798440655.010661,"host":"nAuqgR4iEWti7SOdT3UHPl6rmZU/DeaIm38P2O2OkgA=","mode":"force-https","sts_include_subdomains":false,"sts_observed":1766904655.010663},{"expiry":1771158958.82246,"host":"n/JnTog5u4dqIfqFFLydo6nGO69ZdnM1JYifZdoi7ss=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755434158.822462},{"expiry":1797227932.989249,"host":"qaDeFdT1UTirY0OQe+c5LKw+zjx6vF/+3vFh7CgrAOY=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1765691932.989252},{"expiry":1786952671.323698,"host":"ybtrRgz/Tr6DpPFcMRWkxm21VXSNujk8GGrWr9ew7/g=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755416671.3237},{"expiry":1786970158.131997,"host":"5saCdK9MYjX/Itn8+aAtA/tRPNpaGBN3I3fjBzZOecQ=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1755434158.132},{"expiry":1789952777.339869,"host":"8/RrMmQlCD2Gsp14wUCE1P8r7B2C5+yE0+g79IPyRsc=","mode":"force-https","sts_include_subdomains":true,"sts_observed":1758416777.339871}],"version":2} \ No newline at end of file diff --git a/library/jcef/cache/Default/Preferences b/library/jcef/cache/Default/Preferences index 46499ac..50109ff 100644 --- a/library/jcef/cache/Default/Preferences +++ b/library/jcef/cache/Default/Preferences @@ -1 +1 @@ -{"accessibility":{"captions":{"live_caption_language":"en-US"}},"account_tracker_service_last_update":"13407745281899726","alternate_error_pages":{"backup":true},"announcement_notification_service_first_run_time":"13390881861683238","apps":{"shortcuts_arch":"","shortcuts_version":0},"autocomplete":{"retention_policy_last_version":132},"autofill":{"last_version_deduped":132},"browser":{"app_window_placement":{"DevToolsApp":{"always_on_top":false,"bottom":869,"left":1054,"maximized":false,"right":2134,"top":69,"work_area_bottom":1032,"work_area_left":0,"work_area_right":1920,"work_area_top":0}},"has_seen_welcome_page":false},"commerce_daily_metrics_last_update_time":"13407745281901289","countryid_at_install":17230,"default_apps_install_state":3,"default_search_provider":{"guid":""},"devtools":{"adb_key":"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/fH93dsJxjHkARPbm4ENKQz3YyDD/k9eLSjQRE0mzYLV7BHB3CL8A8gYk8/ohTyTnHQd66224XmN/UogWbGMtejOxyHW/zHgDKeAyW99TljlFwRgeUuagOC3toekozJvP2Lz6c3+prJJjmApbBfpaQ1CQGImOp1yKIsHEeEyy+34GhtZl3763IvQuGdhbXf9nqS1h1pSv13Or5JIGHjRDOY4kZl1ags2RPb92vnpN2imdus+Z9jCKc7ileme2yXpiKZBDzJOh1vRIamb7RWUoOkVz1idsfsScWFDTzv6b7wtbKFBmMvN0SfEg+WQwQP8z+P8QsfdjkIz2K9tRAVVXAgMBAAECggEAFhMBEsGDCtn5vPk2SUlDGJWF1K7HTz840trcA7fogCA+646IdIBynGZKJQpnJF3eUDI9gYMQY/elmrDl9UfLaxuyR+VnBZRJpXu3V7bVmOoxcgQfhqdh5Mh1KvbGTSjBMYW3y1tzNRkqywZk8zdONAeciu/5M5PBjD8TfVWs6dj2qqAOrJQrsH26k5pkG3lykiABavfJrfRrvN/nbdMxlHKv8BYhHWScejMVUR+8wsOqazQjsPW/TtmqUSCwPF8bj7vT19LVqOuE8E1BnaClnCfzqgDISb/kvGPeyvYGZJBA8Bv8PKAat8eZNVvDSGiWGwlqMKC9uMuflafgiEtoMQKBgQD7O+FstyvlT+Q9kNO7GJxptGvgNLpDUgP/8T69UVQuDmGq5pHm1V2sBiIU/gbzqJksnlUY/5+TTIAUrbaNqyoti4lKAzF44JhhE/oECRcxijemfNgT3zO+1rv1vYBvKh5RrjWmdbtKl5VhEByTj4Jj9ksGskYgvupmy1KiRcYFjwKBgQDDHnPlnJRTxd8FLptpikSvBtBX4n09jXb09LRkHbWcoiWSkcKqprNIR7nbU5PIBrniGPYiUcapjksoza4NLPQMZRCVSJ0WW6977piCSx6J7bjkxOVDRxPXcRTYGBM3Skigc/TJbnk9li4kWO/Ey9m7ElK+l/5yQhD5ohuHcpUfuQKBgQC47yFHF6a2TBLkxuE0zi8FGIx4JBggShUrp/fZAC8JIAkA9mzcEJ+9a15XOY8AeVEXqQ1XM4PRt/RoRF8m9aI8mIRc+PDH2/wPKddWdSKfkvDYGvor1peOmR8PC4mpSsW6tpRDjlJp3B9XrbZ7YJ5I2xnrOfupSx/cvzp6vQXBPQKBgAwhFn9NjoLuqACAdzTGXaKrv8PW1oY/BwgPNi+LEUEda0yDNyC4oCh8HPefaG5kzKVbe+GCR+E+cTmmH42+H6WtmqxNCUbciDoiCOUAmnNkjbva6Y2XG4qnAu60AG3NdlpTGwJylaLYdSHZTX5kVbKlXvNuK2ka5kc+Zouy+cYxAoGAaua2QFf45bDw7IaNJRepgHf9VIWjMgkwmf+iRcnKht32tWskhxKiefAaf0ArK2qpeYI5r6jF/ThiDixZ+EGlU9WcfC3cm3LcweY5RRRthnlKIJAsxHgxP4I7O4iBG+hcVaWVnl1S2oHndGldOOJnHtbafRVoZBvRfWGjuusT6lc=","last_open_timestamp":"13399989213872","preferences":{"closeable-tabs":"{\"security\":true,\"freestyler\":true,\"chrome-recorder\":true}","console.sidebar-selected-filter":"\"message\"","console.sidebar.width":"{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","disable-locale-info-bar":"true","elements.styles.sidebar.width":"{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","inspector.drawer-split-view-state":"{\"horizontal\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","inspectorVersion":"38","network-panel-sidebar-state":"{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","network-panel-split-view-state":"{\"vertical\":{\"size\":0}}","network-panel-split-view-waterfall":"{\"vertical\":{\"size\":0}}","panel-selected-tab":"\"sources\"","request-info-form-data-category-expanded":"true","request-info-general-category-expanded":"true","request-info-query-string-category-expanded":"true","request-info-request-headers-category-expanded":"true","request-info-request-payload-category-expanded":"true","request-info-response-headers-category-expanded":"true","selected-profile-type":"\"HEAP\"","sources-panel-navigator-split-view-state":"{\"vertical\":{\"size\":0,\"showMode\":\"Both\"}}","sources-panel-split-view-state":"{\"vertical\":{\"size\":0,\"showMode\":\"Both\"}}","styles-pane-sidebar-tab-order":"{\"styles\":10,\"computed\":20}","timeline-counters-split-view-state":"{\"horizontal\":{\"size\":0}}","timeline-panel-sidebar-state":"{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","timeline-tree-view-details-split-widget":"{\"vertical\":{\"size\":0}}"},"synced_preferences_sync_disabled":{"adorner-settings":"[{\"adorner\":\"grid\",\"isEnabled\":true},{\"adorner\":\"subgrid\",\"isEnabled\":true},{\"adorner\":\"flex\",\"isEnabled\":true},{\"adorner\":\"ad\",\"isEnabled\":true},{\"adorner\":\"scroll-snap\",\"isEnabled\":true},{\"adorner\":\"container\",\"isEnabled\":true},{\"adorner\":\"slot\",\"isEnabled\":true},{\"adorner\":\"top-layer\",\"isEnabled\":true},{\"adorner\":\"reveal\",\"isEnabled\":true},{\"adorner\":\"media\",\"isEnabled\":false},{\"adorner\":\"scroll\",\"isEnabled\":true}]","disable-self-xss-warning":"true","language":"\"zh\"","syncedInspectorVersion":"38"}},"domain_diversity":{"last_reporting_timestamp":"13407745281900968"},"enterprise_profile_guid":"07ed7021-5e22-4f28-ba47-4050106d111f","extensions":{"alerts":{"initialized":true},"chrome_url_overrides":{},"commands":{},"last_chrome_version":"132.0.6834.83"},"gaia_cookie":{"changed_time":1753261745.285936,"hash":"2jmj7l5rSw0yVb/vlWAYkK/YBwk=","last_list_accounts_data":"[\"gaia.l.a.r\",[]]"},"gcm":{"product_category_for_subtypes":"org.chromium.windows"},"google":{"services":{"signin_scoped_device_id":"aba259d5-7b93-4602-9254-14556d2abff6"}},"history_clusters":{"all_cache":{"all_keywords":{},"all_timestamp":"0"},"short_cache":{"short_keywords":{},"short_timestamp":"0"}},"https_upgrade_navigations":{"2025-08-18":100},"in_product_help":{"new_badge":{"Compose":{"feature_enabled_time":"13399955274182245","show_count":0,"used_count":0},"ComposeNudge":{"feature_enabled_time":"13399955274182272","show_count":0,"used_count":0},"ComposeProactiveNudge":{"feature_enabled_time":"13399955274182278","show_count":0,"used_count":0},"LensOverlay":{"feature_enabled_time":"13399955274182284","show_count":0,"used_count":0}},"recent_session_enabled_time":"13399955274179816","recent_session_start_times":["13399955274179816"],"session_last_active_time":"13399989528974780","session_start_time":"13399955274179816"},"intl":{"accept_languages":"en-US,en","selected_languages":"en-US,en"},"invalidation":{"per_sender_topics_to_handler":{"1013309121859":{}}},"media":{"engagement":{"schema_version":5}},"media_router":{"receiver_id_hash_token":"GDyHbsNwaJywl4I7Xhu2zWpA0xHt8oxXsvhUy4j3sqy1GsJ1F67UrrkMAT2lfWxLRVowNBaruSs3pFlNBSkocQ=="},"ntp":{"num_personal_suggestions":2},"optimization_guide":{"hintsfetcher":{"hosts_successfully_fetched":{}},"pending_hints_processing_version":"572","previous_optimization_types_with_filter":{"AMERICAN_EXPRESS_CREDIT_CARD_FLIGHT_BENEFITS":true,"AMERICAN_EXPRESS_CREDIT_CARD_SUBSCRIPTION_BENEFITS":true,"AUTOFILL_ABLATION_SITES_LIST1":true,"AUTOFILL_ABLATION_SITES_LIST2":true,"AUTOFILL_ABLATION_SITES_LIST3":true,"AUTOFILL_ABLATION_SITES_LIST4":true,"AUTOFILL_ABLATION_SITES_LIST5":true,"AUTOFILL_PREDICTION_IMPROVEMENTS_ALLOWLIST":true,"BUY_NOW_PAY_LATER_ALLOWLIST_AFFIRM":true,"BUY_NOW_PAY_LATER_ALLOWLIST_ZIP":true,"CAPITAL_ONE_CREDIT_CARD_BENEFITS_BLOCKED":true,"CAPITAL_ONE_CREDIT_CARD_DINING_BENEFITS":true,"CAPITAL_ONE_CREDIT_CARD_ENTERTAINMENT_BENEFITS":true,"CAPITAL_ONE_CREDIT_CARD_GROCERY_BENEFITS":true,"CAPITAL_ONE_CREDIT_CARD_STREAMING_BENEFITS":true,"FORMS_ANNOTATIONS":true,"HISTORY_CLUSTERS":true,"HISTORY_EMBEDDINGS":true,"IBAN_AUTOFILL_BLOCKED":true,"PIX_MERCHANT_ORIGINS_ALLOWLIST":true,"PIX_PAYMENT_MERCHANT_ALLOWLIST":true,"SHARED_CREDIT_CARD_DINING_BENEFITS":true,"SHARED_CREDIT_CARD_ENTERTAINMENT_BENEFITS":true,"SHARED_CREDIT_CARD_FLIGHT_BENEFITS":true,"SHARED_CREDIT_CARD_GROCERY_BENEFITS":true,"SHARED_CREDIT_CARD_STREAMING_BENEFITS":true,"SHARED_CREDIT_CARD_SUBSCRIPTION_BENEFITS":true,"SHOPPING_PAGE_PREDICTOR":true,"TEXT_CLASSIFIER_ENTITY_DETECTION":true,"VCN_MERCHANT_OPT_OUT_DISCOVER":true,"VCN_MERCHANT_OPT_OUT_MASTERCARD":true,"VCN_MERCHANT_OPT_OUT_VISA":true},"previously_registered_optimization_types":{"ABOUT_THIS_SITE":true,"HISTORY_CLUSTERS":true,"PRICE_TRACKING":true,"V8_COMPILE_HINTS":true},"store_file_paths_to_delete":{}},"password_manager":{"autofillable_credentials_account_store_login_database":false,"autofillable_credentials_profile_store_login_database":false},"privacy_sandbox":{"first_party_sets_data_access_allowed_initialized":true},"profile":{"avatar_index":26,"content_settings":{"did_migrate_adaptive_notification_quieting_to_cpss":true,"disable_quiet_permission_ui_time":{"notifications":"13390881861687508"},"enable_cpss":{"notifications":true},"enable_quiet_permission_ui":{"notifications":false},"enable_quiet_permission_ui_enabling_method":{"notifications":1},"exceptions":{"3pcd_heuristics_grants":{},"3pcd_support":{},"abusive_notification_permissions":{},"access_to_get_all_screens_media_in_session":{},"anti_abuse":{},"app_banner":{},"ar":{},"auto_picture_in_picture":{},"auto_select_certificate":{},"automatic_downloads":{},"automatic_fullscreen":{},"autoplay":{},"background_sync":{},"bluetooth_chooser_data":{},"bluetooth_guard":{},"bluetooth_scanning":{},"camera_pan_tilt_zoom":{},"captured_surface_control":{},"client_hints":{"https://cn.bing.com:443,*":{"last_modified":"13399890271326482","setting":{"client_hints":[6,8,9,10,11,12,13,14,16,23]}}},"clipboard":{},"cookie_controls_metadata":{},"cookies":{},"direct_sockets":{},"direct_sockets_private_network_access":{},"display_media_system_audio":{},"durable_storage":{},"fedcm_idp_registration":{},"fedcm_idp_signin":{"https://accounts.google.com:443,*":{"last_modified":"13391185831813708","setting":{"chosen-objects":[{"idp-origin":"https://accounts.google.com","idp-signin-status":false}]}}},"fedcm_share":{},"file_system_access_chooser_data":{},"file_system_access_extended_permission":{},"file_system_access_restore_permission":{},"file_system_last_picked_directory":{},"file_system_read_guard":{},"file_system_write_guard":{},"formfill_metadata":{},"geolocation":{},"hand_tracking":{},"hid_chooser_data":{},"hid_guard":{},"http_allowed":{},"https_enforced":{},"idle_detection":{},"images":{},"important_site_info":{},"insecure_private_network":{},"intent_picker_auto_display":{},"javascript":{},"javascript_jit":{},"javascript_optimizer":{},"keyboard_lock":{},"legacy_cookie_access":{},"local_fonts":{},"media_engagement":{"https://casdoor.lingqi.vip:443,*":{"expiration":"13411148676152994","last_modified":"13403372676153000","lifetime":"7776000000000","setting":{"hasHighScore":false,"lastMediaPlaybackTime":0.0,"mediaPlaybacks":0,"visits":8}}},"media_stream_camera":{},"media_stream_mic":{},"midi_sysex":{},"mixed_script":{},"nfc_devices":{},"notification_interactions":{},"notification_permission_review":{},"notifications":{},"password_protection":{},"payment_handler":{},"permission_autoblocking_data":{},"permission_autorevocation_data":{},"pointer_lock":{},"popups":{},"private_network_chooser_data":{},"private_network_guard":{},"protected_media_identifier":{},"protocol_handler":{},"reduced_accept_language":{},"safe_browsing_url_check_data":{},"sensors":{},"serial_chooser_data":{},"serial_guard":{},"site_engagement":{"https://casdoor.lingqi.vip:443,*":{"last_modified":"13407745282379639","setting":{"lastEngagementTime":1.3407716482379188e+16,"lastShortcutLaunchTime":0.0,"pointsAddedToday":0.0,"rawScore":66.81816182171741}},"https://casdoor.org:443,*":{"last_modified":"13407745282379615","setting":{"lastEngagementTime":1.34073298904183e+16,"lastShortcutLaunchTime":0.0,"pointsAddedToday":0.0,"rawScore":9.02839070352501}}},"sound":{},"speaker_selection":{},"ssl_cert_decisions":{},"storage_access":{},"storage_access_header_origin_trial":{},"subresource_filter":{},"subresource_filter_data":{},"third_party_storage_partitioning":{},"top_level_3pcd_origin_trial":{},"top_level_3pcd_support":{},"top_level_storage_access":{},"tracking_protection":{},"unused_site_permissions":{},"usb_chooser_data":{},"usb_guard":{},"vr":{},"web_app_installation":{},"webid_api":{},"webid_auto_reauthn":{},"window_placement":{}},"pref_version":1},"created_by_version":"132.0.6834.83","creation_time":"13390881861649643","did_work_around_bug_364820109_default":true,"did_work_around_bug_364820109_exceptions":true,"exit_type":"Crashed","family_link_user_state":6,"family_member_role":"not_in_family","last_engagement_time":"13407747260152855","last_time_obsolete_http_credentials_removed":1763271873.862677,"last_time_password_store_metrics_reported":1763271711.795472,"managed":{"locally_parent_approved_extensions":{},"locally_parent_approved_extensions_migration_state":1},"managed_user_id":"","name":"Person 1","password_account_storage_settings":{},"password_hash_data_list":[],"were_old_google_logins_removed":true},"safebrowsing":{"event_timestamps":{"0":{"10":[]}},"metrics_last_log_time":"13407745281","saw_interstitial_sber2":true,"scout_reporting_enabled_when_deprecated":false,"unhandled_sync_password_reuses":{}},"safety_hub":{"unused_site_permissions_revocation":{"migration_completed":true}},"saved_tab_groups":{"specifics_to_data_migration":true},"segmentation_platform":{"client_result_prefs":"CmoKGmNocm9tZV9sb3dfdXNlcl9lbmdhZ2VtZW50EkwKQQ0AAIA/EKaaxbjfyOgXGi8KJwolDQAAAD8SF0Nocm9tZUxvd1VzZXJFbmdhZ2VtZW50GgVPdGhlchIEEAcYBCACEOGaxbjfyOgXCuUCChFjcm9zc19kZXZpY2VfdXNlchLPAgrDAg0AAIA/EIWdxbjfyOgXGrACCqcCGqQCChkNAACAPxISTm9Dcm9zc0RldmljZVVzYWdlChgNAAAAQBIRQ3Jvc3NEZXZpY2VNb2JpbGUKGQ0AAEBAEhJDcm9zc0RldmljZURlc2t0b3AKGA0AAIBAEhFDcm9zc0RldmljZVRhYmxldAoiDQAAoEASG0Nyb3NzRGV2aWNlTW9iaWxlQW5kRGVza3RvcAohDQAAwEASGkNyb3NzRGV2aWNlTW9iaWxlQW5kVGFibGV0CiINAADgQBIbQ3Jvc3NEZXZpY2VEZXNrdG9wQW5kVGFibGV0CiANAAAAQRIZQ3Jvc3NEZXZpY2VBbGxEZXZpY2VUeXBlcwoXDQAAEEESEENyb3NzRGV2aWNlT3RoZXISEk5vQ3Jvc3NEZXZpY2VVc2FnZRIEEAcYBCACEK2dxbjfyOgXCmAKEXJlc3VtZV9oZWF2eV91c2VyEksKQA0AAAAAEOSdxbjfyOgXGi4KJgokDQAAAD8SFlJlc3VtZUhlYXZ5VXNlclNlZ21lbnQaBU90aGVyEgQQDhgEIAIQ/p3FuN/I6BcKUgoNc2hvcHBpbmdfdXNlchJBCjYNAAAAABDrm8W438joFxokChwKGg0AAAA/EgxTaG9wcGluZ1VzZXIaBU90aGVyEgQQAhgEIAMQmZzFuN/I6BcKZAoLc2VhcmNoX3VzZXISVQpKDQAAAAAQj6DFuN/I6BcaOAowGi4KCg0AAIA/EgNMb3cKDQ0AAKBAEgZNZWRpdW0KCw0AALBBEgRIaWdoEgROb25lEgQQBxgEIAIQr6DFuN/I6BcKcwoVcGFzc3dvcmRfbWFuYWdlcl91c2VyEloKTw0AAAAAEKyfxbjfyOgXGj0KNQozDQAAAD8SE1Bhc3N3b3JkTWFuYWdlclVzZXIaF05vdF9QYXNzd29yZE1hbmFnZXJVc2VyEgQQBxgEIAEQ6J/FuN/I6Bc=","device_switcher_util":{"result":{"labels":["NotSynced"]}},"last_db_compaction_time":"13407638399000000","uma_in_sql_start_time":"13390881861679139"},"sessions":{"event_log":[{"crashed":true,"time":"13407745413860747","type":0},{"crashed":true,"time":"13407745535072820","type":0},{"crashed":true,"time":"13407745861139052","type":0},{"crashed":true,"time":"13407745891293361","type":0},{"crashed":true,"time":"13407746014783635","type":0},{"crashed":true,"time":"13407746164002293","type":0},{"crashed":true,"time":"13407746471540660","type":0},{"crashed":true,"time":"13407746502953363","type":0},{"crashed":true,"time":"13407746604366841","type":0},{"crashed":true,"time":"13407746729257841","type":0},{"crashed":true,"time":"13407746772870090","type":0},{"crashed":true,"time":"13407746827346731","type":0},{"crashed":true,"time":"13407746898441978","type":0},{"crashed":true,"time":"13407746961671334","type":0},{"crashed":true,"time":"13407747013813891","type":0},{"crashed":true,"time":"13407747068359682","type":0},{"crashed":true,"time":"13407747140080991","type":0},{"crashed":true,"time":"13407747294570358","type":0},{"crashed":true,"time":"13407752173092480","type":0}],"session_data_status":1},"should_read_incoming_syncing_theme_prefs":true,"signin":{"allowed":false},"spellcheck":{"dictionaries":["en-US"],"dictionary":""},"sync":{"data_type_status_for_sync_to_signin":{"app_list":false,"app_settings":false,"apps":false,"arc_package":false,"autofill":false,"autofill_profiles":false,"autofill_wallet":false,"autofill_wallet_credential":false,"autofill_wallet_metadata":false,"autofill_wallet_offer":false,"autofill_wallet_usage":false,"bookmarks":false,"collaboration_group":false,"contact_info":false,"cookies":false,"device_info":false,"dictionary":false,"extension_settings":false,"extensions":false,"history":false,"history_delete_directives":false,"incoming_password_sharing_invitation":false,"managed_user_settings":false,"nigori":false,"os_preferences":false,"os_priority_preferences":false,"outgoing_password_sharing_invitation":false,"passwords":false,"plus_address":false,"plus_address_setting":false,"power_bookmark":false,"preferences":false,"printers":false,"printers_authorization_servers":false,"priority_preferences":false,"product_comparison":false,"reading_list":false,"saved_tab_group":false,"search_engines":false,"security_events":false,"send_tab_to_self":false,"sessions":false,"shared_tab_group_data":false,"sharing_message":false,"themes":false,"user_consent":false,"user_events":false,"web_apps":false,"webapks":false,"webauthn_credential":false,"wifi_configurations":false,"workspace_desk":false},"encryption_bootstrap_token_per_account_migration_done":true,"feature_status_for_sync_to_signin":5},"tab_group_saves_ui_update_migrated":true,"total_passwords_available_for_account":0,"total_passwords_available_for_profile":0,"translate_site_blacklist":[],"translate_site_blocklist_with_time":{},"updateclientdata":{"apps":{"ncennffkjdiamlpmcbajkmaiiiddgioo":{"cohort":"1::","cohortname":"","dlrc":6893,"fp":"1.fc32d125fc37b80eefad0fd38661479f97fdf322709f352ad1350dd8a14f3dcf","installdate":6698,"max_pv":"3.52.13","pf":"b7d4f38a-f370-405d-8c60-645c2f161174","pv":"3.52.14"}}},"updateclientlastupdatecheckerror":-356,"updateclientlastupdatecheckerrorcategory":5,"updateclientlastupdatecheckerrorextracode1":0,"web_apps":{"did_migrate_default_chrome_apps":["MigrateDefaultChromeAppToWebAppsGSuite","MigrateDefaultChromeAppToWebAppsNonGSuite"],"last_preinstall_synchronize_version":"132"}} \ No newline at end of file +{"accessibility":{"captions":{"live_caption_language":"en-US"}},"account_tracker_service_last_update":"13411376389842727","alternate_error_pages":{"backup":true},"announcement_notification_service_first_run_time":"13390881861683238","apps":{"shortcuts_arch":"","shortcuts_version":0},"autocomplete":{"retention_policy_last_version":132},"autofill":{"last_version_deduped":132},"browser":{"app_window_placement":{"DevToolsApp":{"always_on_top":false,"bottom":961,"left":1058,"maximized":false,"right":2138,"top":161,"work_area_bottom":1032,"work_area_left":0,"work_area_right":1920,"work_area_top":0}},"has_seen_welcome_page":false},"commerce_daily_metrics_last_update_time":"13411376389843691","countryid_at_install":17230,"default_apps_install_state":3,"default_search_provider":{"guid":""},"devtools":{"adb_key":"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/fH93dsJxjHkARPbm4ENKQz3YyDD/k9eLSjQRE0mzYLV7BHB3CL8A8gYk8/ohTyTnHQd66224XmN/UogWbGMtejOxyHW/zHgDKeAyW99TljlFwRgeUuagOC3toekozJvP2Lz6c3+prJJjmApbBfpaQ1CQGImOp1yKIsHEeEyy+34GhtZl3763IvQuGdhbXf9nqS1h1pSv13Or5JIGHjRDOY4kZl1ags2RPb92vnpN2imdus+Z9jCKc7ileme2yXpiKZBDzJOh1vRIamb7RWUoOkVz1idsfsScWFDTzv6b7wtbKFBmMvN0SfEg+WQwQP8z+P8QsfdjkIz2K9tRAVVXAgMBAAECggEAFhMBEsGDCtn5vPk2SUlDGJWF1K7HTz840trcA7fogCA+646IdIBynGZKJQpnJF3eUDI9gYMQY/elmrDl9UfLaxuyR+VnBZRJpXu3V7bVmOoxcgQfhqdh5Mh1KvbGTSjBMYW3y1tzNRkqywZk8zdONAeciu/5M5PBjD8TfVWs6dj2qqAOrJQrsH26k5pkG3lykiABavfJrfRrvN/nbdMxlHKv8BYhHWScejMVUR+8wsOqazQjsPW/TtmqUSCwPF8bj7vT19LVqOuE8E1BnaClnCfzqgDISb/kvGPeyvYGZJBA8Bv8PKAat8eZNVvDSGiWGwlqMKC9uMuflafgiEtoMQKBgQD7O+FstyvlT+Q9kNO7GJxptGvgNLpDUgP/8T69UVQuDmGq5pHm1V2sBiIU/gbzqJksnlUY/5+TTIAUrbaNqyoti4lKAzF44JhhE/oECRcxijemfNgT3zO+1rv1vYBvKh5RrjWmdbtKl5VhEByTj4Jj9ksGskYgvupmy1KiRcYFjwKBgQDDHnPlnJRTxd8FLptpikSvBtBX4n09jXb09LRkHbWcoiWSkcKqprNIR7nbU5PIBrniGPYiUcapjksoza4NLPQMZRCVSJ0WW6977piCSx6J7bjkxOVDRxPXcRTYGBM3Skigc/TJbnk9li4kWO/Ey9m7ElK+l/5yQhD5ohuHcpUfuQKBgQC47yFHF6a2TBLkxuE0zi8FGIx4JBggShUrp/fZAC8JIAkA9mzcEJ+9a15XOY8AeVEXqQ1XM4PRt/RoRF8m9aI8mIRc+PDH2/wPKddWdSKfkvDYGvor1peOmR8PC4mpSsW6tpRDjlJp3B9XrbZ7YJ5I2xnrOfupSx/cvzp6vQXBPQKBgAwhFn9NjoLuqACAdzTGXaKrv8PW1oY/BwgPNi+LEUEda0yDNyC4oCh8HPefaG5kzKVbe+GCR+E+cTmmH42+H6WtmqxNCUbciDoiCOUAmnNkjbva6Y2XG4qnAu60AG3NdlpTGwJylaLYdSHZTX5kVbKlXvNuK2ka5kc+Zouy+cYxAoGAaua2QFf45bDw7IaNJRepgHf9VIWjMgkwmf+iRcnKht32tWskhxKiefAaf0ArK2qpeYI5r6jF/ThiDixZ+EGlU9WcfC3cm3LcweY5RRRthnlKIJAsxHgxP4I7O4iBG+hcVaWVnl1S2oHndGldOOJnHtbafRVoZBvRfWGjuusT6lc=","last_open_timestamp":"13408267868797","preferences":{"closeable-tabs":"{\"security\":true,\"freestyler\":true,\"chrome-recorder\":true}","console.sidebar-selected-filter":"\"message\"","console.sidebar.width":"{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","disable-locale-info-bar":"true","elements.styles.sidebar.width":"{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","inspector.drawer-split-view-state":"{\"horizontal\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","inspectorVersion":"38","network-panel-sidebar-state":"{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","network-panel-split-view-state":"{\"vertical\":{\"size\":0}}","network-panel-split-view-waterfall":"{\"vertical\":{\"size\":0}}","panel-selected-tab":"\"console\"","request-info-form-data-category-expanded":"true","request-info-general-category-expanded":"true","request-info-query-string-category-expanded":"true","request-info-request-headers-category-expanded":"true","request-info-request-payload-category-expanded":"true","request-info-response-headers-category-expanded":"true","selected-profile-type":"\"HEAP\"","sources-panel-navigator-split-view-state":"{\"vertical\":{\"size\":0,\"showMode\":\"Both\"}}","sources-panel-split-view-state":"{\"vertical\":{\"size\":0,\"showMode\":\"Both\"}}","styles-pane-sidebar-tab-order":"{\"styles\":10,\"computed\":20}","timeline-counters-split-view-state":"{\"horizontal\":{\"size\":0}}","timeline-panel-sidebar-state":"{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","timeline-tree-view-details-split-widget":"{\"vertical\":{\"size\":0}}"},"synced_preferences_sync_disabled":{"adorner-settings":"[{\"adorner\":\"grid\",\"isEnabled\":true},{\"adorner\":\"subgrid\",\"isEnabled\":true},{\"adorner\":\"flex\",\"isEnabled\":true},{\"adorner\":\"ad\",\"isEnabled\":true},{\"adorner\":\"scroll-snap\",\"isEnabled\":true},{\"adorner\":\"container\",\"isEnabled\":true},{\"adorner\":\"slot\",\"isEnabled\":true},{\"adorner\":\"top-layer\",\"isEnabled\":true},{\"adorner\":\"reveal\",\"isEnabled\":true},{\"adorner\":\"media\",\"isEnabled\":false},{\"adorner\":\"scroll\",\"isEnabled\":true}]","disable-self-xss-warning":"true","language":"\"zh\"","syncedInspectorVersion":"38"}},"domain_diversity":{"last_reporting_timestamp":"13411376389843380"},"enterprise_profile_guid":"07ed7021-5e22-4f28-ba47-4050106d111f","extensions":{"alerts":{"initialized":true},"chrome_url_overrides":{},"commands":{},"last_chrome_version":"132.0.6834.83"},"gaia_cookie":{"changed_time":1753261745.285936,"hash":"2jmj7l5rSw0yVb/vlWAYkK/YBwk=","last_list_accounts_data":"[\"gaia.l.a.r\",[]]"},"gcm":{"product_category_for_subtypes":"org.chromium.windows"},"google":{"services":{"signin_scoped_device_id":"8a3479e8-f4d8-45ec-9392-fe114e7f98ed"}},"history_clusters":{"all_cache":{"all_keywords":{},"all_timestamp":"0"},"short_cache":{"short_keywords":{},"short_timestamp":"0"}},"https_upgrade_navigations":{"2025-08-18":100},"in_product_help":{"new_badge":{"Compose":{"feature_enabled_time":"13399955274182245","show_count":0,"used_count":0},"ComposeNudge":{"feature_enabled_time":"13399955274182272","show_count":0,"used_count":0},"ComposeProactiveNudge":{"feature_enabled_time":"13399955274182278","show_count":0,"used_count":0},"LensOverlay":{"feature_enabled_time":"13399955274182284","show_count":0,"used_count":0}},"recent_session_enabled_time":"13399955274179816","recent_session_start_times":["13408263815088255","13408246635765929"],"session_last_active_time":"13408267868798050","session_start_time":"13408263815088255"},"intl":{"accept_languages":"en-US,en","selected_languages":"en-US,en"},"invalidation":{"per_sender_topics_to_handler":{"1013309121859":{}}},"media":{"engagement":{"schema_version":5}},"media_router":{"receiver_id_hash_token":"GDyHbsNwaJywl4I7Xhu2zWpA0xHt8oxXsvhUy4j3sqy1GsJ1F67UrrkMAT2lfWxLRVowNBaruSs3pFlNBSkocQ=="},"ntp":{"num_personal_suggestions":8},"optimization_guide":{"hintsfetcher":{"hosts_successfully_fetched":{}},"previous_optimization_types_with_filter":{"AMERICAN_EXPRESS_CREDIT_CARD_FLIGHT_BENEFITS":true,"AMERICAN_EXPRESS_CREDIT_CARD_SUBSCRIPTION_BENEFITS":true,"AUTOFILL_ABLATION_SITES_LIST1":true,"AUTOFILL_ABLATION_SITES_LIST2":true,"AUTOFILL_ABLATION_SITES_LIST3":true,"AUTOFILL_ABLATION_SITES_LIST4":true,"AUTOFILL_ABLATION_SITES_LIST5":true,"BUY_NOW_PAY_LATER_ALLOWLIST_AFFIRM":true,"BUY_NOW_PAY_LATER_ALLOWLIST_ZIP":true,"CAPITAL_ONE_CREDIT_CARD_BENEFITS_BLOCKED":true,"CAPITAL_ONE_CREDIT_CARD_DINING_BENEFITS":true,"CAPITAL_ONE_CREDIT_CARD_ENTERTAINMENT_BENEFITS":true,"CAPITAL_ONE_CREDIT_CARD_GROCERY_BENEFITS":true,"CAPITAL_ONE_CREDIT_CARD_STREAMING_BENEFITS":true,"HISTORY_CLUSTERS":true,"HISTORY_EMBEDDINGS":true,"IBAN_AUTOFILL_BLOCKED":true,"PIX_MERCHANT_ORIGINS_ALLOWLIST":true,"PIX_PAYMENT_MERCHANT_ALLOWLIST":true,"SHARED_CREDIT_CARD_DINING_BENEFITS":true,"SHARED_CREDIT_CARD_ENTERTAINMENT_BENEFITS":true,"SHARED_CREDIT_CARD_FLIGHT_BENEFITS":true,"SHARED_CREDIT_CARD_GROCERY_BENEFITS":true,"SHARED_CREDIT_CARD_STREAMING_BENEFITS":true,"SHARED_CREDIT_CARD_SUBSCRIPTION_BENEFITS":true,"SHOPPING_PAGE_PREDICTOR":true,"TEXT_CLASSIFIER_ENTITY_DETECTION":true,"VCN_MERCHANT_OPT_OUT_DISCOVER":true,"VCN_MERCHANT_OPT_OUT_MASTERCARD":true,"VCN_MERCHANT_OPT_OUT_VISA":true},"previously_registered_optimization_types":{"ABOUT_THIS_SITE":true,"HISTORY_CLUSTERS":true,"PRICE_TRACKING":true,"V8_COMPILE_HINTS":true},"store_file_paths_to_delete":{}},"partition":{"per_host_zoom_levels":{"x":{"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAACQCAYAAAABQ+u5AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAABkhJREFUeNrs2O1L02sYB/Dvfu5Z5zbnmnO5CZIwQnogDJktbGJgJViNKBpEsSAoQuiRouhN/QlBbxJ6ERREhUVaCxuumJWzwWZN5rBlukct25N7uM+7cc7Bde7fzpvjYT/Yq93XtXv3dV0fftwcAAQsHy6XC5FIhGw2C4ZtcFVVFRQKBTZu3AiRSAQum2CxWAydTgeFQoHZ2VkkEgn6BI2Njejq6oJOp8PQ0BAikQhWVlboEmzfvh0nTpyAVqvF48ePMTs7i3Q6Xfye/O5z6dIlMjU1Rb5+/UoGBweJTqf7+5rSwVeuXCGpVIpkMhni9XqJyWRabd3qwbdu3SL5fJ7k83mSTqfJjRs3Sv3Q6glyuRwpFAokl8uRq1ev/u5vlk6Qy+XIzZs3f3tGnHI68c8Pg3/5VBKA3TT+2QOhUIhcLleeB3V1ddDr9RAKhex2IBKJoNVqIZfLEQwGkUwm6RM0NDTAaDSiqakJL168QCwWo/dg27ZtsFgsWL9+PZ49e4ZgMEjvwcDAABkfHycej4fcvn2bNDU10Xtw7tw5srCwQMLhMHE6nWTnzp3003j9+nUSj8dJPB4noVCIXL58md04x2Ixsri4SGKxGDl//jx7D2KxGInFYuTatWsVD/6/HggEgvI9kMlkaG1tLc8DjUYDuVyOb9++sfNApVKho6MDGo0GNpsN8Xgc2WyWLsHmzZtx6NAhaDQaDA8PY25uDplMhu4QT506hYMHD0IsFuPt27cYHR3F8vLyX9aUHJTTp0+TL1++kOnpafLq1StiMBjop/HixYtkZmaGzMzMEJ/PRwYGBtiNs9/vJ4FAgPj9fnLmzBn2Hvj9fuL3+8mFCxcqHvw/Paiqqir//YBhGMhkMrS0tEAgELDbgVAoRGNjI6RSKebn55FKpegTKJVKtLe3Q61Ww263Y3Fxkd6DtrY29Pf3Q61W4/Xr1/j+/Tu9B8ePH0dfXx9EIhHGx8cxNjaGX79+0VXBarXCarWCYRgEg0EMDQ1hfn6eroxnz57FsWPHAAC5XA4vX76E0+lcde2qY+pyucjk5CRxuVzk5MmTJcf5t31ACMHdu3dx586dkmsqHqxpD8p+P2AYBrW1tWhubmbvgUAgQENDA2praxEKhdh5oFAosGXLFqhUKrx79w5LS0vI5XJ0CfR6PXp7e6FSqTA2NoZQKISVlRW6Qzxy5Ah2794NoVAIl8sFp9OJRCJBV4WjR4/CYrGAYRjMzc1hZGQEoVCIroxWqxWHDx8uevDmzRtMTEzQT6PNZgOHwwEhBIODg7h37x77TiSE4P79+yWDKx6seQ/4fD7y+Xx5HkgkEmi1WvD5fPYeKJVKSCQSRCIRpNNp+gR1dXVoa2uDUqnEhw8f8PPnT3oPWltb0d3dDaVSCafTiXA4TO/BgQMH0NXVBaFQCLfbjY8fPyKZTNJVwWw2w2w2g2EYLCwsYHR0FJFIhK6MFosF+/fvL3rgcDjgdrvp+6C/v7840g8fPsSDBw/K8+DRo0clgyserHkPeDxe+R7U1NRAo9Gw94DP56O+vh41NTWIRqPIZDL0CWQyGfR6Perr6zE5OYnl5WV6D1paWmA0GqFQKDAxMYFoNIpsNkt3iHv37kVnZycEAgG8Xi/cbjdSqRRdFfr6+tDX1wcOh4NwOAyHw4FoNEpXRrPZjD179gAA8vk8xsfH4fV66fugt7e3ONJPnjzB06dPy/Pg+fPnJYMrHqxpDxiGAY/HQ6FQKM+D6upqqNVq8Hg89h7I5XJUV1cjHo+z80AqlWLDhg2Qy+XweDxIJBLI5/N0CXQ6HTo6OiCXy+F2u4uXD1SH2N3djfb2dggEAvh8PkxNTdF70NPTg56eHjAMg2g0ivfv3yMej9OVcd++fTCZTEUPXC4XfD4ffR/s2rWrONIjIyMYHh4uzwObzVYyuOLBmveAy+WW5wGHw4FIJMK6devA5XLZ7YDH40EqlUIsFmNpaYn+OhAAJBIJmpubIZPJ4PP5kEwm6T3QaDTYunUrpFIpPn/+XLx8oDrEzs5ObNq0CXw+H4FAANPT08XLyH9MsGPHDhiNRnA4HCwuLuLTp0/48eMHXRlNJhMMBgMAoFAowOPxIBAI0PeBwWAo3h/Y7XbY7fbyPHA4HCWDKx78VxL8MQAkmJQSUYGOsAAAAABJRU5ErkJggg==":{"last_modified":"13410191271557769","zoom_level":-1.5778829311823859},"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAH2mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDggNzkuMTY0MDM2LCAyMDE5LzA4LzEzLTAxOjA2OjU3ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIzLTA3LTE3VDE2OjIyOjM3KzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyNC0wNy0zMFQxMDo1NToyMSswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNC0wNy0zMFQxMDo1NToyMSswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpjZDUzMmY3NS0wNGNjLTYyNDQtYmZmMi1lNTAzZTU2YzFiNzgiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo4ZGQ2ZWE3NC0wMTFmLWJhNDgtOTdhMS1lODFhNDU4ZGEzMGQiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo1MDU2NDgxNS1lNjU2LWE5NGYtOGM0ZC1mODZhY2ZkZTcwODQiPiA8cGhvdG9zaG9wOkRvY3VtZW50QW5jZXN0b3JzPiA8cmRmOkJhZz4gPHJkZjpsaT41MzY5NDE5NDY1QzdCMjNBNjdCN0JENjk0MzFEOEM1NjwvcmRmOmxpPiA8cmRmOmxpPmFkb2JlOmRvY2lkOnBob3Rvc2hvcDo3NWZjNjY4Ni1hNDU4LTBkNGItYmVkZi1mNTJiMjFlZjY5NjY8L3JkZjpsaT4gPHJkZjpsaT5hZG9iZTpkb2NpZDpwaG90b3Nob3A6ZDg3NDA2YmMtZTBjMi0wMzQ4LWJhYjYtNjUxYTFhY2FiNWY3PC9yZGY6bGk+IDwvcmRmOkJhZz4gPC9waG90b3Nob3A6RG9jdW1lbnRBbmNlc3RvcnM+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6NTA1NjQ4MTUtZTY1Ni1hOTRmLThjNGQtZjg2YWNmZGU3MDg0IiBzdEV2dDp3aGVuPSIyMDIzLTA3LTE3VDE2OjIyOjM3KzA4OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjEuMCAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQyYjkyZjNiLTUwOGYtZWI0Yy04MGRkLTZiZmYwYWEyYmQ4NiIgc3RFdnQ6d2hlbj0iMjAyMy0wNy0xN1QxNjoyMzozMiswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDIxLjAgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpjZDUzMmY3NS0wNGNjLTYyNDQtYmZmMi1lNTAzZTU2YzFiNzgiIHN0RXZ0OndoZW49IjIwMjQtMDctMzBUMTA6NTU6MjErMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMS4wIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7oPIg7AAACI0lEQVRYhb1XzUrcUBT+TrA6aFf2AURwPSs3Xbhy47LQZ+hDdFeh4CMIPoNMRUFQFK2000WlFDo2DupYpDQzulG0/o75XCSTuUnunUkyTg5ckpyce7/vnHNP7okgByEZ3C9WwBcWYQEAJA94jwBJbB0wJJtVhVm/CXz/w5jsHpNWHgR+/QUnx+L6wYGUC7VCmWYcNuKek+TPE5JpM5AWvNpwteDVesbcpwHfN4Dbnj4TfmLwvbo+7L4+VJbPTsAEXnHa4H0jYBvAbcXzvhGwu+S8rwQqjh58r/6oBe9KQFmjKwH71ABu8DwNAfWqJXB+qQevNNrzeiUQvQ/GxZUe/Me/zp5nIRB7vrzRg5dPkoGrBLQHsm/AyHsCkOs7cngoPu36jhgeksQHvPim3QhEbbSx+39LjBSSg6sEOh3HwYIiEkyIytWtmxpclZ76gfsm8bJg9dRXdUpB8AhARCSaGuP8RMBJgqZWQEQHkjhznJ57OmNTRBK7zm82789RfFUkSdEwF5+d7l1y0dWp67Zr/VttRy13nX0oSmlEuwnLtc9UvRksjAKelwJvD7RGS8Tz4xlOuS9H26Gvmx+JmJ0hItl7vfWlT1wrlULgX2vbxg2oIZI9D+tLpeg6LHt5z948JBQLAPjohpQbK8t4PT6Vy4+bNf9xlpPFmZAyp19GAMCA5TYxOjGC5gOxs1oCCEy/eZsbB1n48J7ip/nd7FyezgMAngBiObqoWMJNIAAAAABJRU5ErkJggg==":{"last_modified":"13410190740190610","zoom_level":-3.8017840169239308},"data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+PGh0bWw+PGJvZHkgc3R5bGU9Im1hcmdpbjowO2JhY2tncm91bmQ6IzBiMGIwYjsiPjxwcmUgc3R5bGU9ImNvbG9yOiNlNmU2ZTY7cGFkZGluZzoxMHB4O2ZvbnQtZmFtaWx5Om1vbm9zcGFjZTt3aGl0ZS1zcGFjZTpwcmUtd3JhcDsiPnsKICAibWFwcGluZ3MiOiB7CiAgICAibmV0L2RpZXh2L2RpZXh2c3dvcmQvbWl4aW4vRGVhdGhUaW1lTG9ja01peGluIjogewogICAgICAiYWN0dWFsbHlIdXJ0IjogIkxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9MaXZpbmdFbnRpdHk7bV82NDc1XyhMbmV0L21pbmVjcmFmdC93b3JsZC9kYW1hZ2Vzb3VyY2UvRGFtYWdlU291cmNlO0YpViIsCiAgICAgICJ0aWNrIjogIkxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9MaXZpbmdFbnRpdHk7bV84MTE5XygpViIKICAgIH0sCiAgICAibmV0L2RpZXh2L2RpZXh2c3dvcmQvbWl4aW4vU2NyZWVuTWl4aW4iOiB7CiAgICAgICJyZW5kZXJCYWNrZ3JvdW5kIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvc2NyZWVucy9TY3JlZW47bV8yODAyNzNfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7KVYiLAogICAgICAicmVuZGVyIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvc2NyZWVucy9TY3JlZW47bV84ODMxNV8oTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9HdWlHcmFwaGljcztJSUYpViIKICAgIH0sCiAgICAibmV0L2RpZXh2L2RpZXh2c3dvcmQvbWl4aW4vRnVja1JlbmRlck1peGluIjogewogICAgICAicmVuZGVyQmFja2dyb3VuZCI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL3NjcmVlbnMvU2NyZWVuO21fMjgwMjczXyhMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL0d1aUdyYXBoaWNzOylWIiwKICAgICAgInJlbmRlciI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL3NjcmVlbnMvU2NyZWVuO21fODgzMTVfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7SUlGKVYiCiAgICB9LAogICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL0VudGl0eVNlbGVjdG9yTWl4aW4iOiB7CiAgICAgICJmaW5kRW50aXRpZXMiOiAiTG5ldC9taW5lY3JhZnQvY29tbWFuZHMvYXJndW1lbnRzL3NlbGVjdG9yL0VudGl0eVNlbGVjdG9yO21fMTIxMTYwXyhMbmV0L21pbmVjcmFmdC9jb21tYW5kcy9Db21tYW5kU291cmNlU3RhY2s7KUxqYXZhL3V0aWwvTGlzdDsiLAogICAgICAiZmluZFNpbmdsZUVudGl0eSI6ICJMbmV0L21pbmVjcmFmdC9jb21tYW5kcy9hcmd1bWVudHMvc2VsZWN0b3IvRW50aXR5U2VsZWN0b3I7bV8xMjExMzlfKExuZXQvbWluZWNyYWZ0L2NvbW1hbmRzL0NvbW1hbmRTb3VyY2VTdGFjazspTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L0VudGl0eTsiCiAgICB9LAogICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL1BsYXllck1peGluIjogewogICAgICAiZ2V0TWF4SGVhbHRoIjogIkxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9MaXZpbmdFbnRpdHk7bV8yMTIzM18oKUYiLAogICAgICAiZ2V0SGVhbHRoIjogIkxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9MaXZpbmdFbnRpdHk7bV8yMTIyM18oKUYiCiAgICB9LAogICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL0xldmVsUmVuZGVyZXJNaXhpbiI6IHsKICAgICAgInJlbmRlckRlYnVnIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9MZXZlbFJlbmRlcmVyO21fMjY5MjQwXyhMY29tL21vamFuZy9ibGF6ZTNkL3ZlcnRleC9Qb3NlU3RhY2s7TG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL011bHRpQnVmZmVyU291cmNlO0xuZXQvbWluZWNyYWZ0L2NsaWVudC9DYW1lcmE7KVYiLAogICAgICAicmVuZGVyRW5kU2t5IjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9MZXZlbFJlbmRlcmVyO21fMTA5NzgwXyhMY29tL21vamFuZy9ibGF6ZTNkL3ZlcnRleC9Qb3NlU3RhY2s7KVYiLAogICAgICAicmVuZGVyU2hhcGUiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL0xldmVsUmVuZGVyZXI7bV8xMDk3ODJfKExjb20vbW9qYW5nL2JsYXplM2QvdmVydGV4L1Bvc2VTdGFjaztMY29tL21vamFuZy9ibGF6ZTNkL3ZlcnRleC9WZXJ0ZXhDb25zdW1lcjtMbmV0L21pbmVjcmFmdC93b3JsZC9waHlzL3NoYXBlcy9Wb3hlbFNoYXBlO0REREZGRkYpViIsCiAgICAgICJyZW5kZXJTbm93QW5kUmFpbiI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvTGV2ZWxSZW5kZXJlcjttXzEwOTcwM18oTG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL0xpZ2h0VGV4dHVyZTtGREREKVYiLAogICAgICAic2V0dXBSZW5kZXIiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL0xldmVsUmVuZGVyZXI7bV8xOTQzMzhfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9DYW1lcmE7TG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL2N1bGxpbmcvRnJ1c3R1bTtaWilWIiwKICAgICAgInJlbmRlckNsb3VkcyI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvTGV2ZWxSZW5kZXJlcjttXzI1MzA1NF8oTGNvbS9tb2phbmcvYmxhemUzZC92ZXJ0ZXgvUG9zZVN0YWNrO0xvcmcvam9tbC9NYXRyaXg0ZjtGREREKVYiLAogICAgICAicmVuZGVyU2t5IjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9MZXZlbFJlbmRlcmVyO21fMjAyNDIzXyhMY29tL21vamFuZy9ibGF6ZTNkL3ZlcnRleC9Qb3NlU3RhY2s7TG9yZy9qb21sL01hdHJpeDRmO0ZMbmV0L21pbmVjcmFmdC9jbGllbnQvQ2FtZXJhO1pMamF2YS9sYW5nL1J1bm5hYmxlOylWIgogICAgfSwKICAgICJuZXQvZGlleHYvZGlleHZzd29yZC9taXhpbi9MaWZlTG9ja01peGluIjogewogICAgICAiaGVhbCI6ICJMbmV0L21pbmVjcmFmdC93b3JsZC9lbnRpdHkvTGl2aW5nRW50aXR5O21fNTYzNF8oRilWIiwKICAgICAgImdldEhlYWx0aCI6ICJMbmV0L21pbmVjcmFmdC93b3JsZC9lbnRpdHkvTGl2aW5nRW50aXR5O21fMjEyMjNfKClGIiwKICAgICAgInNldEhlYWx0aCI6ICJMbmV0L21pbmVjcmFmdC93b3JsZC9lbnRpdHkvTGl2aW5nRW50aXR5O21fMjExNTNfKEYpViIKICAgIH0sCiAgICAibmV0L2RpZXh2L2RpZXh2c3dvcmQvbWl4aW4vRGlleHZBbnRpRGlzYXJtTWl4aW4iOiB7CiAgICAgICJkcm9wKExuZXQvbWluZWNyYWZ0L3dvcmxkL2l0ZW0vSXRlbVN0YWNrO1paKUxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9pdGVtL0l0ZW1FbnRpdHk7IjogIkxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9wbGF5ZXIvUGxheWVyO21fNzE5N18oTG5ldC9taW5lY3JhZnQvd29ybGQvaXRlbS9JdGVtU3RhY2s7WlopTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L2l0ZW0vSXRlbUVudGl0eTsiCiAgICB9LAogICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL1N5bmNoZWREYXRhTWl4aW4iOiB7CiAgICAgICJnZXQiOiAiTG5ldC9taW5lY3JhZnQvbmV0d29yay9zeW5jaGVyL1N5bmNoZWRFbnRpdHlEYXRhO21fMTM1MzcwXyhMbmV0L21pbmVjcmFmdC9uZXR3b3JrL3N5bmNoZXIvRW50aXR5RGF0YUFjY2Vzc29yOylMamF2YS9sYW5nL09iamVjdDsiCiAgICB9LAogICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL0NsaWVudERlYXRoVGltZU1peGluIjogewogICAgICAiZ2V0V2hpdGVPdmVybGF5UHJvZ3Jlc3MiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL2VudGl0eS9MaXZpbmdFbnRpdHlSZW5kZXJlcjttXzY5MzFfKExuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9MaXZpbmdFbnRpdHk7RilGIgogICAgfSwKICAgICJuZXQvZGlleHYvZGlleHZzd29yZC9taXhpbi9JdGVtUmVuZGVyZXJNaXhpbiI6IHsKICAgICAgInJlbmRlciI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvZW50aXR5L0l0ZW1SZW5kZXJlcjttXzExNTE0M18oTG5ldC9taW5lY3JhZnQvd29ybGQvaXRlbS9JdGVtU3RhY2s7TG5ldC9taW5lY3JhZnQvd29ybGQvaXRlbS9JdGVtRGlzcGxheUNvbnRleHQ7Wkxjb20vbW9qYW5nL2JsYXplM2QvdmVydGV4L1Bvc2VTdGFjaztMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvTXVsdGlCdWZmZXJTb3VyY2U7SUlMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVzb3VyY2VzL21vZGVsL0Jha2VkTW9kZWw7KVYiCiAgICB9LAogICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL0d1aUdyYXBoaWNzTWl4aW4iOiB7CiAgICAgICJmaWxsR3JhZGllbnQoSUlJSUlJSSlWIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7bV8yODAxMjBfKElJSUlJSUkpViIsCiAgICAgICJkcmF3Q2VudGVyZWRTdHJpbmcoTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9Gb250O0xuZXQvbWluZWNyYWZ0L25ldHdvcmsvY2hhdC9Db21wb25lbnQ7SUlJKVYiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9HdWlHcmFwaGljczttXzI4MDY1M18oTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9Gb250O0xuZXQvbWluZWNyYWZ0L25ldHdvcmsvY2hhdC9Db21wb25lbnQ7SUlJKVYiLAogICAgICAiZHJhd1dvcmRXcmFwIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7bV8yODA1NTRfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvRm9udDtMbmV0L21pbmVjcmFmdC9uZXR3b3JrL2NoYXQvRm9ybWF0dGVkVGV4dDtJSUlJKVYiLAogICAgICAicmVuZGVyRmFrZUl0ZW0iOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9HdWlHcmFwaGljczttXzI4MDIwM18oTG5ldC9taW5lY3JhZnQvd29ybGQvaXRlbS9JdGVtU3RhY2s7SUkpViIsCiAgICAgICJibGl0KExuZXQvbWluZWNyYWZ0L3Jlc291cmNlcy9SZXNvdXJjZUxvY2F0aW9uO0lJSUlJSSlWIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7bV8yODAyMThfKExuZXQvbWluZWNyYWZ0L3Jlc291cmNlcy9SZXNvdXJjZUxvY2F0aW9uO0lJSUlJSSlWIiwKICAgICAgImRyYXdDZW50ZXJlZFN0cmluZyhMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL0ZvbnQ7TG5ldC9taW5lY3JhZnQvdXRpbC9Gb3JtYXR0ZWRDaGFyU2VxdWVuY2U7SUlJKVYiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9HdWlHcmFwaGljczttXzI4MDM2NF8oTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9Gb250O0xuZXQvbWluZWNyYWZ0L3V0aWwvRm9ybWF0dGVkQ2hhclNlcXVlbmNlO0lJSSlWIiwKICAgICAgImJsaXQoTG5ldC9taW5lY3JhZnQvcmVzb3VyY2VzL1Jlc291cmNlTG9jYXRpb247SUlGRklJSUkpViI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL0d1aUdyYXBoaWNzO21fMjgwMTYzXyhMbmV0L21pbmVjcmFmdC9yZXNvdXJjZXMvUmVzb3VyY2VMb2NhdGlvbjtJSUZGSUlJSSlWIiwKICAgICAgImJsaXQoTG5ldC9taW5lY3JhZnQvcmVzb3VyY2VzL1Jlc291cmNlTG9jYXRpb247SUlJSUZGSUlJSSlWIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7bV8yODA0MTFfKExuZXQvbWluZWNyYWZ0L3Jlc291cmNlcy9SZXNvdXJjZUxvY2F0aW9uO0lJSUlGRklJSUkpViIsCiAgICAgICJkcmF3Q2VudGVyZWRTdHJpbmcoTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9Gb250O0xqYXZhL2xhbmcvU3RyaW5nO0lJSSlWIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7bV8yODAxMzdfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvRm9udDtMamF2YS9sYW5nL1N0cmluZztJSUkpViIsCiAgICAgICJmaWxsR3JhZGllbnQoTG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL1JlbmRlclR5cGU7SUlJSUlJSSlWIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7bV8yODU5NzhfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9SZW5kZXJUeXBlO0lJSUlJSUkpViIsCiAgICAgICJmaWxsR3JhZGllbnQoSUlJSUlJKVYiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9HdWlHcmFwaGljczttXzI4MDAyNF8oSUlJSUlJKVYiLAogICAgICAiYmxpdChMbmV0L21pbmVjcmFmdC9yZXNvdXJjZXMvUmVzb3VyY2VMb2NhdGlvbjtJSUlGRklJSUkpViI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL0d1aUdyYXBoaWNzO21fMjgwMzk4XyhMbmV0L21pbmVjcmFmdC9yZXNvdXJjZXMvUmVzb3VyY2VMb2NhdGlvbjtJSUlGRklJSUkpViIKICAgIH0sCiAgICAibmV0L2RpZXh2L2RpZXh2c3dvcmQvbWl4aW4vRW50aXR5UmVuZGVyTWl4aW4iOiB7CiAgICAgICJyZW5kZXJFbnRpdHkiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL0xldmVsUmVuZGVyZXI7bV8xMDk1MTdfKExuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9FbnRpdHk7RERERkxjb20vbW9qYW5nL2JsYXplM2QvdmVydGV4L1Bvc2VTdGFjaztMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvTXVsdGlCdWZmZXJTb3VyY2U7KVYiCiAgICB9LAogICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL0dhbWVSZW5kZXJlck1peGluIjogewogICAgICAic2h1dGRvd25TaGFkZXJzIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9HYW1lUmVuZGVyZXI7bV8xNzI3NTlfKClWIiwKICAgICAgImdldFJlbmRlckRpc3RhbmNlIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9HYW1lUmVuZGVyZXI7bV8xMDkxNTJfKClGIiwKICAgICAgInNodXRkb3duRWZmZWN0IjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9HYW1lUmVuZGVyZXI7bV8xMDkwODZfKClWIiwKICAgICAgInJlbmRlciI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvR2FtZVJlbmRlcmVyO21fMTA5MDkzXyhGSlopViIKICAgIH0sCiAgICAibmV0L2RpZXh2L2RpZXh2c3dvcmQvbWl4aW4vRW50aXR5QW5pbWF0aW9uTWl4aW4iOiB7CiAgICAgICJ0aWNrIjogIkxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9MaXZpbmdFbnRpdHk7bV84MTE5XygpViIsCiAgICAgICJ0aWNrRGVhdGgiOiAiTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L0xpdmluZ0VudGl0eTttXzYxNTNfKClWIgogICAgfSwKICAgICJuZXQvZGlleHYvZGlleHZzd29yZC9taXhpbi9EaWV4dkludmVudG9yeVByb3RlY3Rpb25NaXhpbiI6IHsKICAgICAgInN3YXBQYWludCI6ICJMbmV0L21pbmVjcmFmdC93b3JsZC9lbnRpdHkvcGxheWVyL0ludmVudG9yeTttXzM1OTg4XyhEKVYiLAogICAgICAiZHJvcEFsbCI6ICJMbmV0L21pbmVjcmFmdC93b3JsZC9lbnRpdHkvcGxheWVyL0ludmVudG9yeTttXzM2MDcxXygpViIsCiAgICAgICJjbGVhckNvbnRlbnQiOiAiTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L3BsYXllci9JbnZlbnRvcnk7bV82MjExXygpViIsCiAgICAgICJzZXRJdGVtIjogIkxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9wbGF5ZXIvSW52ZW50b3J5O21fNjgzNl8oSUxuZXQvbWluZWNyYWZ0L3dvcmxkL2l0ZW0vSXRlbVN0YWNrOylWIiwKICAgICAgInJlbW92ZUl0ZW0oSUkpTG5ldC9taW5lY3JhZnQvd29ybGQvaXRlbS9JdGVtU3RhY2s7IjogIkxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9wbGF5ZXIvSW52ZW50b3J5O21fNzQwN18oSUkpTG5ldC9taW5lY3JhZnQvd29ybGQvaXRlbS9JdGVtU3RhY2s7IgogICAgfSwKICAgICJuZXQvZGlleHYvZGlleHZzd29yZC9taXhpbi9XaW5kb3dNaXhpbiI6IHsKICAgICAgInJlbmRlciI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvR2FtZVJlbmRlcmVyO21fMTA5MDkzXyhGSlopViIKICAgIH0sCiAgICAibmV0L2RpZXh2L2RpZXh2c3dvcmQvbWl4aW4vSlNDTWl4aW4iOiB7CiAgICAgICJhZGRFbnRpdHkiOiAiTG5ldC9taW5lY3JhZnQvc2VydmVyL2xldmVsL1NlcnZlckxldmVsO21fODg3Ml8oTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L0VudGl0eTspWiIKICAgIH0sCiAgICAibmV0L2RpZXh2L2RpZXh2c3dvcmQvbWl4aW4vV2luZG93VGl0bGVEaWN0YXRvck1peGluIjogewogICAgICAic2V0VGl0bGUiOiAiTGNvbS9tb2phbmcvYmxhemUzZC9wbGF0Zm9ybS9XaW5kb3c7bV84NTQyMl8oTGphdmEvbGFuZy9TdHJpbmc7KVYiCiAgICB9LAogICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL1NldFNjcmVlbk1peGluIjogewogICAgICAidGljayI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvTWluZWNyYWZ0O21fOTEzOThfKClWIiwKICAgICAgInNldFNjcmVlbiI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvTWluZWNyYWZ0O21fOTExNTJfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvc2NyZWVucy9TY3JlZW47KVYiCiAgICB9LAogICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL0xldmVsVGlja01peGluIjogewogICAgICAidGljayI6ICJMbmV0L21pbmVjcmFmdC9zZXJ2ZXIvbGV2ZWwvU2VydmVyTGV2ZWw7bV84NzkzXyhMamF2YS91dGlsL2Z1bmN0aW9uL0Jvb2xlYW5TdXBwbGllcjspViIKICAgIH0KICB9LAogICJkYXRhIjogewogICAgInNlYXJnZSI6IHsKICAgICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL0RlYXRoVGltZUxvY2tNaXhpbiI6IHsKICAgICAgICAiYWN0dWFsbHlIdXJ0IjogIkxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9MaXZpbmdFbnRpdHk7bV82NDc1XyhMbmV0L21pbmVjcmFmdC93b3JsZC9kYW1hZ2Vzb3VyY2UvRGFtYWdlU291cmNlO0YpViIsCiAgICAgICAgInRpY2siOiAiTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L0xpdmluZ0VudGl0eTttXzgxMTlfKClWIgogICAgICB9LAogICAgICAibmV0L2RpZXh2L2RpZXh2c3dvcmQvbWl4aW4vU2NyZWVuTWl4aW4iOiB7CiAgICAgICAgInJlbmRlckJhY2tncm91bmQiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9zY3JlZW5zL1NjcmVlbjttXzI4MDI3M18oTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9HdWlHcmFwaGljczspViIsCiAgICAgICAgInJlbmRlciI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL3NjcmVlbnMvU2NyZWVuO21fODgzMTVfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7SUlGKVYiCiAgICAgIH0sCiAgICAgICJuZXQvZGlleHYvZGlleHZzd29yZC9taXhpbi9GdWNrUmVuZGVyTWl4aW4iOiB7CiAgICAgICAgInJlbmRlckJhY2tncm91bmQiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9zY3JlZW5zL1NjcmVlbjttXzI4MDI3M18oTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9HdWlHcmFwaGljczspViIsCiAgICAgICAgInJlbmRlciI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL3NjcmVlbnMvU2NyZWVuO21fODgzMTVfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7SUlGKVYiCiAgICAgIH0sCiAgICAgICJuZXQvZGlleHYvZGlleHZzd29yZC9taXhpbi9FbnRpdHlTZWxlY3Rvck1peGluIjogewogICAgICAgICJmaW5kRW50aXRpZXMiOiAiTG5ldC9taW5lY3JhZnQvY29tbWFuZHMvYXJndW1lbnRzL3NlbGVjdG9yL0VudGl0eVNlbGVjdG9yO21fMTIxMTYwXyhMbmV0L21pbmVjcmFmdC9jb21tYW5kcy9Db21tYW5kU291cmNlU3RhY2s7KUxqYXZhL3V0aWwvTGlzdDsiLAogICAgICAgICJmaW5kU2luZ2xlRW50aXR5IjogIkxuZXQvbWluZWNyYWZ0L2NvbW1hbmRzL2FyZ3VtZW50cy9zZWxlY3Rvci9FbnRpdHlTZWxlY3RvcjttXzEyMTEzOV8oTG5ldC9taW5lY3JhZnQvY29tbWFuZHMvQ29tbWFuZFNvdXJjZVN0YWNrOylMbmV0L21pbmVjcmFmdC93b3JsZC9lbnRpdHkvRW50aXR5OyIKICAgICAgfSwKICAgICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL1BsYXllck1peGluIjogewogICAgICAgICJnZXRNYXhIZWFsdGgiOiAiTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L0xpdmluZ0VudGl0eTttXzIxMjMzXygpRiIsCiAgICAgICAgImdldEhlYWx0aCI6ICJMbmV0L21pbmVjcmFmdC93b3JsZC9lbnRpdHkvTGl2aW5nRW50aXR5O21fMjEyMjNfKClGIgogICAgICB9LAogICAgICAibmV0L2RpZXh2L2RpZXh2c3dvcmQvbWl4aW4vTGV2ZWxSZW5kZXJlck1peGluIjogewogICAgICAgICJyZW5kZXJEZWJ1ZyI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvTGV2ZWxSZW5kZXJlcjttXzI2OTI0MF8oTGNvbS9tb2phbmcvYmxhemUzZC92ZXJ0ZXgvUG9zZVN0YWNrO0xuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9NdWx0aUJ1ZmZlclNvdXJjZTtMbmV0L21pbmVjcmFmdC9jbGllbnQvQ2FtZXJhOylWIiwKICAgICAgICAicmVuZGVyRW5kU2t5IjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9MZXZlbFJlbmRlcmVyO21fMTA5NzgwXyhMY29tL21vamFuZy9ibGF6ZTNkL3ZlcnRleC9Qb3NlU3RhY2s7KVYiLAogICAgICAgICJyZW5kZXJTaGFwZSI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvTGV2ZWxSZW5kZXJlcjttXzEwOTc4Ml8oTGNvbS9tb2phbmcvYmxhemUzZC92ZXJ0ZXgvUG9zZVN0YWNrO0xjb20vbW9qYW5nL2JsYXplM2QvdmVydGV4L1ZlcnRleENvbnN1bWVyO0xuZXQvbWluZWNyYWZ0L3dvcmxkL3BoeXMvc2hhcGVzL1ZveGVsU2hhcGU7RERERkZGRilWIiwKICAgICAgICAicmVuZGVyU25vd0FuZFJhaW4iOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL0xldmVsUmVuZGVyZXI7bV8xMDk3MDNfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9MaWdodFRleHR1cmU7RkRERClWIiwKICAgICAgICAic2V0dXBSZW5kZXIiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL0xldmVsUmVuZGVyZXI7bV8xOTQzMzhfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9DYW1lcmE7TG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL2N1bGxpbmcvRnJ1c3R1bTtaWilWIiwKICAgICAgICAicmVuZGVyQ2xvdWRzIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9MZXZlbFJlbmRlcmVyO21fMjUzMDU0XyhMY29tL21vamFuZy9ibGF6ZTNkL3ZlcnRleC9Qb3NlU3RhY2s7TG9yZy9qb21sL01hdHJpeDRmO0ZEREQpViIsCiAgICAgICAgInJlbmRlclNreSI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvTGV2ZWxSZW5kZXJlcjttXzIwMjQyM18oTGNvbS9tb2phbmcvYmxhemUzZC92ZXJ0ZXgvUG9zZVN0YWNrO0xvcmcvam9tbC9NYXRyaXg0ZjtGTG5ldC9taW5lY3JhZnQvY2xpZW50L0NhbWVyYTtaTGphdmEvbGFuZy9SdW5uYWJsZTspViIKICAgICAgfSwKICAgICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL0xpZmVMb2NrTWl4aW4iOiB7CiAgICAgICAgImhlYWwiOiAiTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L0xpdmluZ0VudGl0eTttXzU2MzRfKEYpViIsCiAgICAgICAgImdldEhlYWx0aCI6ICJMbmV0L21pbmVjcmFmdC93b3JsZC9lbnRpdHkvTGl2aW5nRW50aXR5O21fMjEyMjNfKClGIiwKICAgICAgICAic2V0SGVhbHRoIjogIkxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9MaXZpbmdFbnRpdHk7bV8yMTE1M18oRilWIgogICAgICB9LAogICAgICAibmV0L2RpZXh2L2RpZXh2c3dvcmQvbWl4aW4vRGlleHZBbnRpRGlzYXJtTWl4aW4iOiB7CiAgICAgICAgImRyb3AoTG5ldC9taW5lY3JhZnQvd29ybGQvaXRlbS9JdGVtU3RhY2s7WlopTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L2l0ZW0vSXRlbUVudGl0eTsiOiAiTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L3BsYXllci9QbGF5ZXI7bV83MTk3XyhMbmV0L21pbmVjcmFmdC93b3JsZC9pdGVtL0l0ZW1TdGFjaztaWilMbmV0L21pbmVjcmFmdC93b3JsZC9lbnRpdHkvaXRlbS9JdGVtRW50aXR5OyIKICAgICAgfSwKICAgICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL1N5bmNoZWREYXRhTWl4aW4iOiB7CiAgICAgICAgImdldCI6ICJMbmV0L21pbmVjcmFmdC9uZXR3b3JrL3N5bmNoZXIvU3luY2hlZEVudGl0eURhdGE7bV8xMzUzNzBfKExuZXQvbWluZWNyYWZ0L25ldHdvcmsvc3luY2hlci9FbnRpdHlEYXRhQWNjZXNzb3I7KUxqYXZhL2xhbmcvT2JqZWN0OyIKICAgICAgfSwKICAgICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL0NsaWVudERlYXRoVGltZU1peGluIjogewogICAgICAgICJnZXRXaGl0ZU92ZXJsYXlQcm9ncmVzcyI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvZW50aXR5L0xpdmluZ0VudGl0eVJlbmRlcmVyO21fNjkzMV8oTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L0xpdmluZ0VudGl0eTtGKUYiCiAgICAgIH0sCiAgICAgICJuZXQvZGlleHYvZGlleHZzd29yZC9taXhpbi9JdGVtUmVuZGVyZXJNaXhpbiI6IHsKICAgICAgICAicmVuZGVyIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9lbnRpdHkvSXRlbVJlbmRlcmVyO21fMTE1MTQzXyhMbmV0L21pbmVjcmFmdC93b3JsZC9pdGVtL0l0ZW1TdGFjaztMbmV0L21pbmVjcmFmdC93b3JsZC9pdGVtL0l0ZW1EaXNwbGF5Q29udGV4dDtaTGNvbS9tb2phbmcvYmxhemUzZC92ZXJ0ZXgvUG9zZVN0YWNrO0xuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9NdWx0aUJ1ZmZlclNvdXJjZTtJSUxuZXQvbWluZWNyYWZ0L2NsaWVudC9yZXNvdXJjZXMvbW9kZWwvQmFrZWRNb2RlbDspViIKICAgICAgfSwKICAgICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL0d1aUdyYXBoaWNzTWl4aW4iOiB7CiAgICAgICAgImZpbGxHcmFkaWVudChJSUlJSUlJKVYiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9HdWlHcmFwaGljczttXzI4MDEyMF8oSUlJSUlJSSlWIiwKICAgICAgICAiZHJhd0NlbnRlcmVkU3RyaW5nKExuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvRm9udDtMbmV0L21pbmVjcmFmdC9uZXR3b3JrL2NoYXQvQ29tcG9uZW50O0lJSSlWIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7bV8yODA2NTNfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvRm9udDtMbmV0L21pbmVjcmFmdC9uZXR3b3JrL2NoYXQvQ29tcG9uZW50O0lJSSlWIiwKICAgICAgICAiZHJhd1dvcmRXcmFwIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7bV8yODA1NTRfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvRm9udDtMbmV0L21pbmVjcmFmdC9uZXR3b3JrL2NoYXQvRm9ybWF0dGVkVGV4dDtJSUlJKVYiLAogICAgICAgICJyZW5kZXJGYWtlSXRlbSI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL0d1aUdyYXBoaWNzO21fMjgwMjAzXyhMbmV0L21pbmVjcmFmdC93b3JsZC9pdGVtL0l0ZW1TdGFjaztJSSlWIiwKICAgICAgICAiYmxpdChMbmV0L21pbmVjcmFmdC9yZXNvdXJjZXMvUmVzb3VyY2VMb2NhdGlvbjtJSUlJSUkpViI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL0d1aUdyYXBoaWNzO21fMjgwMjE4XyhMbmV0L21pbmVjcmFmdC9yZXNvdXJjZXMvUmVzb3VyY2VMb2NhdGlvbjtJSUlJSUkpViIsCiAgICAgICAgImRyYXdDZW50ZXJlZFN0cmluZyhMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL0ZvbnQ7TG5ldC9taW5lY3JhZnQvdXRpbC9Gb3JtYXR0ZWRDaGFyU2VxdWVuY2U7SUlJKVYiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9HdWlHcmFwaGljczttXzI4MDM2NF8oTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9Gb250O0xuZXQvbWluZWNyYWZ0L3V0aWwvRm9ybWF0dGVkQ2hhclNlcXVlbmNlO0lJSSlWIiwKICAgICAgICAiYmxpdChMbmV0L21pbmVjcmFmdC9yZXNvdXJjZXMvUmVzb3VyY2VMb2NhdGlvbjtJSUZGSUlJSSlWIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7bV8yODAxNjNfKExuZXQvbWluZWNyYWZ0L3Jlc291cmNlcy9SZXNvdXJjZUxvY2F0aW9uO0lJRkZJSUlJKVYiLAogICAgICAgICJibGl0KExuZXQvbWluZWNyYWZ0L3Jlc291cmNlcy9SZXNvdXJjZUxvY2F0aW9uO0lJSUlGRklJSUkpViI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL0d1aUdyYXBoaWNzO21fMjgwNDExXyhMbmV0L21pbmVjcmFmdC9yZXNvdXJjZXMvUmVzb3VyY2VMb2NhdGlvbjtJSUlJRkZJSUlJKVYiLAogICAgICAgICJkcmF3Q2VudGVyZWRTdHJpbmcoTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9Gb250O0xqYXZhL2xhbmcvU3RyaW5nO0lJSSlWIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7bV8yODAxMzdfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvRm9udDtMamF2YS9sYW5nL1N0cmluZztJSUkpViIsCiAgICAgICAgImZpbGxHcmFkaWVudChMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvUmVuZGVyVHlwZTtJSUlJSUlJKVYiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L2d1aS9HdWlHcmFwaGljczttXzI4NTk3OF8oTG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL1JlbmRlclR5cGU7SUlJSUlJSSlWIiwKICAgICAgICAiZmlsbEdyYWRpZW50KElJSUlJSSlWIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvR3VpR3JhcGhpY3M7bV8yODAwMjRfKElJSUlJSSlWIiwKICAgICAgICAiYmxpdChMbmV0L21pbmVjcmFmdC9yZXNvdXJjZXMvUmVzb3VyY2VMb2NhdGlvbjtJSUlGRklJSUkpViI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvZ3VpL0d1aUdyYXBoaWNzO21fMjgwMzk4XyhMbmV0L21pbmVjcmFmdC9yZXNvdXJjZXMvUmVzb3VyY2VMb2NhdGlvbjtJSUlGRklJSUkpViIKICAgICAgfSwKICAgICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL0VudGl0eVJlbmRlck1peGluIjogewogICAgICAgICJyZW5kZXJFbnRpdHkiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL0xldmVsUmVuZGVyZXI7bV8xMDk1MTdfKExuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9FbnRpdHk7RERERkxjb20vbW9qYW5nL2JsYXplM2QvdmVydGV4L1Bvc2VTdGFjaztMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvTXVsdGlCdWZmZXJTb3VyY2U7KVYiCiAgICAgIH0sCiAgICAgICJuZXQvZGlleHYvZGlleHZzd29yZC9taXhpbi9HYW1lUmVuZGVyZXJNaXhpbiI6IHsKICAgICAgICAic2h1dGRvd25TaGFkZXJzIjogIkxuZXQvbWluZWNyYWZ0L2NsaWVudC9yZW5kZXJlci9HYW1lUmVuZGVyZXI7bV8xNzI3NTlfKClWIiwKICAgICAgICAiZ2V0UmVuZGVyRGlzdGFuY2UiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL0dhbWVSZW5kZXJlcjttXzEwOTE1Ml8oKUYiLAogICAgICAgICJzaHV0ZG93bkVmZmVjdCI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvR2FtZVJlbmRlcmVyO21fMTA5MDg2XygpViIsCiAgICAgICAgInJlbmRlciI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvcmVuZGVyZXIvR2FtZVJlbmRlcmVyO21fMTA5MDkzXyhGSlopViIKICAgICAgfSwKICAgICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL0VudGl0eUFuaW1hdGlvbk1peGluIjogewogICAgICAgICJ0aWNrIjogIkxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9MaXZpbmdFbnRpdHk7bV84MTE5XygpViIsCiAgICAgICAgInRpY2tEZWF0aCI6ICJMbmV0L21pbmVjcmFmdC93b3JsZC9lbnRpdHkvTGl2aW5nRW50aXR5O21fNjE1M18oKVYiCiAgICAgIH0sCiAgICAgICJuZXQvZGlleHYvZGlleHZzd29yZC9taXhpbi9EaWV4dkludmVudG9yeVByb3RlY3Rpb25NaXhpbiI6IHsKICAgICAgICAic3dhcFBhaW50IjogIkxuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9wbGF5ZXIvSW52ZW50b3J5O21fMzU5ODhfKEQpViIsCiAgICAgICAgImRyb3BBbGwiOiAiTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L3BsYXllci9JbnZlbnRvcnk7bV8zNjA3MV8oKVYiLAogICAgICAgICJjbGVhckNvbnRlbnQiOiAiTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L3BsYXllci9JbnZlbnRvcnk7bV82MjExXygpViIsCiAgICAgICAgInNldEl0ZW0iOiAiTG5ldC9taW5lY3JhZnQvd29ybGQvZW50aXR5L3BsYXllci9JbnZlbnRvcnk7bV82ODM2XyhJTG5ldC9taW5lY3JhZnQvd29ybGQvaXRlbS9JdGVtU3RhY2s7KVYiLAogICAgICAgICJyZW1vdmVJdGVtKElJKUxuZXQvbWluZWNyYWZ0L3dvcmxkL2l0ZW0vSXRlbVN0YWNrOyI6ICJMbmV0L21pbmVjcmFmdC93b3JsZC9lbnRpdHkvcGxheWVyL0ludmVudG9yeTttXzc0MDdfKElJKUxuZXQvbWluZWNyYWZ0L3dvcmxkL2l0ZW0vSXRlbVN0YWNrOyIKICAgICAgfSwKICAgICAgIm5ldC9kaWV4di9kaWV4dnN3b3JkL21peGluL1dpbmRvd01peGluIjogewogICAgICAgICJyZW5kZXIiOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L3JlbmRlcmVyL0dhbWVSZW5kZXJlcjttXzEwOTA5M18oRkpaKVYiCiAgICAgIH0sCiAgICAgICJuZXQvZGlleHYvZGlleHZzd29yZC9taXhpbi9KU0NNaXhpbiI6IHsKICAgICAgICAiYWRkRW50aXR5IjogIkxuZXQvbWluZWNyYWZ0L3NlcnZlci9sZXZlbC9TZXJ2ZXJMZXZlbDttXzg4NzJfKExuZXQvbWluZWNyYWZ0L3dvcmxkL2VudGl0eS9FbnRpdHk7KVoiCiAgICAgIH0sCiAgICAgICJuZXQvZGlleHYvZGlleHZzd29yZC9taXhpbi9XaW5kb3dUaXRsZURpY3RhdG9yTWl4aW4iOiB7CiAgICAgICAgInNldFRpdGxlIjogIkxjb20vbW9qYW5nL2JsYXplM2QvcGxhdGZvcm0vV2luZG93O21fODU0MjJfKExqYXZhL2xhbmcvU3RyaW5nOylWIgogICAgICB9LAogICAgICAibmV0L2RpZXh2L2RpZXh2c3dvcmQvbWl4aW4vU2V0U2NyZWVuTWl4aW4iOiB7CiAgICAgICAgInRpY2siOiAiTG5ldC9taW5lY3JhZnQvY2xpZW50L01pbmVjcmFmdDttXzkxMzk4XygpViIsCiAgICAgICAgInNldFNjcmVlbiI6ICJMbmV0L21pbmVjcmFmdC9jbGllbnQvTWluZWNyYWZ0O21fOTExNTJfKExuZXQvbWluZWNyYWZ0L2NsaWVudC9ndWkvc2NyZWVucy9TY3JlZW47KVYiCiAgICAgIH0sCiAgICAgICJuZXQvZGlleHYvZGlleHZzd29yZC9taXhpbi9MZXZlbFRpY2tNaXhpbiI6IHsKICAgICAgICAidGljayI6ICJMbmV0L21pbmVjcmFmdC9zZXJ2ZXIvbGV2ZWwvU2VydmVyTGV2ZWw7bV84NzkzXyhMamF2YS91dGlsL2Z1bmN0aW9uL0Jvb2xlYW5TdXBwbGllcjspViIKICAgICAgfQogICAgfQogIH0KfTwvcHJlPjwvYm9keT48L2h0bWw+":{"last_modified":"13410191221017173","zoom_level":0.5227586988632231},"file:///C:/Users/Administrator/MCreatorWorkspaces/AxisInnovatorsBox/javascript/LinuxTerminal.html":{"last_modified":"13410163296735238","zoom_level":1.2239010857415449}}}},"password_manager":{"autofillable_credentials_account_store_login_database":false,"autofillable_credentials_profile_store_login_database":false},"privacy_sandbox":{"first_party_sets_data_access_allowed_initialized":true},"profile":{"avatar_index":26,"content_settings":{"did_migrate_adaptive_notification_quieting_to_cpss":true,"disable_quiet_permission_ui_time":{"notifications":"13390881861687508"},"enable_cpss":{"notifications":true},"enable_quiet_permission_ui":{"notifications":false},"enable_quiet_permission_ui_enabling_method":{"notifications":1},"exceptions":{"3pcd_heuristics_grants":{},"3pcd_support":{},"abusive_notification_permissions":{},"access_to_get_all_screens_media_in_session":{},"anti_abuse":{},"app_banner":{},"ar":{},"auto_picture_in_picture":{},"auto_select_certificate":{},"automatic_downloads":{},"automatic_fullscreen":{},"autoplay":{},"background_sync":{},"bluetooth_chooser_data":{},"bluetooth_guard":{},"bluetooth_scanning":{},"camera_pan_tilt_zoom":{},"captured_surface_control":{},"client_hints":{"https://cn.bing.com:443,*":{"last_modified":"13399890271326482","setting":{"client_hints":[6,8,9,10,11,12,13,14,16,23]}}},"clipboard":{},"cookie_controls_metadata":{},"cookies":{},"direct_sockets":{},"direct_sockets_private_network_access":{},"display_media_system_audio":{},"durable_storage":{},"fedcm_idp_registration":{},"fedcm_idp_signin":{"https://accounts.google.com:443,*":{"last_modified":"13391185831813708","setting":{"chosen-objects":[{"idp-origin":"https://accounts.google.com","idp-signin-status":false}]}}},"fedcm_share":{},"file_system_access_chooser_data":{},"file_system_access_extended_permission":{},"file_system_access_restore_permission":{},"file_system_last_picked_directory":{},"file_system_read_guard":{},"file_system_write_guard":{},"formfill_metadata":{},"geolocation":{},"hand_tracking":{},"hid_chooser_data":{},"hid_guard":{},"http_allowed":{},"https_enforced":{},"idle_detection":{},"images":{},"important_site_info":{},"insecure_private_network":{},"intent_picker_auto_display":{},"javascript":{},"javascript_jit":{},"javascript_optimizer":{},"keyboard_lock":{},"legacy_cookie_access":{},"local_fonts":{},"media_engagement":{},"media_stream_camera":{},"media_stream_mic":{},"midi_sysex":{},"mixed_script":{},"nfc_devices":{},"notification_interactions":{},"notification_permission_review":{},"notifications":{},"password_protection":{},"payment_handler":{},"permission_autoblocking_data":{},"permission_autorevocation_data":{},"pointer_lock":{},"popups":{},"private_network_chooser_data":{},"private_network_guard":{},"protected_media_identifier":{},"protocol_handler":{},"reduced_accept_language":{},"safe_browsing_url_check_data":{},"sensors":{},"serial_chooser_data":{},"serial_guard":{},"site_engagement":{"https://casdoor.lingqi.vip:443,*":{"last_modified":"13411376394279362","setting":{"lastEngagementTime":1.3411347594279308e+16,"lastShortcutLaunchTime":0.0,"pointsAddedToday":0.0,"rawScore":66.81816182171741}}},"sound":{},"speaker_selection":{},"ssl_cert_decisions":{},"storage_access":{},"storage_access_header_origin_trial":{},"subresource_filter":{},"subresource_filter_data":{},"third_party_storage_partitioning":{},"top_level_3pcd_origin_trial":{},"top_level_3pcd_support":{},"top_level_storage_access":{},"tracking_protection":{},"unused_site_permissions":{},"usb_chooser_data":{},"usb_guard":{},"vr":{},"web_app_installation":{},"webid_api":{},"webid_auto_reauthn":{},"window_placement":{}},"pref_version":1},"created_by_version":"132.0.6834.83","creation_time":"13390881861649643","did_work_around_bug_364820109_default":true,"did_work_around_bug_364820109_exceptions":true,"exit_type":"Crashed","family_link_user_state":6,"family_member_role":"not_in_family","last_engagement_time":"13411378344262056","last_time_obsolete_http_credentials_removed":1763271873.862677,"last_time_password_store_metrics_reported":1766902819.794058,"managed":{"locally_parent_approved_extensions":{},"locally_parent_approved_extensions_migration_state":1},"managed_user_id":"","name":"Person 1","password_account_storage_settings":{},"password_hash_data_list":[],"were_old_google_logins_removed":true},"safebrowsing":{"event_timestamps":{"0":{"10":[],"12":["13410165210","13410169679","13410187258","13410187264","13410187273","13410188720","13410188726","13410188925","13410190237","13410191005","13410191015","13410191306","13410191318"]}},"metrics_last_log_time":"13411376389","saw_interstitial_sber2":true,"scout_reporting_enabled_when_deprecated":false,"unhandled_sync_password_reuses":{}},"safety_hub":{"unused_site_permissions_revocation":{"migration_completed":true}},"saved_tab_groups":{"specifics_to_data_migration":true},"segmentation_platform":{"client_result_prefs":"CmAKEXJlc3VtZV9oZWF2eV91c2VyEksKQA0AAAAAELD+3q+2sukXGi4KJgokDQAAAD8SFlJlc3VtZUhlYXZ5VXNlclNlZ21lbnQaBU90aGVyEgQQDhgEIAIQ3/7er7ay6RcKUgoNc2hvcHBpbmdfdXNlchJBCjYNAAAAABCI/N6vtrLpFxokChwKGg0AAAA/EgxTaG9wcGluZ1VzZXIaBU90aGVyEgQQAhgEIAMQtvzer7ay6RcKcwoVcGFzc3dvcmRfbWFuYWdlcl91c2VyEloKTw0AAAAAEO+A36+2sukXGj0KNQozDQAAAD8SE1Bhc3N3b3JkTWFuYWdlclVzZXIaF05vdF9QYXNzd29yZE1hbmFnZXJVc2VyEgQQBxgEIAEQqoHfr7ay6RcK5QIKEWNyb3NzX2RldmljZV91c2VyEs8CCsMCDQAAgD8Qnv3er7ay6RcasAIKpwIapAIKGQ0AAIA/EhJOb0Nyb3NzRGV2aWNlVXNhZ2UKGA0AAABAEhFDcm9zc0RldmljZU1vYmlsZQoZDQAAQEASEkNyb3NzRGV2aWNlRGVza3RvcAoYDQAAgEASEUNyb3NzRGV2aWNlVGFibGV0CiINAACgQBIbQ3Jvc3NEZXZpY2VNb2JpbGVBbmREZXNrdG9wCiENAADAQBIaQ3Jvc3NEZXZpY2VNb2JpbGVBbmRUYWJsZXQKIg0AAOBAEhtDcm9zc0RldmljZURlc2t0b3BBbmRUYWJsZXQKIA0AAABBEhlDcm9zc0RldmljZUFsbERldmljZVR5cGVzChcNAAAQQRIQQ3Jvc3NEZXZpY2VPdGhlchISTm9Dcm9zc0RldmljZVVzYWdlEgQQBxgEIAIQ2f3er7ay6RcKagoaY2hyb21lX2xvd191c2VyX2VuZ2FnZW1lbnQSTApBDQAAgD8Ql/rer7ay6RcaLwonCiUNAAAAPxIXQ2hyb21lTG93VXNlckVuZ2FnZW1lbnQaBU90aGVyEgQQBxgEIAIQ2/rer7ay6RcKZAoLc2VhcmNoX3VzZXISVQpKDQAAAAAQ34Hfr7ay6RcaOAowGi4KCg0AAIA/EgNMb3cKDQ0AAKBAEgZNZWRpdW0KCw0AALBBEgRIaWdoEgROb25lEgQQBxgEIAIQloLfr7ay6Rc=","device_switcher_util":{"result":{"labels":["NotSynced"]}},"last_db_compaction_time":"13411267199000000","uma_in_sql_start_time":"13390881861679139"},"sessions":{"event_log":[{"crashed":true,"time":"13410189771620613","type":0},{"crashed":true,"time":"13410189853243223","type":0},{"crashed":true,"time":"13410189940214903","type":0},{"crashed":true,"time":"13410190111239653","type":0},{"crashed":true,"time":"13410190173882418","type":0},{"crashed":true,"time":"13410190489381975","type":0},{"crashed":true,"time":"13410190566806394","type":0},{"crashed":true,"time":"13410190643125862","type":0},{"crashed":true,"time":"13410190847585404","type":0},{"crashed":true,"time":"13410191675223638","type":0},{"crashed":true,"time":"13410239923591354","type":0},{"crashed":true,"time":"13410596638442743","type":0},{"crashed":true,"time":"13410599267506564","type":0},{"crashed":true,"time":"13410600021520342","type":0},{"crashed":true,"time":"13410616600659739","type":0},{"crashed":true,"time":"13410697465344414","type":0},{"crashed":true,"time":"13410700077504629","type":0},{"crashed":true,"time":"13411376389792535","type":0},{"crashed":true,"time":"13411378254257884","type":0}],"session_data_status":1},"should_read_incoming_syncing_theme_prefs":true,"signin":{"allowed":false},"spellcheck":{"dictionaries":["en-US"],"dictionary":""},"sync":{"data_type_status_for_sync_to_signin":{"app_list":false,"app_settings":false,"apps":false,"arc_package":false,"autofill":false,"autofill_profiles":false,"autofill_wallet":false,"autofill_wallet_credential":false,"autofill_wallet_metadata":false,"autofill_wallet_offer":false,"autofill_wallet_usage":false,"bookmarks":false,"collaboration_group":false,"contact_info":false,"cookies":false,"device_info":false,"dictionary":false,"extension_settings":false,"extensions":false,"history":false,"history_delete_directives":false,"incoming_password_sharing_invitation":false,"managed_user_settings":false,"nigori":false,"os_preferences":false,"os_priority_preferences":false,"outgoing_password_sharing_invitation":false,"passwords":false,"plus_address":false,"plus_address_setting":false,"power_bookmark":false,"preferences":false,"printers":false,"printers_authorization_servers":false,"priority_preferences":false,"product_comparison":false,"reading_list":false,"saved_tab_group":false,"search_engines":false,"security_events":false,"send_tab_to_self":false,"sessions":false,"shared_tab_group_data":false,"sharing_message":false,"themes":false,"user_consent":false,"user_events":false,"web_apps":false,"webapks":false,"webauthn_credential":false,"wifi_configurations":false,"workspace_desk":false},"encryption_bootstrap_token_per_account_migration_done":true,"feature_status_for_sync_to_signin":5},"tab_group_saves_ui_update_migrated":true,"total_passwords_available_for_account":0,"total_passwords_available_for_profile":0,"translate_site_blacklist":[],"translate_site_blocklist_with_time":{},"updateclientdata":{"apps":{"ncennffkjdiamlpmcbajkmaiiiddgioo":{"cohort":"1::","cohortname":"","dlrc":6935,"fp":"1.fc32d125fc37b80eefad0fd38661479f97fdf322709f352ad1350dd8a14f3dcf","installdate":6698,"max_pv":"3.52.13","pf":"850d6ec0-7798-4863-845d-718352fd6d50","pv":"3.52.14"}}},"updateclientlastupdatecheckerror":-356,"updateclientlastupdatecheckerrorcategory":5,"updateclientlastupdatecheckerrorextracode1":0,"web_apps":{"did_migrate_default_chrome_apps":["MigrateDefaultChromeAppToWebAppsGSuite","MigrateDefaultChromeAppToWebAppsNonGSuite"],"last_preinstall_synchronize_version":"132"}} \ No newline at end of file diff --git a/library/jcef/cache/Default/Session Storage/000012.log b/library/jcef/cache/Default/Session Storage/000012.log index 8ce2329..2019801 100644 Binary files a/library/jcef/cache/Default/Session Storage/000012.log and b/library/jcef/cache/Default/Session Storage/000012.log differ diff --git a/library/jcef/cache/Default/Session Storage/LOG b/library/jcef/cache/Default/Session Storage/LOG index 031ce1c..6bc246d 100644 --- a/library/jcef/cache/Default/Session Storage/LOG +++ b/library/jcef/cache/Default/Session Storage/LOG @@ -1,3 +1,3 @@ -2025/11/16-15:36:31.538 14a4 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Session Storage/MANIFEST-000001 -2025/11/16-15:36:31.539 14a4 Recovering log #12 -2025/11/16-15:36:31.542 14a4 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Session Storage/000012.log +2025/12/28-14:51:01.393 2160 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Session Storage/MANIFEST-000001 +2025/12/28-14:51:01.395 2160 Recovering log #12 +2025/12/28-14:51:01.401 2160 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Session Storage/000012.log diff --git a/library/jcef/cache/Default/Session Storage/LOG.old b/library/jcef/cache/Default/Session Storage/LOG.old index 007bb75..ca2c99e 100644 --- a/library/jcef/cache/Default/Session Storage/LOG.old +++ b/library/jcef/cache/Default/Session Storage/LOG.old @@ -1,3 +1,3 @@ -2025/11/16-14:15:05.342 200 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Session Storage/MANIFEST-000001 -2025/11/16-14:15:05.344 200 Recovering log #12 -2025/11/16-14:15:05.347 200 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Session Storage/000012.log +2025/12/20-18:28:04.218 294 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Session Storage/MANIFEST-000001 +2025/12/20-18:28:04.219 294 Recovering log #12 +2025/12/20-18:28:04.222 294 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Session Storage/000012.log diff --git a/library/jcef/cache/Default/Site Characteristics Database/000003.log b/library/jcef/cache/Default/Site Characteristics Database/000003.log index 9fba8e9..2dede0c 100644 Binary files a/library/jcef/cache/Default/Site Characteristics Database/000003.log and b/library/jcef/cache/Default/Site Characteristics Database/000003.log differ diff --git a/library/jcef/cache/Default/Site Characteristics Database/LOG b/library/jcef/cache/Default/Site Characteristics Database/LOG index 6cbf646..91d4a63 100644 --- a/library/jcef/cache/Default/Site Characteristics Database/LOG +++ b/library/jcef/cache/Default/Site Characteristics Database/LOG @@ -1,3 +1,3 @@ -2025/11/16-15:36:13.152 5138 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Site Characteristics Database/MANIFEST-000001 -2025/11/16-15:36:13.154 5138 Recovering log #3 -2025/11/16-15:36:13.168 5138 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Site Characteristics Database/000003.log +2025/12/28-14:50:54.257 186c Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Site Characteristics Database/MANIFEST-000001 +2025/12/28-14:50:54.263 186c Recovering log #3 +2025/12/28-14:50:54.264 186c Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Site Characteristics Database/000003.log diff --git a/library/jcef/cache/Default/Site Characteristics Database/LOG.old b/library/jcef/cache/Default/Site Characteristics Database/LOG.old index da7b4a0..9dc6781 100644 --- a/library/jcef/cache/Default/Site Characteristics Database/LOG.old +++ b/library/jcef/cache/Default/Site Characteristics Database/LOG.old @@ -1,3 +1,3 @@ -2025/11/16-14:14:54.588 27c Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Site Characteristics Database/MANIFEST-000001 -2025/11/16-14:14:54.588 27c Recovering log #3 -2025/11/16-14:14:54.590 27c Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Site Characteristics Database/000003.log +2025/12/28-14:19:49.782 193c Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Site Characteristics Database/MANIFEST-000001 +2025/12/28-14:19:49.813 193c Recovering log #3 +2025/12/28-14:19:49.814 193c Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Site Characteristics Database/000003.log diff --git a/library/jcef/cache/Default/Sync Data/LevelDB/LOG b/library/jcef/cache/Default/Sync Data/LevelDB/LOG index 381d616..fdefc35 100644 --- a/library/jcef/cache/Default/Sync Data/LevelDB/LOG +++ b/library/jcef/cache/Default/Sync Data/LevelDB/LOG @@ -1,3 +1,3 @@ -2025/11/16-15:36:13.093 4714 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Sync Data\LevelDB/MANIFEST-000001 -2025/11/16-15:36:13.104 4714 Recovering log #3 -2025/11/16-15:36:13.105 4714 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Sync Data\LevelDB/000003.log +2025/12/28-14:50:54.253 37d4 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Sync Data\LevelDB/MANIFEST-000001 +2025/12/28-14:50:54.263 37d4 Recovering log #3 +2025/12/28-14:50:54.264 37d4 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Sync Data\LevelDB/000003.log diff --git a/library/jcef/cache/Default/Sync Data/LevelDB/LOG.old b/library/jcef/cache/Default/Sync Data/LevelDB/LOG.old index 20ad34a..ca06660 100644 --- a/library/jcef/cache/Default/Sync Data/LevelDB/LOG.old +++ b/library/jcef/cache/Default/Sync Data/LevelDB/LOG.old @@ -1,3 +1,3 @@ -2025/11/16-14:14:54.568 ec0 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Sync Data\LevelDB/MANIFEST-000001 -2025/11/16-14:14:54.578 ec0 Recovering log #3 -2025/11/16-14:14:54.578 ec0 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Sync Data\LevelDB/000003.log +2025/12/28-14:19:49.766 bcc Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Sync Data\LevelDB/MANIFEST-000001 +2025/12/28-14:19:49.813 bcc Recovering log #3 +2025/12/28-14:19:49.814 bcc Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\Sync Data\LevelDB/000003.log diff --git a/library/jcef/cache/Default/Top Sites b/library/jcef/cache/Default/Top Sites index 38d7860..5427a0c 100644 Binary files a/library/jcef/cache/Default/Top Sites and b/library/jcef/cache/Default/Top Sites differ diff --git a/library/jcef/cache/Default/Visited Links b/library/jcef/cache/Default/Visited Links index 73ae263..c22d1e2 100644 Binary files a/library/jcef/cache/Default/Visited Links and b/library/jcef/cache/Default/Visited Links differ diff --git a/library/jcef/cache/Default/shared_proto_db/000008.ldb b/library/jcef/cache/Default/shared_proto_db/000008.ldb deleted file mode 100644 index 3297d66..0000000 Binary files a/library/jcef/cache/Default/shared_proto_db/000008.ldb and /dev/null differ diff --git a/library/jcef/cache/Default/shared_proto_db/000011.ldb b/library/jcef/cache/Default/shared_proto_db/000011.ldb deleted file mode 100644 index 8ff9815..0000000 Binary files a/library/jcef/cache/Default/shared_proto_db/000011.ldb and /dev/null differ diff --git a/library/jcef/cache/Default/shared_proto_db/000013.log b/library/jcef/cache/Default/shared_proto_db/000013.log deleted file mode 100644 index b4bd393..0000000 Binary files a/library/jcef/cache/Default/shared_proto_db/000013.log and /dev/null differ diff --git a/library/jcef/cache/Default/shared_proto_db/000014.ldb b/library/jcef/cache/Default/shared_proto_db/000014.ldb deleted file mode 100644 index 825452e..0000000 Binary files a/library/jcef/cache/Default/shared_proto_db/000014.ldb and /dev/null differ diff --git a/library/jcef/cache/Default/shared_proto_db/LOG b/library/jcef/cache/Default/shared_proto_db/LOG index a7d2e8c..8290831 100644 --- a/library/jcef/cache/Default/shared_proto_db/LOG +++ b/library/jcef/cache/Default/shared_proto_db/LOG @@ -1,3 +1,3 @@ -2025/11/16-15:36:13.167 9a8 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db/MANIFEST-000001 -2025/11/16-15:36:13.168 9a8 Recovering log #13 -2025/11/16-15:36:13.169 9a8 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db/000013.log +2025/12/28-14:50:54.299 22b4 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db/MANIFEST-000001 +2025/12/28-14:50:54.299 22b4 Recovering log #19 +2025/12/28-14:50:54.301 22b4 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db/000019.log diff --git a/library/jcef/cache/Default/shared_proto_db/LOG.old b/library/jcef/cache/Default/shared_proto_db/LOG.old index 416e24f..ff102f8 100644 --- a/library/jcef/cache/Default/shared_proto_db/LOG.old +++ b/library/jcef/cache/Default/shared_proto_db/LOG.old @@ -1,3 +1,3 @@ -2025/11/16-14:14:54.657 27c Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db/MANIFEST-000001 -2025/11/16-14:14:54.658 27c Recovering log #13 -2025/11/16-14:14:54.658 27c Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db/000013.log +2025/12/28-14:19:49.860 6ac Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db/MANIFEST-000001 +2025/12/28-14:19:49.861 6ac Recovering log #19 +2025/12/28-14:19:49.866 6ac Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db/000019.log diff --git a/library/jcef/cache/Default/shared_proto_db/MANIFEST-000001 b/library/jcef/cache/Default/shared_proto_db/MANIFEST-000001 index 7a2141a..23e2789 100644 Binary files a/library/jcef/cache/Default/shared_proto_db/MANIFEST-000001 and b/library/jcef/cache/Default/shared_proto_db/MANIFEST-000001 differ diff --git a/library/jcef/cache/Default/shared_proto_db/metadata/000003.log b/library/jcef/cache/Default/shared_proto_db/metadata/000003.log index c71b04d..ea7ec04 100644 Binary files a/library/jcef/cache/Default/shared_proto_db/metadata/000003.log and b/library/jcef/cache/Default/shared_proto_db/metadata/000003.log differ diff --git a/library/jcef/cache/Default/shared_proto_db/metadata/LOG b/library/jcef/cache/Default/shared_proto_db/metadata/LOG index 20456e0..80d08c0 100644 --- a/library/jcef/cache/Default/shared_proto_db/metadata/LOG +++ b/library/jcef/cache/Default/shared_proto_db/metadata/LOG @@ -1,3 +1,3 @@ -2025/11/16-15:36:13.140 9a8 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db\metadata/MANIFEST-000001 -2025/11/16-15:36:13.141 9a8 Recovering log #3 -2025/11/16-15:36:13.143 9a8 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db\metadata/000003.log +2025/12/28-14:50:54.290 22b4 Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db\metadata/MANIFEST-000001 +2025/12/28-14:50:54.291 22b4 Recovering log #3 +2025/12/28-14:50:54.291 22b4 Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db\metadata/000003.log diff --git a/library/jcef/cache/Default/shared_proto_db/metadata/LOG.old b/library/jcef/cache/Default/shared_proto_db/metadata/LOG.old index 4ff2865..53965fc 100644 --- a/library/jcef/cache/Default/shared_proto_db/metadata/LOG.old +++ b/library/jcef/cache/Default/shared_proto_db/metadata/LOG.old @@ -1,3 +1,3 @@ -2025/11/16-14:14:54.614 27c Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db\metadata/MANIFEST-000001 -2025/11/16-14:14:54.615 27c Recovering log #3 -2025/11/16-14:14:54.619 27c Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db\metadata/000003.log +2025/12/28-14:19:49.851 6ac Reusing MANIFEST C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db\metadata/MANIFEST-000001 +2025/12/28-14:19:49.852 6ac Recovering log #3 +2025/12/28-14:19:49.854 6ac Reusing old log C:\Users\Administrator\MCreatorWorkspaces\AxisInnovatorsBox\library\jcef\cache\Default\shared_proto_db\metadata/000003.log diff --git a/library/jcef/cache/GrShaderCache/data_0 b/library/jcef/cache/GrShaderCache/data_0 index 43acca7..d2d7e0c 100644 Binary files a/library/jcef/cache/GrShaderCache/data_0 and b/library/jcef/cache/GrShaderCache/data_0 differ diff --git a/library/jcef/cache/GrShaderCache/data_1 b/library/jcef/cache/GrShaderCache/data_1 index 0e893c7..cd64f07 100644 Binary files a/library/jcef/cache/GrShaderCache/data_1 and b/library/jcef/cache/GrShaderCache/data_1 differ diff --git a/library/jcef/cache/GrShaderCache/data_3 b/library/jcef/cache/GrShaderCache/data_3 index a428696..5f1c6bc 100644 Binary files a/library/jcef/cache/GrShaderCache/data_3 and b/library/jcef/cache/GrShaderCache/data_3 differ diff --git a/library/jcef/cache/GraphiteDawnCache/data_1 b/library/jcef/cache/GraphiteDawnCache/data_1 index 508393d..d57206e 100644 Binary files a/library/jcef/cache/GraphiteDawnCache/data_1 and b/library/jcef/cache/GraphiteDawnCache/data_1 differ diff --git a/library/jcef/cache/Last Browser b/library/jcef/cache/Last Browser index b4508ea..a7254e9 100644 Binary files a/library/jcef/cache/Last Browser and b/library/jcef/cache/Last Browser differ diff --git a/library/jcef/cache/Local State b/library/jcef/cache/Local State index 6689729..e7536a2 100644 --- a/library/jcef/cache/Local State +++ b/library/jcef/cache/Local State @@ -1 +1 @@ -{"accessibility":{"captions":{"soda_registered_language_packs":["en-US"]},"screen_ai":{"last_used_time":"13407752173964870"}},"autofill":{"ablation_seed":"TALAAWR3U5o=","states_data_dir":"C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\library\\jcef\\cache\\AutofillStates\\2025.6.13.84507"},"background_tracing":{"session_state":{"privacy_filter":false,"state":0}},"breadcrumbs":{"enabled":false,"enabled_time":"13407745281692940"},"browser":{"shortcut_migration_version":"132.0.6834.83"},"chrome_labs_activation_threshold":44,"hardware_acceleration_mode_previous":true,"legacy":{"profile":{"name":{"migrated":true}}},"local":{"password_hash_data_list":[]},"management":{"platform":{"azure_active_directory":0,"enterprise_mdm_win":0}},"optimization_guide":{"model_store_metadata":{},"on_device":{"last_version":"132.0.6834.83","model_crash_count":0,"performance_class":7}},"origin_trials":{"disabled_features":["CanvasTextNg"]},"os_crypt":{"audit_enabled":true,"encrypted_key":"RFBBUEkBAAAA0Iyd3wEV0RGMegDAT8KX6wEAAADBwqsP1QzrT6MWcdZVJfu5EAAAABIAAABDAGgAcgBvAG0AaQB1AG0AAAAQZgAAAAEAACAAAACB1ryRyzwij1C09/fW7Nq6xYWNYBouyiCtBQVy/dm3CQAAAAAOgAAAAAIAACAAAADbFABSUOggpWGtdyt8gjY1U0KOsQGeTko2gmAW90ZayTAAAADFq9PcgOxeNssHaxgGBueXV4hvHQJJpmfEMZ+q1Evd16SVsDQI/5ryEswRmAd6zwNAAAAAOuqKXbFa23SsXpgg7Pl2JS8o587xUaAWaRKpBdtanJxeqCeHs86s5Xb7UUFCatlPZCByX+JyuenI90CW2C8Ncw=="},"policy":{"last_statistics_update":"13407745281686359"},"privacy_budget":{"meta_experiment_activation_salt":0.8784738491452397},"profile":{"info_cache":{"Default":{"active_time":1755515613.9453,"avatar_icon":"chrome://theme/IDR_PROFILE_AVATAR_26","background_apps":false,"force_signin_profile_locked":false,"gaia_given_name":"","gaia_id":"","gaia_name":"","hosted_domain":"","is_consented_primary_account":false,"is_ephemeral":false,"is_using_default_avatar":true,"is_using_default_name":true,"managed_user_id":"","metrics_bucket_index":1,"name":"Person 1","signin.with_credential_provider":false,"user_name":""}},"last_active_profiles":[],"metrics":{"next_bucket_index":2},"profile_counts_reported":"13407745281696569","profiles_order":["Default"],"show_picker_on_startup":false},"profile_network_context_service":{"http_cache_finch_experiment_groups":"None None None None"},"session_id_generator_last_value":"1067264888","signin":{"active_accounts_last_emitted":"13407745281488254"},"subresource_filter":{"ruleset_version":{"checksum":805734063,"content":"9.62.0","format":36}},"tab_stats":{"discards_external":0,"discards_frozen":0,"discards_proactive":0,"discards_suggested":0,"discards_urgent":0,"last_daily_sample":"13407745281631241","max_tabs_per_window":1,"reloads_external":0,"reloads_frozen":0,"reloads_proactive":0,"reloads_suggested":0,"reloads_urgent":0,"total_tab_count_max":1,"window_count_max":1},"tpcd":{"metadata":{"cohorts":{"+85uj8UpFJFs1LbZzRODD1aQ+Vs=":2,"+EvRah+wIaVJthrhxHGvfjZWQqY=":2,"+Goy06x/MCwrTV/aHU6CfXEkvHs=":2,"+OlMW5y2ANwBFsH03kShVXYVYM4=":2,"+WavPWcVf6qGVorrutx5lkDvL8g=":2,"+exM1B26jXxhR2Ux05ie/WWp0x0=":2,"+mNvpfM3JkKTeK+6ohl+LXstAC8=":2,"+qkOunaBVbv6XoaIwvMn3m6HluM=":2,"+tsbvVZgVIUs6CaBR9z7zuZH70o=":2,"+xOc8Z8Bc8iWT3jhs1SRha/IbDM=":2,"/AzXcP7UuUNwY1auU8IKM+kO+4A=":2,"/Bvm/Rrh3ZMwqH+/+4QOIcUQPPk=":2,"/FeZnUMHLubMD1MVDBjadEPAlVo=":2,"/HAmLxXpHT88v9y7xE9hHTkgIvM=":2,"/Qdp4MAUrNtjqZV9mfb20WAMugw=":2,"/QxFmTaKVmgoQI7u32pqfP+71Bc=":2,"/aGCDNLRew1LLZH+59lHCuAYNdk=":2,"/hbF3j5JOvAak7vNsAbK79bOP34=":2,"/llw2C0PMltsatGnpTHqrkbluYM=":2,"/oHhyW5YAQhdBpgYcbm1vJyiP+Q=":2,"/rtQf7RE4vMc92KjutC8LkjUZgQ=":2,"/xVDyosmqM6bfIMKyDuRAUI1Qyo=":2,"0Eq8eriICMngC2bt8vmV6V5tJCc=":2,"0OWhcqeF92w5b13FI7cuw0wYOiM=":2,"0b9gyhS7XLqmkmOe5OQuD409YLc=":2,"0gnBMXopl7lReGu+XSk/UzZakLk=":2,"0o2/D9RwYjAcr69AeW+JJDm3uHQ=":2,"0y3W2Bn1Kfxh/5CIvP2vZz5fwTE=":2,"164zkQ1BuqOVnZxlkMScGNXYC8Y=":2,"190T6zIzSL3PTH7lqquaA8KAqVg=":2,"1KHE7JEW3MsvkGFH8et2wPovDqk=":2,"1MfDQw3vHCQqaDa7CJ05y8CkuJs=":2,"1XfEZ7+2f5d1GWbkN5KoDjaQBL8=":2,"1v+25/b3OROEvLwpc+58pTQHvUQ=":2,"24O8PXrKNoFD7n9OiIn/kPM8q1E=":2,"26KadWj8dsla9qY4pbLWwc3WCQY=":2,"2EnXui3e9m8cVkso31bGopul3g0=":2,"2PGjwY0nYdnDLk59gOOl/h012PY=":2,"2W5F+NooL0WF8r0ykkHsqw75NVM=":2,"2ks61ETyiwJVBKjWdeLi+jBzvHw=":2,"2ogzRk2u3FpQekT714FkK0vkFy8=":2,"3IgtLbRDI7pm77T7imadPrRxR5E=":2,"3LIGdlgJ5Rw05ZwZulvKp0JajOw=":2,"3UWlaBDJKI5jNgpXTezvx7uZ7L8=":2,"3dFQSDfXS18fA563uc5mxhMXjLk=":2,"3iGyG3EVKY4vxn8RarUIggZVVkY=":2,"3pCe5f7oscOC94pJnWvhlszLhr0=":2,"3qs39ewgIOr/6zygzv+0ltu0SjE=":2,"3vvP54USUtSZlm/osFRbdXjCN/U=":2,"4+yW/l/2EqzM0fGVj0BcXV5TqDI=":2,"44pYIrJiSE08LkH770bq0wwoNEA=":2,"4TwK5fjrPUvueMXp4HZKGWvCJ0M=":2,"4ZC+GtEmLmHToJys2q2fbofqXeE=":2,"4eXKnzRS6ifFncgprkihsauqdGE=":2,"4qJ4K07ijGN6GmvmTNYzTJYmtRw=":2,"4rqrzEd2r2LoSpwBy3Wd1v4Drtc=":2,"4tQsYPAXpmciDFIC4nUACa9X4Wo=":2,"4vXUVM3Beyj2Rn+4lrrMuX455kY=":2,"55tALpi6iGTNuAMJkIQHrV/h8Zw=":2,"5Mo2uOqOAgDMAFR9xJxpj+Ps9vw=":2,"5Roy+ahYhL8V+JUUKT4pljRssYk=":2,"5UJk8zfVHpYxYI3tEGAQfMrks6k=":2,"6/9fhC6bkvQ+GDnCZzKV5b1uEs4=":2,"6218Tv51tHCkvM+pj15cmmrEp44=":2,"6AuvpHbKzPKvnQ5Iq4ZnJrH7VJc=":2,"6DjDzfLMbfYOmlGOJVAk9WK0yxs=":2,"6Edcp8jwSlxhClh0ZABA7VHryeY=":2,"6HE+xAlLmyPiZz4+IOE6QJR2+qU=":2,"6MJKJThHrZ77JxHeDkpqjqsJmPc=":2,"6S99zH9nPCcQM3Z3Z1tFjU0qI/8=":2,"6WtNqNcKg7znf4yFj/CmTJII+Ik=":2,"6iyZ50rVcsj89+phE3AIAQJ5Rl8=":2,"6sUp2EUK4KkR37GrEyH5Yoqc+Vg=":2,"74ScHGU1kOsGMCDn6+SMbExuJ0Y=":2,"74kVp2Qz/ClJ+826v36FnrVlEmU=":2,"7G9aQ3bdYQZRR5Xfs9AsSwsaPRs=":2,"7JjfkitOjBaGC/olVMv7P7VXegA=":2,"7Q3tTd5EeObvMV4js0wywUGWCDo=":2,"7ZvG0q9vmUTKE/kR/8lnxgr+W9g=":2,"7trRGs2suPfs2j1Fw8il8Ct2I4w=":2,"7ypMXoaWrM++zdAbjUUcvDdl6DA=":2,"84VFR/BgNC855g31gNI+lcgcvK0=":2,"8EArI4IQxFb1Jy5KKpgqxnjYqU8=":2,"8WDNGSYrBtXaSuNuCUcuENOqzbE=":2,"8jVuTrHMGXgJ62nUXx9V7cN/mzM=":2,"8nS7YyD+ru/R67lIeZKe30RKet8=":2,"8tt9aAK5Lm7Zcy9hZvUy06bgxCU=":2,"8ur3KjsVLO0lz7bfhzH5Rz06+fM=":2,"9J2Hjgd0WomJNXCVXUnnFp7HblY=":2,"9MZYN7ZW722NmXNr0I0a6xNGHw8=":2,"9PrZjFg2RssHXVdygP2FMo63tHk=":2,"9UuNM5QRWcp6mYkcu/t/I7FADLw=":2,"9V5BfQG4ZefovfNaXdHkf3YSIbM=":2,"9fj5AS7ShECezTEbos0drF7hGYo=":2,"9kQgpCCVk03s/PvG6sFZp1HUjFU=":2,"9rJ/kzjFOsSGSnyRLXimccFODIk=":2,"A24Vd0mboNwTK86qjAnL5NPT0ew=":2,"A7mJev6l+9dO4av3/tE3zj9JzLY=":2,"ANAAVt1I64nz8zJraV4+sB3bn5M=":2,"AgtaUdyyXGWAAMwjRWqwR8Ni8fk=":2,"AsgodONRHQuPbpgDwflsruAzPns=":2,"BJMwY+kVmXIH7sXwmXK/Q13fPkY=":2,"BVHoUpngEWtu0oY2qTH5s35an88=":2,"Bg6wgMZDYx0+/TxkfcfpqINBID0=":2,"BhE51h7DPwhQMQULRUIC2yvAK0Y=":2,"Bpex3OdFL9MLPykjDHF83THK4XU=":2,"Bxd9P1pUJldfaa0T/bjJ4RryaWE=":2,"C3WnzDhbWkxjLSTmT0Q4X+C5Y0g=":2,"CAPrE/fqgLuDiAW9p2hVJvKDVow=":2,"CBUdQZWRoFXyEsfV+uWNQuCMeFA=":2,"CFfzn2HEP0aH/l6Ix4YZ61KhVaM=":2,"CHJAmkWmDHuhYsu3pfxXnIh7HZQ=":2,"COZfIR9QIUwbad3hWuzoXMpnAN8=":2,"CQDGAq5GGMwFpHfW1z5j8CT63sM=":2,"Cjr9Pmookm1kE1NrmqrajTbbNGQ=":2,"CkEWJOcxrLmlVHE98ZW0fCC7cLo=":2,"CpdpCv4T0/puu4pnh4QKOoEMmAY=":2,"CvqZCPmZan5z33NXlrLvk5mzYrc=":2,"Cvywmg5xUNWlVmE1IlOnnaftKTc=":2,"Cz5dAHQlqdvtLvLtHPDUZiiiu+8=":2,"D7m1f2X1ZclMkgJz6eoxMvjV/BY=":2,"DBJzFXqFvKfiVCuXYUaENduUW+4=":2,"DK9He0ANXehmcN8YZhoASlkBlBo=":2,"DjMDHHpf+nnOnHxm/T1q8y5AoGs=":2,"DptldxPMxrOcxrsoRizh9K9nzEs=":2,"E+JAai/R1FoHjUcPV6PvZ+pFENY=":2,"E6/wFsja05ZZziyq3W/qhIfGjWM=":2,"E6h3qgjEj/9/yT6FUGw6YpUCrEU=":2,"E90biI3zHl/mrbRJIr35sUO1G2A=":2,"ECSWj1Bohkn3n1AWHIMEKlbepxQ=":2,"ELx9iaWnrWA5GpWryrvIbwgayb0=":2,"EYpM3f28E5iXkICbU1lbq13/xDs=":2,"EcH6B6F0qgOh+aDUV+DrOVR8Ag0=":2,"EdIuR7LPx1E9lRrlGHXSFSHL2zo=":2,"EkweGdn7Hj2CcJIpLShtXcn4Z5U=":2,"ErqMWkx4Pxb7qmvS+z+hDqzHXtM=":2,"Ew5jcdiDb1RN24kn4qz0nvhVBRg=":2,"Ewtt2SfL5QqYenyxIDEkC9k3tXs=":2,"F+TX3oARl/flaR0nHt5Js6PSCF0=":2,"FBC4lg/vz1H0FxdIxuTqNU4ZEB8=":2,"FJGJZYWxDCoJrYWPy2BUgtq1HM0=":2,"FRQtGBzKKPjy+HEtL7DsX3Felx0=":2,"FcelBmyYBLBRw/HaUwB9s6j5p3Q=":2,"FmvG9vz/LOjYYeU35txjwm9nZRQ=":2,"Fo02W4qlJUivZslOW61nZcCyA9I=":2,"FxMDzlEZgYp2JToAhAg+/yUllRU=":2,"G37v4qWLagnhBfOYr/ow2BEX8wQ=":2,"G3w71gAwbEQRQHACN74TIHrskus=":2,"GA+aolhIHd7aBXICZM5+0OrHfac=":2,"GBKfC442lwAxRkYbckvrizRO9s0=":2,"GVXlVv4EGFdKXDT5DPw/i710C9c=":2,"GWb9dIET7NeLkFZHxmz/DyBcwvw=":2,"GYhBRDeJahYz7i3JJ/8IyE2P5Oc=":2,"GgzZdRVruTyuSjP6N/LfEPt+Rw8=":2,"Gpss+UglPt1UUU2jy1ZlC2Haaos=":2,"GtCeVEmWQHdw3rryz0AuH5gyJxM=":2,"H3GqtTSpNlUUna7Umw0oI+0NgR4=":2,"H4j81ysGT6UYvop5kplp6lxlqXg=":2,"HABGlXq1BaOVH1Ifx+TyX6oI1c8=":2,"HKs5tNwpPQnqsBWBwrTC+hZGTTk=":2,"HM/pWnlnNgRgj3BUP0fYxxzl73Q=":2,"HPKcn8DkK6LusgLP9nDxVh2uJC8=":2,"HSh4Zm3wQfJIatvBZlrOfUeOfwk=":2,"HeMPvC/blr02FSRJtCharxgmzco=":2,"HmcCaa4SwSvvXXelNnwnv7AJeY8=":2,"Ht53X4lOUdtiGjATCg1fkJrokAU=":2,"HwkleqQc/sC85c17L6GdLZZRy14=":2,"I+7UKSXKSzGBgefFdkILZwsI9bs=":2,"I/ZGfrelShQUfRY9aoFBE2Ey1es=":2,"I5BwwRq++KWQv4ptZSLHGgVymIs=":2,"I9HlcZVHx3L832KLRSQTyyKcszM=":2,"IR5VBOOYDolqu3h+57TkJv62y4Y=":2,"IVYz/Wpt7sibxI0sN4+ORgouym4=":2,"IXYNhiWHet2dJvLpHZUkTrdX3T8=":2,"IYpFc21SFnpXN7O5VIPFH6jzdDU=":2,"IZM2fgogPf28F3qsGfathZESrto=":2,"IbS6gvfuP5iBCntJRl7/kGFWfU0=":2,"IpkxtRCHzfxeCqhGtGlgTZho5PA=":2,"Is6xz79EUUUlkrq+vTsITATIXy0=":2,"IvBIWWt8SMIZYO4zi8CNRQBdMa4=":2,"J0sdM/l3EGX6Frv/rSg7j7GL01Y=":2,"J6lcmiUnipJu3gVcoFiUbu+lbDs=":2,"JCDhMYyv6cBx12AS4NeS46EYq1U=":2,"JNVlxkqEWTvStjmY4qJmC71fnzo=":2,"JTODY14/ed4R5V5Q07Wyq1nHZCs=":2,"JWymogHaDHPgCtgoLPRcgF77J7g=":2,"JX1E7bBowYq/NXrg6uP+3EJOu1E=":2,"JgkRGWnXEHJQ70T82y1m8BrqX3Q=":2,"JslOsSCDdI34ClPlNXHA44C8BdM=":2,"K/m4A4gm9l73A6cVZkwC06VtJOU=":2,"K0pey71AM2vyu7pnYfphijaZFQQ=":2,"KCVNDq0FuwrUaqe8KovMG7Uj3F4=":2,"KCmDv40c4KHL+sZ06jr0iETCE/o=":2,"KHAia59NLQnyXDqGRHyg0ZiaTFA=":2,"KJyIq4mOXhu24VxX9gSFD3D93v4=":2,"KfKkkVLOQcHL8t9vHg9yVAoakwQ=":2,"KixfNZ8p0zlprYxunHrPzxobaq4=":2,"KmZM9Qj7kdUlBfwHt3Ha5IOIaXo=":2,"KnVtOxTqcw8Bj0cILIShIFWDZRU=":2,"Koq5VrYu1jgSuPxzC8JneftbXrY=":2,"KqtJUSyT2ifVLtE+YTa+Jwrew0k=":2,"L/1K37AEimYFLDPtWP00QHs4Y68=":2,"L2SILwi57slYAS17LPKLyjzn//M=":2,"L67fEFjVgKvKrPk8WOq+ypaV0dc=":2,"L8Sb2X7fKyM5N332D0ndTxRV6UY=":2,"LBcANp6Rge+D7JyH+lPItmNHsqw=":2,"LH6IDw2lqWicdgcu+tSQmhjaVvs=":2,"LI77XnWaUgy77p5DAeqIO7vOH14=":2,"LIcUrXtcBWBDTsYmK/hSjTpkhOQ=":2,"LK/nTUZLp4wQL8LSp6SlGXML0Xo=":2,"LLWfG5BXDbqHYtiETKDto5KENC0=":2,"LPP/dWFPHE2kpSUwpzspR7jegbE=":2,"LW/7lCwmMHUj5quQrOys8yKgpSU=":2,"LYaNdeviHa0JUthz/IPOwEOXmQQ=":2,"LZMdcjkdapf2PBM+TcQgrrw5l1Q=":2,"LadD1LUTKp90k3P8uJv25vGYSHI=":2,"LcbdMwrGmghZm+QEi615YhcnzP0=":2,"LhU39BVBhzq0HvhANd7D6dP5Qu4=":2,"LtHcu/ZmzB4KTac2VXn1G9F2+yE=":2,"LxPYBWzEULXwJn7iyMSa8QtD+kg=":2,"M+ZG7S72MQJCJe2aQVcvZoKftWA=":2,"M/Wjwu0AfUQ2o20egq9z+7bIzRE=":2,"MVHNDtRF1gJXlUK/+UZ6MIq+cMk=":2,"MXK6lMxDUXU9R5KLAL2bNOMx//A=":2,"MYnF7KiFcThaEWDO9xNzhikWzZg=":2,"McPp8MRX+uUUktsdxYDRi8o+eos=":2,"MftGgIb+TIwSyHnx7apoYs9NrDA=":2,"MpWCvxXFEf9daTeLjHcm3R/E81U=":2,"MqHL/cxomXHa8ev3atB93jJzbZI=":2,"MuDS3URWGZcyPBilzs4FXRzmboc=":2,"MuuNU16haFees5FcNMYXYToRZfY=":2,"N8NipVw4J3jV3lwC90mjPwfCHxg=":2,"NEHnjJf0uubHBmHAJBilzidYpks=":2,"NF8b22VZThqOOVOFtwz90G+TnlE=":2,"NMjxROmwGnztdYpQh/UAc4Bbnr4=":2,"NQamteBltpv0Ps+H619TiFUCf+I=":2,"NdXqc2xTrq/FN6tgl0gsTiq3F38=":2,"Ne1UYIth2fIOE+GqWmLouOzVGoM=":2,"NeSyTyiMagGROQJlNI8QSaSlBSw=":2,"NgAzcAy15WMJsY2pkT/2GxdgG04=":2,"NmMsYpAfxlJVp0FWodzxuSiHS3c=":2,"O/ynEwzhifwXixFynPqJ/W/oWh4=":2,"O0wSnPIMZPh/STNUh0vac3hUGJA=":2,"OEU7USrAsrnhG8bqMiZ26hK2CNg=":2,"OGH8Nmp74ZiT2sjux1xx41S2tNw=":2,"OJcSYTQfOFc27T/8rITzt7968R8=":2,"OQmSZcXWlR6aMwil6XEKlWcjacg=":2,"OVBSN2PMsKlMAlmaAKYB1cRcoY4=":2,"Obd7ogklY6JivNJCQIXV8d0qDuk=":2,"OjHuY9k7IqD58ta4pJplHxor6YM=":2,"OrjPTpbv6b9JNjns2OKkVTiKM+s=":2,"Ow215V5uWo11K+h3r5uqPKLzJI0=":2,"OzlQr4k8StWsbx6xo25olxpqFPo=":2,"P20wwKcWg8wwuTQl2+Brvgsvt0I=":2,"P2fxs0FUJWvTtwxgQ60U8ShnO10=":2,"PDaqV454hbqksZYGhTh5MEKnTws=":2,"PP/b+e8PPjUMQYS2OBT8GhMPS1o=":2,"PZwAWgz5MHGCT1WnkwTC53E/m9M=":2,"Pmvf6keEdJ2RdJbIEPbC6yjlB2I=":2,"Pn/ePL4HFaa4hTTOIC1z+UcbhSE=":2,"Q7VBdSOn3tXuYecIipApfUrWc0E=":2,"QANgRaF/b2zkl6ZtfzavHjFDGww=":2,"QGhV7+yJFgHnsLlp61izzFLm+8M=":2,"QKSyrWjQ6MjhtW2FNppRoKVNRCY=":2,"Qbrqdt73OY7jzL0r98xuGkILcf0=":2,"QtCZzUY6hCGEqCUTc2M5HNrrs4w=":2,"QuBiJAmt3+xnOmt838WFWkZNBII=":2,"Quz0fwq2iFeVentUcxv7EtGXBgI=":2,"Qy0HTOBuuQRuxmyN6GCjTBI+2MQ=":2,"R7CcmUEwytA73udabElrP9G8sN0=":2,"RF2lGGs4R20QEkXEifuLc5MTiy4=":2,"RIrVY4vPSyEJqz7rQNux/M6K2Uc=":2,"RNUHLVNAftYYrVsfw0XdkUFumwo=":2,"RSTkS8lWrQGjrgVquWcQVopYcRc=":2,"RUj0ztXJ7+kOsCpP9Kv3TDeFJJg=":2,"RVzwez9xPSX1AEn5pHSL/PR1Ak0=":2,"RYWzXqC3fQdwkaxnwdmOPmZixUw=":2,"RbcSJm/cbTD27QN7lN6Us62QIlE=":2,"RnwhHERLjD4kuXuJm44mHsUem+Y=":2,"RpqgmmdI4JgMujDXyfPAuYlQsNc=":2,"RqXHWd3nIsw7tt+RmTWynHdd0X4=":2,"S3CW+p7BtwcbD0fgDCiZ0RAQyjE=":2,"SAo2aVtafLNYHW7zVkEhRT9bh2Y=":2,"SY+bhxGSSGCnz1kQKI5yVUmhEfE=":2,"SYm5CVFkFOVllamvQ9D/tRM1JDw=":2,"SdL3nSP2tifv3D4axuGNQnI2bUc=":2,"Sdit/gOF9Dasz7o9sp6F7f95VxU=":2,"Sf6QB4b+AtQzltHOGfemdKTv/FI=":2,"So1TyGdA4U1tMl43UysxLdrBD+8=":2,"SvBLShco9LDUjRwg1aaiMvtvTFo=":2,"T/wIOHUG39AOmrfsXhUAzuEQY68=":2,"T2+of555wmTbJ2TrkfFXZPtJe6w=":2,"T9Qe0SNV0OBiGFU2oks1F78khLA=":2,"TMo985XELW9v74mmn50qi7dfmbc=":2,"TXp5FPH1q8BqEe/vPr2XzQNN4DQ=":2,"TYN2QA74YpLLdgx/KIWyDC7yWrs=":2,"TmrP6vdRPLfVW5N55bGHKWuqKxY=":2,"ToVZFnRaRPFc+bC+kUfL0o6oVbY=":2,"Twx+PUyhAazRa7zunJLUk7AuLcc=":2,"U/HvDF5lgUDIOvDbP9v5BmirEUs=":2,"U/MXSpdHG3Qh2p/vzyc0aFq/U94=":2,"U3KMQW6Rs95g1UJIi3OsZRqYWvQ=":2,"U7ti2JIQ2rB+nGUoJfrARNYcm/o=":2,"U9A/mkuLQvuMuaD1/0NbkxKJwsk=":2,"UDzrIJUrsqeKvuc/bTIuZnU0+5Q=":2,"UHJJhRN9z3qlaau2hbL1mfcfrI4=":2,"USKrPvDKw/JS/mQnPgXXm0PjWhI=":2,"Ucs6z5K6yxsQzCuxBg8IhFUW1uY=":2,"UyrsycnE0Y34SKsZr4aPMI19T4Q=":2,"V2W3L8FR9XTVZQtEl9UZ76GRaOk=":2,"VC/PaSikiazeBowkWU8F8s8Fbdc=":2,"VXhkGMKyQv4EGmsqXOlEmAdtX8E=":2,"Vus+nTDrUYcfuhZkTwWq8pp72Fo=":2,"W/vJPSCn52d0z02T4zSZuXmUFIo=":2,"W3JXUQpisayYUb8fvciX7mz/LUw=":2,"W79Q1UtfGoRJvjuDwvvCFd/g0g8=":2,"WGcOskzornIFeV5Wbec+z/7T8yQ=":2,"WN6w7LqpMGoL89o4ulIxTcXAttM=":2,"WirG4pLCvHATRD/XepELhtbx2z4=":2,"WybscQ6r1DfHRHCfANqlzsLEfR4=":2,"X+IBU7yum6s8R9EIK1eZ5xNXHzY=":2,"X1hwqKxZESTzs9BvFVN1cudNbU0=":2,"X3CsotjCGLmix01VOhQnaVzerc4=":2,"X3r0cKrB50GCupilXtIT0OsNmNU=":2,"X88GhHdCWKsBm24R727HFAkDr7U=":2,"XIcpBEZDocLvzctDOSolZeZZGMM=":2,"XJGnm7SMThSxDgLYX1WCQCpXIeE=":2,"XXS4Q2MvRlQ3g/5H4ppGQKiDMuc=":2,"XdRdTTf2L82I/5T7+QKhT3Pho24=":2,"XiqKy7gubyO5rqh2hQCzWLmuRP0=":2,"XkVTCFQo/kf96t12VPlUHI7Bsoo=":2,"XlU2doslDe9k2Sjyz+HoF+s4Fuk=":2,"Xmi6obAjhT4C07AkOLr1DrZOYng=":2,"Y+3yeiQnPoLWrymZUS7uiptfvWE=":2,"Y9F1acusJNtR1MKQ9sV/LUbtLcU=":2,"YCITb6CU1HEkdv0e/aMbXU15Bsc=":2,"YCrDNn1PepBzFGwS4liz7EGhd20=":2,"YQX9fwmNvbp79I5BVuG+xSFVcjM=":2,"YXSX9V1CWZmwRJWSO+196koeC7Y=":2,"YeDH3FcQ76eu+6wKfeDV3Z75z4k=":2,"Yv9p2UhdpPR9HiQAehTqepmaOtk=":2,"Z1cXRToCPBewKDIjZhu01gzNgvM=":2,"ZAqPZQWJRYyqIy1vmo7cQCQVpEU=":2,"ZCc7WSp1R56ujdXzRr1nbB6X4PY=":2,"ZIWJDsMLDNK3inlfrSMccHToQv0=":2,"ZN87pxH8AlA0PR/ktFLGAjm8JDc=":2,"ZSyVOd9TPha8GxMzhtgZiF93aZg=":2,"ZcCE53MqfEUAG0OC9vuXsgNygQA=":2,"ZnEawSbVIhbrxvLmTU/51FR3PHc=":2,"Zntf7wQ+SmweAQbnVsys7KRLiCA=":2,"ZtCtW2Sze8YbTG1fS4loW1n3F4s=":2,"ZuT1OewLis1kVKZoBEacnH8c4oQ=":2,"a/06Cc1qCMoW5/jphsMeYfBLXrA=":2,"a13zReUtyPNWrTN/Br6vION4M9k=":2,"aMpjfejGmbvrz24NGwVgyoJWmB0=":2,"aOhZbO9SzuqTcdPglSVnbInJ0HY=":2,"akhZxq2ZypH9U+g8ptVEix2Ys30=":2,"b390+KlW3do0iY5dWxyw/Nvj8y8=":2,"b64LG8t0nZIMAH4frxWe0Xe0lVc=":2,"bAfivuZXv6xQHZCq1H7RdaBGeJs=":2,"bI1Vo/T/gZu6ziJq0A76h0bkQ3A=":2,"cDMRM41OYKodBqf5yPs7PXp7Ibc=":2,"cFGuCTImI1LKaInDPxQtiun7tc4=":2,"cFmOFeeAC2RTc++FepBrbqvZJu8=":2,"cS+k6IBZ28FX5Gu5yS+3rwfash0=":2,"cU2FpWNjt8mGURI0k5QPpMUA1p0=":2,"cVRrJOXl5PBGO3dbZJV8A5XlMQY=":2,"cYEiRDkwdEht5TZ8ftQ2T12/vmI=":2,"cZnefofy4yEnFmkQ0gaP7nfgGBQ=":2,"ccdh7Hta93FtJR+qwt7DElNPqBI=":2,"chKNF84vgaJ1RtQrKV4ytLiKjlk=":2,"ciN5+j5UQseF5/8p+leZThdpwLk=":2,"cnQYSsJdyO+otNbyW42g39tQHFw=":2,"dCGPD+ybLSgoWN7NZsSKWMIMa8g=":2,"dNRX77I/GjMbKJwIPRRuZQnef5k=":2,"dSgRwJW6QXt5Gyti9tvXKOSloVI=":2,"dULO06RXKgWKOnT+2EPWFhzSOzQ=":2,"ddNzLLovIOQAjI1Fuour/azCRPs=":2,"dfVd5Cks1FFJVdNmS+sD4zItmmQ=":2,"dhvEuIu4bREe/yc3n+uWgemDH+Y=":2,"e7QR994kinvEvNi5PEREfEgRBPk=":2,"eIkv3FutAxmGf2Mh7yo0HiDjrls=":2,"ePMMAHx9Ax6ezSppn6dpqbBnLhE=":2,"ex04CvLWFikDWXjGQ4RtjbOeRNM=":2,"f/TYvHakawJF91GiVgpjciGJPc8=":2,"f8cvQ/sraTsg0bwM+aS4D6pFT6I=":2,"f9ysKU6hcNbVfpf1njOmFQ2qbZ0=":2,"fBmcT7XNbLpOQsfKdgdXEdc1P38=":1,"fLj05EfmTLEt58m3jVUhPVS04sU=":2,"fMus4OBg1K5k+k8tLnAZyRBbnLY=":1,"fa4i6qfS5+dDiDVKFZDjsFnF6Rc=":2,"fnRvo3ItSPsvU3LKSXBRXJg0FUs=":2,"foNSnwHq8ph3wPaXJ8I88LehpI4=":2,"fuy5x4yKH5LLTw4kz6c1pnFiOcQ=":2,"g0FiO0sC3nMBLvy49sLKeESA7h0=":2,"gCgo0usJBkT8uf+0XUuS3gWGdoA=":2,"gJJSwVtCLng5e1xxugIzmlnHbd4=":2,"gJK5pMWuNFrD9OGNIgKananYNSo=":2,"gMpTRAKA7Ayl+W/YVqQhr5GU6x0=":2,"gSTTNamgmAMk2//gdb5jcZMN9AQ=":2,"gqGHjniFghep8E6txoAdzX/4h3M=":2,"gr0I/I6o5WZbCX3ANuvNoagEEe8=":2,"gsU6EH/i8w1ThrqsEm6GK79feO4=":2,"gtc5hvQud0vqhMhm2fmcV4S+Agc=":2,"gxn+9RnfotKjIQsWN6Ldd6tJFwQ=":2,"hGJJ6Hh+MF5i2sXu2g4Yz8nZwHs=":2,"hJM03Qy65rIixL+QnwFnZUWdS7o=":2,"hRdDjllKxVjmSDiRpxs+uOUT9Q4=":2,"hl/Ql44a9B+9BQR34dUhENrlIJ4=":2,"hmt01LHgEU30nJb6VMA41XMWRyg=":2,"ho9bP3IJ21F4d3qP3pTJA0JkTj4=":2,"htyr9QaPXQOHGsfVmr+97oeW0UM=":2,"iBD/DGiehe+56IosGVlv3c1wJ3Q=":2,"iD9S1fFa6FpM5DcIk5pQUCSj4aw=":2,"iGzRH+UPc4Ea3ApuY49us3/XCaA=":2,"iHnWlD4n0QHbXoUv3k90wclViuU=":2,"iXvZsH9NpG0qHURLzLTudfP9aeU=":2,"ihQuIV4rmAFYvTr6lwMV4HokREI=":2,"ihRhUKLVahjKDEOmS0BbYyhgv7k=":2,"ipbKvdY2LsQlCcEkuSqe8v4By6g=":2,"ipt6XYj9NCIb0hWN8BbyXEF5DfQ=":2,"irephTXGVO+MmlZ2AS7MbB5AyLM=":2,"j+6DOgEHMZgTIWSHhf9CjmUGIK8=":2,"j+GDvmG+Am9Xd+4Q/XDAlrQrFz4=":2,"jAuqNF/yhbdqVNpoWw+2Jo6e6bs=":2,"jEtGDH1uWCTHnmHc2bGPDrpEHCg=":2,"jODdAGIb3/eRchqP2DHAiTYlP0Y=":2,"jQfCzjp4d/PizsLo5UpdD2f8www=":2,"jUoBtJ03/Xr45tg4Dqenc5cYWAU=":2,"jVgQocUsQaH7V/2UxhLVOidhP20=":2,"jYhk6dm72WAaxSdjIvOSNJ3d0sE=":2,"jaMkW1knsztb/0+GNCKx6G/SzaI=":2,"jc0TPKjiMVneKPNeY2avjLI9KVY=":2,"jq2rGPyUu7grG5FJi/I/qcKLKg0=":2,"k/v8xkMt57fg28L2fh73gxXa4Yc=":2,"k1J17FRo3myPzj+UE+LXaZ9ohSc=":2,"k3bwFkPXH/EDNGF7Npn6kwKJu6Q=":2,"k7hYQMMCyKjXIXP+LR8U+d3GIzQ=":2,"kDJnIwokySTxXp20eRGTskuMM9c=":2,"kLV2LBNc1aIFljjZItvqx2bhY1U=":2,"kMGe+97jR78zimxmtL9Ak8a0OHw=":2,"kOvgd336AvzRZ7zhd6KqxVNyMiU=":2,"kYI0w0yGJWsEW8mUvaWKX5BGN34=":2,"kYvIeNAo8XJrY0sLt9RkQb/ArLk=":2,"kbEmSJ7AT4IyCib5dANoydcLsmE=":2,"kfmukk3rEZbsice63or/akfPSU0=":2,"l1TwOsy32JiZV/bM26UQ8oCnn7k=":2,"l1l9LWMnuHXWDBSLcfQ3Zp7bVVk=":2,"l38tVXmuuGlAgD9a3eXwX/jQwgI=":2,"lHA+dhHLhlHLq/O+0+Xz7buufJ4=":2,"lJcNksg20bY6CgGPqZu5aQHbRhE=":2,"lUbDYyCRhvBzS0nDrz8rx/nq0A0=":2,"lUfzJ3y5Jzs7p4PBHh4xhm+zoG8=":2,"lV/IOzmMkT+d9gXfFgqtsErYe2M=":2,"lY3+bGoDRF7A0eSICXUJ1yfpxo8=":2,"lZpQb3elaMvd1gsI/plZxcpUVwY=":2,"lcXOtK8KVWFVeHE4WLEewHPCWWY=":2,"ldPbP2/DX6N+AldQ7AFtoht/Bvc=":2,"lhONN2tBTBA/tSnMtizicuNBPLU=":2,"lmXhk+G6r/qbCMYZcVdJbnn+93I=":2,"mEj21qtta3LLlAxWkZ8sijZvktM=":2,"mSNPvAXxob4waWjBxs3ziW6z71g=":2,"mYby0t8WAz6jV9RgYIRRGyfeWB8=":2,"mg1zJLHTYaJIMLNxa+rsYWsZJhI=":2,"mh3wxEYkuAk6sRhlr/C5G/gxYRI=":2,"miEzBOZuMem0Cj9Uxw1LjN9S9cw=":2,"mlWAbHBzQ5Td8U9HSp8fcPdPyzQ=":2,"mmci9ejkh1yqEt/tTbqbQeSaSwI=":2,"mqI5UE1zlvwODeiYkE+97gk/N38=":2,"mv8xDlT2/YhQtkLKnptz6SQB5uM=":2,"nAj3Ny++JKnx/3X/T3HRcVSVXN0=":2,"nHcOob6uJ8APh5510nMH7Ikg5XI=":2,"nNG/hSMKDgXudXzByhmJ+8Udww8=":2,"o/9SDeB9XVwuJyTLitMsvbgaQKg=":2,"o1UwUqLfJmuxKSuFfNRz/EzR9So=":2,"oHsvpBtYgeqeqVOdT/DQDeNAyp8=":2,"oIsMwEfYOTvIzVNKHP6nF5RhkVk=":2,"oNPOB+kuiVqZt91Ceva1HD2babU=":2,"oNrKGepVbapve5qoLE4s7JK926I=":2,"oVr3ZmvWmeO5V4lOW6+8gEGE5YY=":2,"oX3Jk2hkzKOTCsCjIb8aBoEQxCY=":2,"oc/SFHoD/b37HnDYvDl1S+Ln8+o=":2,"odROFyqXD/frsFAhnWAK7yW9p1E=":2,"oibx5gMRdevDCHgIZ8xHbhmCom0=":2,"pCDKhit9yDclLI07LYJ2Arec58E=":2,"pFBMDn/qQ2sU3hbTzN0XE+gPNlE=":2,"pGtFao+Eqv0xw/MJ4ne73wMtss0=":2,"pKocQQbERh0k2bBtqqHvsL6IzwY=":2,"pQbhy62y3+Jaimld0fQXsr90MPU=":2,"pSQhqungWlpbyd5qgvoDgc7AE/E=":2,"pXA8CdPQ2YBCgTcuH0u3ji94FpM=":2,"pj/VyHVWYMY781dFmsalRMjAdng=":2,"po8Y5k48QpkNI3OQK3HJSajJvIk=":2,"qGuRw9GZC5DTd5qdb/ri4A76b90=":2,"qX+92itsZYUdfyqVnkRNS1z8pEg=":2,"qhL0XSJvQoCDATRSMO6uHK6s0AE=":2,"qnTJ6qWlEO3mGlvjzZxRj1SWBSg=":2,"qpvlQsYaJOxyL6Vr9sOa7itTjWg=":2,"qtTOojYzivrM05kWyMG+4B3oavI=":2,"qxM7PjtM2REivHU89TUKjRFyX/c=":2,"r2jAg5LKs99/R7UDy7n+RVExthw=":2,"r66x+/C7gcK0ek2UGYaFgDG9W7A=":2,"r7iJmf1gYZfcG8O+Vd5YotXOO98=":2,"r7yFRxkCgT3Oeq05RWA9OtXpSI0=":2,"rBvqswHFQGNJ+GA8LqPPr3KtF+8=":2,"rEMdbHd/v8VQAKMX0knLaZEP8KI=":2,"rFDwZivZT0u0vRe8Vj180HEOHEI=":2,"rPu9e+cSQCdzkKfYpDy9vIMdrTU=":2,"rSlZi3H7e2ESXD48TLSxA+uHjp0=":2,"rVsxB7wqXgKFK40cRaUdv5100/8=":2,"rXmjfschRYJInGVNNv9jGIRrjJ4=":2,"rXtx9FDQjNrYAd+Xt+sv0IjIaJ8=":2,"rc4WJXNzddjdAyW+WERMtKMaYYI=":2,"rjWw31OACJd76/zAEVPT3BCWrpE=":2,"rp1qUhW5AwlfEo+FC5F3v84IJG4=":2,"rzbYuWFx8KRHnLGL8HtL+0dJhIo=":2,"s6btLO7QAG1u99wehXlGkxKUb0Y=":2,"sD67cLr9VrAeoarwQnBVmBOjfCs=":2,"sDK4hQp8T2RmWpWvgnHk6FQ2iwM=":2,"sIl513b2C2/QeDrHSuBpH2c6C0k=":2,"sL1dde8EjkxF+Lb89yeCnsBYBOU=":2,"sNAV48ni/e0b7Gn9jEfM3Q3dVe8=":2,"sPTnbQmrNOyx/qW0Xu4g4cu0aR4=":2,"siHaDi1iEPE3xPgGISfpfwb8h4M=":2,"skXg82RVCi6BZHNRI23RIG2DRC4=":2,"sunLW+vgbWmbUrarV07NcFmnKgE=":2,"t/DNJowu9uHR/kBc3bc1Nm7+9lo=":2,"t97bh7mYz2gwiY6nU+/w1i9dgZU=":2,"tBZZ8SUVG/FjRUpROxHXX3KaCyw=":2,"tK2lpUcycitAF1Et7B+/ZhiQ0ZM=":2,"tPPRnCQb4zC0dD4BJYFC6KAMVWw=":1,"tR2fhHtwBTc9bKHWDB/g0JFDNBY=":2,"tWRGM3CFPPslofcsSqj6vpcd7JU=":2,"uBMu4M5/0KKZY37hxUNLN39LPtA=":2,"uKsvvIzSMOyMR+4LPExkT1A8iDA=":2,"uNmw0kNAuK13LWxLuTZiaO140LY=":2,"uSACpi6t4iSWKjHFN6UHju08OB4=":2,"uf4z/h0h9ZnyOD7ycAiwgx/aHFo=":2,"unvMMzjFrurZix1N1pOtUC+RriY=":2,"utfiG74gl+SIxnKipbPl66ZNVWc=":2,"v6RXBuPtNohU+Lb6MHwV9z4lt1w=":2,"vD6n6Z9JW8prLB0rVlTt/g+4cCI=":2,"vDX9FACK55aBjFOaPdncKX2o+Bg=":2,"vW9hX9bdu7jEnh05U+zjXI+SbA8=":2,"vWbt1X54cCEDXSdUl4qpqdCohNE=":2,"vYmUY3JR7HpU6cV3sp31ubgx+YE=":2,"vqAjPDOmXZGBn833qAWFeo1PtsU=":2,"vqaClXLm/YZ8MhiUAukne10yy/o=":2,"w1vJ0NG9kDFtUNHq8zCMuDUVb8M=":2,"wAsstIPtDtjSlxUGn0gh+TfTWmQ=":2,"wJ4TJKlTMrrNezUg8dfBuOltq4U=":2,"wK4Zb71UwMahGOQFp0E3RYeMfYw=":2,"wZinPjTdOm5rrXI4u1NIKiffmp8=":2,"wib9u5YbRGJAqT5Bvh9zNZp4iTY=":2,"wqO+JoG3Un7fd2bDdvRxfAhbJ3Q=":2,"wr9Z2KN9f3Tq2jo3nTe3DpsNr54=":2,"wv3DmXgFeOrq/dbsherqtSjmrO8=":2,"x4c04qHrWNjtvJ2XgQQpd7wW1rA=":2,"xFo5PPJwZaq7i4dWLyzXeywAOEY=":2,"xUJ/eh98DaHs0DxWLA9fYYD6PzM=":2,"xgEKSqpgWJmo+flxFClV2/NiOJk=":2,"xjHJCBSoTdEQjebMV8aARzFcEkA=":2,"xm3y8sIKteMNAiUYEkt/ocEG7VI=":2,"xoxgp5Cx/yYkyO4yumq70s+D5t0=":2,"xtOFUD8jRLwrgCyGI5QnD7K+CG0=":2,"y0OlqPMUw6B8jvG1d2F9DOvkDp4=":2,"y2Kf0efsIVsF8PYgZBOV7tc5AfM=":2,"y2QRZhLYezQlVyzaDO4PEKbAmAc=":2,"yAXQT+zYHydb1uUhkuwtxm5At5k=":2,"yAyH2ZVkhzgat7fcC+nSDXQ11jw=":2,"yKtZdPSQMfJNQVKjIJ6noDw07mQ=":2,"yM5jN0VjPFKIKpUqRuN5KyRPd94=":2,"yad44gD7FAnezf8DgiIRZiDGlRY=":2,"yekkHNxtLVYK9WvooNiEKVWXabA=":2,"ykSAQyJm33Umehd0Txp/8rpum2I=":2,"yx1gN4z+x0naLVTbhc4/HO1c7cY=":2,"yzJqCQsowhulZe3Hx/xsWOvlTbw=":2,"z6CTfToXHCMt/46aowVDcKOYuL8=":2,"zAmtiHUH7ncF4kcOLIROCCAtn5Y=":2,"zTzCkN1zga1linYQP6v2AyMYW5w=":2,"zd+y/4GyfV5LQFAHFVfIKQuur+U=":2,"zejOvMNeql2wesKjXICcANkzyPM=":2,"zvV3Pm+WpZE4xD79k4mjhrJu0gw=":2,"zwzjvFMamlRnd5MSmg2F0LgPqso=":2,"zydtsLKKSp4EUItk7o34H/+dTNQ=":2}}},"ukm":{"persisted_logs":[]},"uninstall_metrics":{"installation_date2":"1746408261"},"updateclientdata":{"apps":{"eeigpngbgcognadeebkilcpcaedhellh":{"cohort":"1:w59:","cohortname":"Auto","dlrc":6893,"fp":"1.4497d8060d0e53c12b4403aa9ebe7e827d4880bae3f4139a26a4feb7ed64c4a2","installdate":6698,"max_pv":"0.0.0.0","pf":"95a3a483-dd7e-4e44-9157-de8c01a6ac4a","pv":"2025.6.13.84507"},"efniojlnjndmcbiieegkicadnoecjjef":{"cohort":"1:18ql:","cohortname":"Auto Stage3","dlrc":6893,"fp":"1.d258f4f112cbff3c06069680768196d70cb5115221c11399aad7c534cf3d3d01","installdate":6698,"max_pv":"1452","pf":"233d3689-50fd-401b-9133-49c5e6dd43f2","pv":"1492"},"gcmjkmgdlgnkkcocmoeiminaijmmjnii":{"cohort":"1:bm1:","cohortname":"Stable","dlrc":6893,"fp":"1.a02aa0594f76065c1ccb48221548b6ade3b6be5b50d24f226579e5d67d5e971d","installdate":6698,"max_pv":"9.60.0","pf":"85f34ddf-6425-40b2-b7aa-d5fc0c289529","pv":"9.62.0"},"ggkkehgbnfjpeggfpleeakpidbkibbmn":{"cohort":"1:ut9/1a0f:","cohortname":"M108 and Above","dlrc":6893,"fp":"1.0bd17169e41bf80771e71e625ed9469c4006d08a33caa457e184caa55174f67b","installdate":6698,"max_pv":"2025.10.2.61","pf":"9bf50e3c-cb13-4140-8c81-6f4d37c1b55c","pv":"2025.10.6.61"},"giekcmmlnklenlaomppkphknjmnnpneh":{"cohort":"1:j5l:","cohortname":"Auto","dlrc":6893,"fp":"1.3eb16d6c28b502ac4cfee8f4a148df05f4d93229fa36a71db8b08d06329ff18a","installdate":6698,"max_pv":"0.0.0.0","pf":"03d91d56-3843-48ce-ab5c-ed6b5f933125","pv":"7"},"gonpemdgkjcecdgbnaabipppbmgfggbe":{"cohort":"1:z1x:","cohortname":"Auto","dlrc":6893,"fp":"1.56c21927faa028be6ce18c931660eec37e41da4bfbfd47cafa48350f828c0dbd","installdate":6698,"max_pv":"0.0.0.0","pf":"dc7a0f82-59bb-4c2f-b0f8-f92f3ed1a065","pv":"2025.7.24.0"},"hfnkpimlhhgieaddgfemjhofmfblmnib":{"cohort":"1:287f:","cohortname":"Auto full","dlrc":6893,"fp":"1.2952996ce90b46eba6d1f09a6a1b70f3125fab9500b413f9148cdfc2b3284fd9","installdate":6698,"max_pv":"10076","pf":"5ea097b6-6cf5-4dca-a1fe-05bf224a411d","pv":"10156"},"jamhcnnkihinmdlkakkaopbjbbcngflc":{"cohort":"1:wvr:","cohortname":"Auto","dlrc":6893,"fp":"1.c52c62a7c50daf7d3f73ec16977cd4b0ea401710807d5dbe3850941dd1b73a70","installdate":6698,"max_pv":"0.0.0.0","pf":"ee844544-7a49-496d-b18d-f172eb81d6f4","pv":"120.0.6050.0"},"jflhchccmppkfebkiaminageehmchikm":{"cohort":"1:26yf:","cohortname":"Stable","dlrc":6893,"fp":"1.b911063da66cd283f0cd92d5b40fa36d891e285ffd0b67e7af79219db69ee5c7","installdate":6698,"max_pv":"2025.10.3.1","pf":"71812a95-6d6f-4911-a8eb-3c6dbeff6b0e","pv":"2025.10.5.1"},"jflookgnkcckhobaglndicnbbgbonegd":{"cohort":"1:s7x:","cohortname":"Auto","dlrc":6893,"fp":"1.a96d0537e5204a795c385b49eb255a32ea466726425aa5dc7fa873fea8acb579","installdate":6698,"max_pv":"3084","pf":"81713815-9ec0-4977-bf1a-a6eb00faf752","pv":"3087"},"khaoiebndkojlmppeemjhbpbandiljpe":{"cohort":"1:cux:","cohortname":"Auto","dlrc":6893,"fp":"1.03c55f4f45c2a62c467f5bcf7e3fbee31e9ec27abeccefed06adc927cdf52968","installdate":6698,"max_pv":"67","pf":"5e916fb3-0c50-4bc9-8727-69c8e8a32420","pv":"144.0.7512.1"},"kiabhabjdbkjdpjbpigfodbdjmbglcoo":{"cohort":"1:v3l:","cohortname":"Auto","dlrc":6893,"fp":"1.caf22da361a099ee7f504cfd6018872cff61e16946d0a5a57fb07c529bfa8072","installdate":6698,"max_pv":"2025.8.25.1","pf":"e82e1687-3d9a-4d6e-96c2-13d88c1bee5d","pv":"2025.9.29.1"},"laoigpblnllgcgjnjnllmfolckpjlhki":{"cohort":"1:10zr:","cohortname":"Auto","dlrc":6893,"fp":"1.e444ba601ac72b669514e6788bc458ac6b4a3f4400f18ad9b297a84eb27009e5","installdate":6698,"max_pv":"0.0.0.0","pf":"067f0479-07a3-4935-a364-16188c4728b6","pv":"1.0.7.1744928549"},"llkgjffcdpffmhiakmfcdcblohccpfmo":{"cohort":"1::","cohortname":"","dlrc":6893,"fp":"1.ee4b855eb4e00f150fe268baead4f478bf3f5a6b9b8b89026d71e09c368876f8","installdate":6698,"max_pv":"1.0.0.17","pf":"3bf924cd-5a23-4368-a0f6-c2798b8ca1ab","pv":"1.0.0.18"},"lmelglejhemejginpboagddgdfbepgmp":{"cohort":"1:lwl:","cohortname":"Auto","dlrc":6893,"fp":"1.abd78817d137324a488d33f2be78c1bcce014f22371fa586085f59d53d0290e8","installdate":6698,"max_pv":"544","pf":"38cfbe94-d634-4ebb-88be-c04632a21e2a","pv":"572"},"mfhmdacoffpmifoibamicehhklffanao":{"cohort":"1:1ge3:3ba9@0.5","cohortname":"Stable","dlrc":6893,"fp":"1.10670cf2529aaff93f703c397dc379c450546c4d2669c5d364dd6ceba8ac3f63","installdate":6698,"max_pv":"140.3","pf":"544d5b81-6ea5-4ddb-99fd-db814fc3ceb4","pv":"140.7"},"niikhdgajlphfehepabhhblakbdgeefj":{"cohort":"1:1uh3:","cohortname":"Auto Main Cohort.","dlrc":6893,"fp":"1.38c89b12bb20a8f2751c9c7cd2e31c173a47af08c115e1ecccc2f5151a2cf2c6","installdate":6698,"max_pv":"0.0.0.0","pf":"422f1b30-f6c9-498a-aafe-639ee7c12083","pv":"2025.6.16.0"},"obedbbhbpmojnkanicioggnmelmoomoc":{"cohort":"1:s6f:","cohortname":"Auto","dlrc":6893,"fp":"1.f0fac1ffee516ccd1505ec8a51acfa6d9c4fca45d78de2059eceaf3dde376216","installdate":6698,"max_pv":"0.0.0.0","pf":"318f3471-81cc-4382-8530-36f42536f2a0","pv":"20250629.778704241.14"},"oimompecagnajdejgnnjijobebaeigek":{"cohort":"1:2qw3:","cohortname":"Auto","dlrc":6893,"fp":"1.a6af95a209b2e652ed6766804b9b8ad6b6a68f2c610b8f14713cd40df0d62bf9","installdate":6698,"max_pv":"0.0.0.0","pf":"14f6ab06-4957-454d-9246-f59c6a5bf58f","pv":"4.10.2891.0"},"ojhpjlocmbogdgmfpkhlaaeamibhnphh":{"cohort":"1:w0x:","cohortname":"All users","dlrc":6893,"fp":"1.545666a4efd056351597bb386aea1368105ededc976ed5650d8682daab9f37ff","installdate":6698,"max_pv":"0.0.0.0","pf":"e6c63597-42ec-41d4-9c0c-f5cfdd222b7b","pv":"3"}}},"user_experience_metrics":{"limited_entropy_randomization_source":"347548CD8B793C0E43E5BEDEBC391338","low_entropy_source3":5052,"machine_id":5183260,"pseudo_low_entropy_source":2545,"session_id":340,"stability":{"browser_last_live_timestamp":"13407752172999232","exited_cleanly":true,"stats_buildtime":"1736279424","stats_version":"132.0.6834.83-64-devel","system_crash_count":0}},"variations_crash_streak":386,"variations_google_groups":{"Default":[]},"variations_limited_entropy_synthetic_trial_seed_v2":"48","was":{"restarted":false}} \ No newline at end of file +{"accessibility":{"captions":{"soda_registered_language_packs":["en-US"]},"screen_ai":{"last_used_time":"13411376390570760"}},"autofill":{"ablation_seed":"TALAAWR3U5o=","states_data_dir":"C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\library\\jcef\\cache\\AutofillStates\\2025.6.13.84507"},"background_tracing":{"session_state":{"privacy_filter":false,"state":0}},"breadcrumbs":{"enabled":false,"enabled_time":"13410596638238299"},"browser":{"shortcut_migration_version":"132.0.6834.83"},"chrome_labs_activation_threshold":44,"hardware_acceleration_mode_previous":true,"legacy":{"profile":{"name":{"migrated":true}}},"local":{"password_hash_data_list":[]},"management":{"platform":{"azure_active_directory":0,"enterprise_mdm_win":0}},"optimization_guide":{"model_store_metadata":{},"on_device":{"last_version":"132.0.6834.83","model_crash_count":0,"performance_class":7}},"origin_trials":{"disabled_features":["CanvasTextNg","WebAssemblyCustomDescriptors"]},"os_crypt":{"audit_enabled":true,"encrypted_key":"RFBBUEkBAAAA0Iyd3wEV0RGMegDAT8KX6wEAAADBwqsP1QzrT6MWcdZVJfu5EAAAABIAAABDAGgAcgBvAG0AaQB1AG0AAAAQZgAAAAEAACAAAACB1ryRyzwij1C09/fW7Nq6xYWNYBouyiCtBQVy/dm3CQAAAAAOgAAAAAIAACAAAADbFABSUOggpWGtdyt8gjY1U0KOsQGeTko2gmAW90ZayTAAAADFq9PcgOxeNssHaxgGBueXV4hvHQJJpmfEMZ+q1Evd16SVsDQI/5ryEswRmAd6zwNAAAAAOuqKXbFa23SsXpgg7Pl2JS8o587xUaAWaRKpBdtanJxeqCeHs86s5Xb7UUFCatlPZCByX+JyuenI90CW2C8Ncw=="},"policy":{"last_statistics_update":"13411376389687513"},"privacy_budget":{"meta_experiment_activation_salt":0.8784738491452397},"profile":{"info_cache":{"Default":{"active_time":1763794268.852173,"avatar_icon":"chrome://theme/IDR_PROFILE_AVATAR_26","background_apps":false,"force_signin_profile_locked":false,"gaia_given_name":"","gaia_id":"","gaia_name":"","hosted_domain":"","is_consented_primary_account":false,"is_ephemeral":false,"is_using_default_avatar":true,"is_using_default_name":true,"managed_user_id":"","metrics_bucket_index":1,"name":"Person 1","signin.with_credential_provider":false,"user_name":""}},"last_active_profiles":["Default"],"metrics":{"next_bucket_index":2},"profile_counts_reported":"13411376389699211","profiles_order":["Default"],"show_picker_on_startup":false},"profile_network_context_service":{"http_cache_finch_experiment_groups":"None None None None"},"session_id_generator_last_value":"1067275048","signin":{"active_accounts_last_emitted":"13411376389481478"},"subresource_filter":{"ruleset_version":{"checksum":1588750443,"content":"9.64.0","format":36}},"tab_stats":{"discards_external":0,"discards_frozen":0,"discards_proactive":0,"discards_suggested":0,"discards_urgent":0,"last_daily_sample":"13411376389632063","max_tabs_per_window":1,"reloads_external":0,"reloads_frozen":0,"reloads_proactive":0,"reloads_suggested":0,"reloads_urgent":0,"total_tab_count_max":1,"window_count_max":1},"tpcd":{"metadata":{"cohorts":{"+85uj8UpFJFs1LbZzRODD1aQ+Vs=":2,"+EvRah+wIaVJthrhxHGvfjZWQqY=":2,"+Goy06x/MCwrTV/aHU6CfXEkvHs=":2,"+OlMW5y2ANwBFsH03kShVXYVYM4=":2,"+WavPWcVf6qGVorrutx5lkDvL8g=":2,"+exM1B26jXxhR2Ux05ie/WWp0x0=":2,"+mNvpfM3JkKTeK+6ohl+LXstAC8=":2,"+qkOunaBVbv6XoaIwvMn3m6HluM=":2,"+tsbvVZgVIUs6CaBR9z7zuZH70o=":2,"+xOc8Z8Bc8iWT3jhs1SRha/IbDM=":2,"/AzXcP7UuUNwY1auU8IKM+kO+4A=":2,"/Bvm/Rrh3ZMwqH+/+4QOIcUQPPk=":2,"/FeZnUMHLubMD1MVDBjadEPAlVo=":2,"/HAmLxXpHT88v9y7xE9hHTkgIvM=":2,"/Qdp4MAUrNtjqZV9mfb20WAMugw=":2,"/QxFmTaKVmgoQI7u32pqfP+71Bc=":2,"/aGCDNLRew1LLZH+59lHCuAYNdk=":2,"/hbF3j5JOvAak7vNsAbK79bOP34=":2,"/llw2C0PMltsatGnpTHqrkbluYM=":2,"/oHhyW5YAQhdBpgYcbm1vJyiP+Q=":2,"/rtQf7RE4vMc92KjutC8LkjUZgQ=":2,"/xVDyosmqM6bfIMKyDuRAUI1Qyo=":2,"0Eq8eriICMngC2bt8vmV6V5tJCc=":2,"0OWhcqeF92w5b13FI7cuw0wYOiM=":2,"0b9gyhS7XLqmkmOe5OQuD409YLc=":2,"0gnBMXopl7lReGu+XSk/UzZakLk=":2,"0o2/D9RwYjAcr69AeW+JJDm3uHQ=":2,"0y3W2Bn1Kfxh/5CIvP2vZz5fwTE=":2,"164zkQ1BuqOVnZxlkMScGNXYC8Y=":2,"190T6zIzSL3PTH7lqquaA8KAqVg=":2,"1KHE7JEW3MsvkGFH8et2wPovDqk=":2,"1MfDQw3vHCQqaDa7CJ05y8CkuJs=":2,"1XfEZ7+2f5d1GWbkN5KoDjaQBL8=":2,"1v+25/b3OROEvLwpc+58pTQHvUQ=":2,"24O8PXrKNoFD7n9OiIn/kPM8q1E=":2,"26KadWj8dsla9qY4pbLWwc3WCQY=":2,"2EnXui3e9m8cVkso31bGopul3g0=":2,"2PGjwY0nYdnDLk59gOOl/h012PY=":2,"2W5F+NooL0WF8r0ykkHsqw75NVM=":2,"2ks61ETyiwJVBKjWdeLi+jBzvHw=":2,"2ogzRk2u3FpQekT714FkK0vkFy8=":2,"3IgtLbRDI7pm77T7imadPrRxR5E=":2,"3LIGdlgJ5Rw05ZwZulvKp0JajOw=":2,"3UWlaBDJKI5jNgpXTezvx7uZ7L8=":2,"3dFQSDfXS18fA563uc5mxhMXjLk=":2,"3iGyG3EVKY4vxn8RarUIggZVVkY=":2,"3pCe5f7oscOC94pJnWvhlszLhr0=":2,"3qs39ewgIOr/6zygzv+0ltu0SjE=":2,"3vvP54USUtSZlm/osFRbdXjCN/U=":2,"4+yW/l/2EqzM0fGVj0BcXV5TqDI=":2,"44pYIrJiSE08LkH770bq0wwoNEA=":2,"4TwK5fjrPUvueMXp4HZKGWvCJ0M=":2,"4ZC+GtEmLmHToJys2q2fbofqXeE=":2,"4eXKnzRS6ifFncgprkihsauqdGE=":2,"4qJ4K07ijGN6GmvmTNYzTJYmtRw=":2,"4rqrzEd2r2LoSpwBy3Wd1v4Drtc=":2,"4tQsYPAXpmciDFIC4nUACa9X4Wo=":2,"4vXUVM3Beyj2Rn+4lrrMuX455kY=":2,"55tALpi6iGTNuAMJkIQHrV/h8Zw=":2,"5Mo2uOqOAgDMAFR9xJxpj+Ps9vw=":2,"5Roy+ahYhL8V+JUUKT4pljRssYk=":2,"5UJk8zfVHpYxYI3tEGAQfMrks6k=":2,"6/9fhC6bkvQ+GDnCZzKV5b1uEs4=":2,"6218Tv51tHCkvM+pj15cmmrEp44=":2,"6AuvpHbKzPKvnQ5Iq4ZnJrH7VJc=":2,"6DjDzfLMbfYOmlGOJVAk9WK0yxs=":2,"6Edcp8jwSlxhClh0ZABA7VHryeY=":2,"6HE+xAlLmyPiZz4+IOE6QJR2+qU=":2,"6MJKJThHrZ77JxHeDkpqjqsJmPc=":2,"6S99zH9nPCcQM3Z3Z1tFjU0qI/8=":2,"6WtNqNcKg7znf4yFj/CmTJII+Ik=":2,"6iyZ50rVcsj89+phE3AIAQJ5Rl8=":2,"6sUp2EUK4KkR37GrEyH5Yoqc+Vg=":2,"74ScHGU1kOsGMCDn6+SMbExuJ0Y=":2,"74kVp2Qz/ClJ+826v36FnrVlEmU=":2,"7G9aQ3bdYQZRR5Xfs9AsSwsaPRs=":2,"7JjfkitOjBaGC/olVMv7P7VXegA=":2,"7Q3tTd5EeObvMV4js0wywUGWCDo=":2,"7ZvG0q9vmUTKE/kR/8lnxgr+W9g=":2,"7trRGs2suPfs2j1Fw8il8Ct2I4w=":2,"7ypMXoaWrM++zdAbjUUcvDdl6DA=":2,"84VFR/BgNC855g31gNI+lcgcvK0=":2,"8EArI4IQxFb1Jy5KKpgqxnjYqU8=":2,"8WDNGSYrBtXaSuNuCUcuENOqzbE=":2,"8jVuTrHMGXgJ62nUXx9V7cN/mzM=":2,"8nS7YyD+ru/R67lIeZKe30RKet8=":2,"8tt9aAK5Lm7Zcy9hZvUy06bgxCU=":2,"8ur3KjsVLO0lz7bfhzH5Rz06+fM=":2,"9J2Hjgd0WomJNXCVXUnnFp7HblY=":2,"9MZYN7ZW722NmXNr0I0a6xNGHw8=":2,"9PrZjFg2RssHXVdygP2FMo63tHk=":2,"9UuNM5QRWcp6mYkcu/t/I7FADLw=":2,"9V5BfQG4ZefovfNaXdHkf3YSIbM=":2,"9fj5AS7ShECezTEbos0drF7hGYo=":2,"9kQgpCCVk03s/PvG6sFZp1HUjFU=":2,"9rJ/kzjFOsSGSnyRLXimccFODIk=":2,"A24Vd0mboNwTK86qjAnL5NPT0ew=":2,"A7mJev6l+9dO4av3/tE3zj9JzLY=":2,"ANAAVt1I64nz8zJraV4+sB3bn5M=":2,"AgtaUdyyXGWAAMwjRWqwR8Ni8fk=":2,"AsgodONRHQuPbpgDwflsruAzPns=":2,"BJMwY+kVmXIH7sXwmXK/Q13fPkY=":2,"BVHoUpngEWtu0oY2qTH5s35an88=":2,"Bg6wgMZDYx0+/TxkfcfpqINBID0=":2,"BhE51h7DPwhQMQULRUIC2yvAK0Y=":2,"Bpex3OdFL9MLPykjDHF83THK4XU=":2,"Bxd9P1pUJldfaa0T/bjJ4RryaWE=":2,"C3WnzDhbWkxjLSTmT0Q4X+C5Y0g=":2,"CAPrE/fqgLuDiAW9p2hVJvKDVow=":2,"CBUdQZWRoFXyEsfV+uWNQuCMeFA=":2,"CFfzn2HEP0aH/l6Ix4YZ61KhVaM=":2,"CHJAmkWmDHuhYsu3pfxXnIh7HZQ=":2,"COZfIR9QIUwbad3hWuzoXMpnAN8=":2,"CQDGAq5GGMwFpHfW1z5j8CT63sM=":2,"Cjr9Pmookm1kE1NrmqrajTbbNGQ=":2,"CkEWJOcxrLmlVHE98ZW0fCC7cLo=":2,"CpdpCv4T0/puu4pnh4QKOoEMmAY=":2,"CvqZCPmZan5z33NXlrLvk5mzYrc=":2,"Cvywmg5xUNWlVmE1IlOnnaftKTc=":2,"Cz5dAHQlqdvtLvLtHPDUZiiiu+8=":2,"D7m1f2X1ZclMkgJz6eoxMvjV/BY=":2,"DBJzFXqFvKfiVCuXYUaENduUW+4=":2,"DK9He0ANXehmcN8YZhoASlkBlBo=":2,"DjMDHHpf+nnOnHxm/T1q8y5AoGs=":2,"DptldxPMxrOcxrsoRizh9K9nzEs=":2,"E+JAai/R1FoHjUcPV6PvZ+pFENY=":2,"E6/wFsja05ZZziyq3W/qhIfGjWM=":2,"E6h3qgjEj/9/yT6FUGw6YpUCrEU=":2,"E90biI3zHl/mrbRJIr35sUO1G2A=":2,"ECSWj1Bohkn3n1AWHIMEKlbepxQ=":2,"ELx9iaWnrWA5GpWryrvIbwgayb0=":2,"EYpM3f28E5iXkICbU1lbq13/xDs=":2,"EcH6B6F0qgOh+aDUV+DrOVR8Ag0=":2,"EdIuR7LPx1E9lRrlGHXSFSHL2zo=":2,"EkweGdn7Hj2CcJIpLShtXcn4Z5U=":2,"ErqMWkx4Pxb7qmvS+z+hDqzHXtM=":2,"Ew5jcdiDb1RN24kn4qz0nvhVBRg=":2,"Ewtt2SfL5QqYenyxIDEkC9k3tXs=":2,"F+TX3oARl/flaR0nHt5Js6PSCF0=":2,"FBC4lg/vz1H0FxdIxuTqNU4ZEB8=":2,"FJGJZYWxDCoJrYWPy2BUgtq1HM0=":2,"FRQtGBzKKPjy+HEtL7DsX3Felx0=":2,"FcelBmyYBLBRw/HaUwB9s6j5p3Q=":2,"FmvG9vz/LOjYYeU35txjwm9nZRQ=":2,"Fo02W4qlJUivZslOW61nZcCyA9I=":2,"FxMDzlEZgYp2JToAhAg+/yUllRU=":2,"G37v4qWLagnhBfOYr/ow2BEX8wQ=":2,"G3w71gAwbEQRQHACN74TIHrskus=":2,"GA+aolhIHd7aBXICZM5+0OrHfac=":2,"GBKfC442lwAxRkYbckvrizRO9s0=":2,"GVXlVv4EGFdKXDT5DPw/i710C9c=":2,"GWb9dIET7NeLkFZHxmz/DyBcwvw=":2,"GYhBRDeJahYz7i3JJ/8IyE2P5Oc=":2,"GgzZdRVruTyuSjP6N/LfEPt+Rw8=":2,"Gpss+UglPt1UUU2jy1ZlC2Haaos=":2,"GtCeVEmWQHdw3rryz0AuH5gyJxM=":2,"H3GqtTSpNlUUna7Umw0oI+0NgR4=":2,"H4j81ysGT6UYvop5kplp6lxlqXg=":2,"HABGlXq1BaOVH1Ifx+TyX6oI1c8=":2,"HKs5tNwpPQnqsBWBwrTC+hZGTTk=":2,"HM/pWnlnNgRgj3BUP0fYxxzl73Q=":2,"HPKcn8DkK6LusgLP9nDxVh2uJC8=":2,"HSh4Zm3wQfJIatvBZlrOfUeOfwk=":2,"HeMPvC/blr02FSRJtCharxgmzco=":2,"HmcCaa4SwSvvXXelNnwnv7AJeY8=":2,"Ht53X4lOUdtiGjATCg1fkJrokAU=":2,"HwkleqQc/sC85c17L6GdLZZRy14=":2,"I+7UKSXKSzGBgefFdkILZwsI9bs=":2,"I/ZGfrelShQUfRY9aoFBE2Ey1es=":2,"I5BwwRq++KWQv4ptZSLHGgVymIs=":2,"I9HlcZVHx3L832KLRSQTyyKcszM=":2,"IR5VBOOYDolqu3h+57TkJv62y4Y=":2,"IVYz/Wpt7sibxI0sN4+ORgouym4=":2,"IXYNhiWHet2dJvLpHZUkTrdX3T8=":2,"IYpFc21SFnpXN7O5VIPFH6jzdDU=":2,"IZM2fgogPf28F3qsGfathZESrto=":2,"IbS6gvfuP5iBCntJRl7/kGFWfU0=":2,"IpkxtRCHzfxeCqhGtGlgTZho5PA=":2,"Is6xz79EUUUlkrq+vTsITATIXy0=":2,"IvBIWWt8SMIZYO4zi8CNRQBdMa4=":2,"J0sdM/l3EGX6Frv/rSg7j7GL01Y=":2,"J6lcmiUnipJu3gVcoFiUbu+lbDs=":2,"JCDhMYyv6cBx12AS4NeS46EYq1U=":2,"JNVlxkqEWTvStjmY4qJmC71fnzo=":2,"JTODY14/ed4R5V5Q07Wyq1nHZCs=":2,"JWymogHaDHPgCtgoLPRcgF77J7g=":2,"JX1E7bBowYq/NXrg6uP+3EJOu1E=":2,"JgkRGWnXEHJQ70T82y1m8BrqX3Q=":2,"JslOsSCDdI34ClPlNXHA44C8BdM=":2,"K/m4A4gm9l73A6cVZkwC06VtJOU=":2,"K0pey71AM2vyu7pnYfphijaZFQQ=":2,"KCVNDq0FuwrUaqe8KovMG7Uj3F4=":2,"KCmDv40c4KHL+sZ06jr0iETCE/o=":2,"KHAia59NLQnyXDqGRHyg0ZiaTFA=":2,"KJyIq4mOXhu24VxX9gSFD3D93v4=":2,"KfKkkVLOQcHL8t9vHg9yVAoakwQ=":2,"KixfNZ8p0zlprYxunHrPzxobaq4=":2,"KmZM9Qj7kdUlBfwHt3Ha5IOIaXo=":2,"KnVtOxTqcw8Bj0cILIShIFWDZRU=":2,"Koq5VrYu1jgSuPxzC8JneftbXrY=":2,"KqtJUSyT2ifVLtE+YTa+Jwrew0k=":2,"L/1K37AEimYFLDPtWP00QHs4Y68=":2,"L2SILwi57slYAS17LPKLyjzn//M=":2,"L67fEFjVgKvKrPk8WOq+ypaV0dc=":2,"L8Sb2X7fKyM5N332D0ndTxRV6UY=":2,"LBcANp6Rge+D7JyH+lPItmNHsqw=":2,"LH6IDw2lqWicdgcu+tSQmhjaVvs=":2,"LI77XnWaUgy77p5DAeqIO7vOH14=":2,"LIcUrXtcBWBDTsYmK/hSjTpkhOQ=":2,"LK/nTUZLp4wQL8LSp6SlGXML0Xo=":2,"LLWfG5BXDbqHYtiETKDto5KENC0=":2,"LPP/dWFPHE2kpSUwpzspR7jegbE=":2,"LW/7lCwmMHUj5quQrOys8yKgpSU=":2,"LYaNdeviHa0JUthz/IPOwEOXmQQ=":2,"LZMdcjkdapf2PBM+TcQgrrw5l1Q=":2,"LadD1LUTKp90k3P8uJv25vGYSHI=":2,"LcbdMwrGmghZm+QEi615YhcnzP0=":2,"LhU39BVBhzq0HvhANd7D6dP5Qu4=":2,"LtHcu/ZmzB4KTac2VXn1G9F2+yE=":2,"LxPYBWzEULXwJn7iyMSa8QtD+kg=":2,"M+ZG7S72MQJCJe2aQVcvZoKftWA=":2,"M/Wjwu0AfUQ2o20egq9z+7bIzRE=":2,"MVHNDtRF1gJXlUK/+UZ6MIq+cMk=":2,"MXK6lMxDUXU9R5KLAL2bNOMx//A=":2,"MYnF7KiFcThaEWDO9xNzhikWzZg=":2,"McPp8MRX+uUUktsdxYDRi8o+eos=":2,"MftGgIb+TIwSyHnx7apoYs9NrDA=":2,"MpWCvxXFEf9daTeLjHcm3R/E81U=":2,"MqHL/cxomXHa8ev3atB93jJzbZI=":2,"MuDS3URWGZcyPBilzs4FXRzmboc=":2,"MuuNU16haFees5FcNMYXYToRZfY=":2,"N8NipVw4J3jV3lwC90mjPwfCHxg=":2,"NEHnjJf0uubHBmHAJBilzidYpks=":2,"NF8b22VZThqOOVOFtwz90G+TnlE=":2,"NMjxROmwGnztdYpQh/UAc4Bbnr4=":2,"NQamteBltpv0Ps+H619TiFUCf+I=":2,"NdXqc2xTrq/FN6tgl0gsTiq3F38=":2,"Ne1UYIth2fIOE+GqWmLouOzVGoM=":2,"NeSyTyiMagGROQJlNI8QSaSlBSw=":2,"NgAzcAy15WMJsY2pkT/2GxdgG04=":2,"NmMsYpAfxlJVp0FWodzxuSiHS3c=":2,"O/ynEwzhifwXixFynPqJ/W/oWh4=":2,"O0wSnPIMZPh/STNUh0vac3hUGJA=":2,"OEU7USrAsrnhG8bqMiZ26hK2CNg=":2,"OGH8Nmp74ZiT2sjux1xx41S2tNw=":2,"OJcSYTQfOFc27T/8rITzt7968R8=":2,"OQmSZcXWlR6aMwil6XEKlWcjacg=":2,"OVBSN2PMsKlMAlmaAKYB1cRcoY4=":2,"Obd7ogklY6JivNJCQIXV8d0qDuk=":2,"OjHuY9k7IqD58ta4pJplHxor6YM=":2,"OrjPTpbv6b9JNjns2OKkVTiKM+s=":2,"Ow215V5uWo11K+h3r5uqPKLzJI0=":2,"OzlQr4k8StWsbx6xo25olxpqFPo=":2,"P20wwKcWg8wwuTQl2+Brvgsvt0I=":2,"P2fxs0FUJWvTtwxgQ60U8ShnO10=":2,"PDaqV454hbqksZYGhTh5MEKnTws=":2,"PP/b+e8PPjUMQYS2OBT8GhMPS1o=":2,"PZwAWgz5MHGCT1WnkwTC53E/m9M=":2,"Pmvf6keEdJ2RdJbIEPbC6yjlB2I=":2,"Pn/ePL4HFaa4hTTOIC1z+UcbhSE=":2,"Q7VBdSOn3tXuYecIipApfUrWc0E=":2,"QANgRaF/b2zkl6ZtfzavHjFDGww=":2,"QGhV7+yJFgHnsLlp61izzFLm+8M=":2,"QKSyrWjQ6MjhtW2FNppRoKVNRCY=":2,"Qbrqdt73OY7jzL0r98xuGkILcf0=":2,"QtCZzUY6hCGEqCUTc2M5HNrrs4w=":2,"QuBiJAmt3+xnOmt838WFWkZNBII=":2,"Quz0fwq2iFeVentUcxv7EtGXBgI=":2,"Qy0HTOBuuQRuxmyN6GCjTBI+2MQ=":2,"R7CcmUEwytA73udabElrP9G8sN0=":2,"RF2lGGs4R20QEkXEifuLc5MTiy4=":2,"RIrVY4vPSyEJqz7rQNux/M6K2Uc=":2,"RNUHLVNAftYYrVsfw0XdkUFumwo=":2,"RSTkS8lWrQGjrgVquWcQVopYcRc=":2,"RUj0ztXJ7+kOsCpP9Kv3TDeFJJg=":2,"RVzwez9xPSX1AEn5pHSL/PR1Ak0=":2,"RYWzXqC3fQdwkaxnwdmOPmZixUw=":2,"RbcSJm/cbTD27QN7lN6Us62QIlE=":2,"RnwhHERLjD4kuXuJm44mHsUem+Y=":2,"RpqgmmdI4JgMujDXyfPAuYlQsNc=":2,"RqXHWd3nIsw7tt+RmTWynHdd0X4=":2,"S3CW+p7BtwcbD0fgDCiZ0RAQyjE=":2,"SAo2aVtafLNYHW7zVkEhRT9bh2Y=":2,"SY+bhxGSSGCnz1kQKI5yVUmhEfE=":2,"SYm5CVFkFOVllamvQ9D/tRM1JDw=":2,"SdL3nSP2tifv3D4axuGNQnI2bUc=":2,"Sdit/gOF9Dasz7o9sp6F7f95VxU=":2,"Sf6QB4b+AtQzltHOGfemdKTv/FI=":2,"So1TyGdA4U1tMl43UysxLdrBD+8=":2,"SvBLShco9LDUjRwg1aaiMvtvTFo=":2,"T/wIOHUG39AOmrfsXhUAzuEQY68=":2,"T2+of555wmTbJ2TrkfFXZPtJe6w=":2,"T9Qe0SNV0OBiGFU2oks1F78khLA=":2,"TMo985XELW9v74mmn50qi7dfmbc=":2,"TXp5FPH1q8BqEe/vPr2XzQNN4DQ=":2,"TYN2QA74YpLLdgx/KIWyDC7yWrs=":2,"TmrP6vdRPLfVW5N55bGHKWuqKxY=":2,"ToVZFnRaRPFc+bC+kUfL0o6oVbY=":2,"Twx+PUyhAazRa7zunJLUk7AuLcc=":2,"U/HvDF5lgUDIOvDbP9v5BmirEUs=":2,"U/MXSpdHG3Qh2p/vzyc0aFq/U94=":2,"U3KMQW6Rs95g1UJIi3OsZRqYWvQ=":2,"U7ti2JIQ2rB+nGUoJfrARNYcm/o=":2,"U9A/mkuLQvuMuaD1/0NbkxKJwsk=":2,"UDzrIJUrsqeKvuc/bTIuZnU0+5Q=":2,"UHJJhRN9z3qlaau2hbL1mfcfrI4=":2,"USKrPvDKw/JS/mQnPgXXm0PjWhI=":2,"Ucs6z5K6yxsQzCuxBg8IhFUW1uY=":2,"UyrsycnE0Y34SKsZr4aPMI19T4Q=":2,"V2W3L8FR9XTVZQtEl9UZ76GRaOk=":2,"VC/PaSikiazeBowkWU8F8s8Fbdc=":2,"VXhkGMKyQv4EGmsqXOlEmAdtX8E=":2,"Vus+nTDrUYcfuhZkTwWq8pp72Fo=":2,"W/vJPSCn52d0z02T4zSZuXmUFIo=":2,"W3JXUQpisayYUb8fvciX7mz/LUw=":2,"W79Q1UtfGoRJvjuDwvvCFd/g0g8=":2,"WGcOskzornIFeV5Wbec+z/7T8yQ=":2,"WN6w7LqpMGoL89o4ulIxTcXAttM=":2,"WirG4pLCvHATRD/XepELhtbx2z4=":2,"WybscQ6r1DfHRHCfANqlzsLEfR4=":2,"X+IBU7yum6s8R9EIK1eZ5xNXHzY=":2,"X1hwqKxZESTzs9BvFVN1cudNbU0=":2,"X3CsotjCGLmix01VOhQnaVzerc4=":2,"X3r0cKrB50GCupilXtIT0OsNmNU=":2,"X88GhHdCWKsBm24R727HFAkDr7U=":2,"XIcpBEZDocLvzctDOSolZeZZGMM=":2,"XJGnm7SMThSxDgLYX1WCQCpXIeE=":2,"XXS4Q2MvRlQ3g/5H4ppGQKiDMuc=":2,"XdRdTTf2L82I/5T7+QKhT3Pho24=":2,"XiqKy7gubyO5rqh2hQCzWLmuRP0=":2,"XkVTCFQo/kf96t12VPlUHI7Bsoo=":2,"XlU2doslDe9k2Sjyz+HoF+s4Fuk=":2,"Xmi6obAjhT4C07AkOLr1DrZOYng=":2,"Y+3yeiQnPoLWrymZUS7uiptfvWE=":2,"Y9F1acusJNtR1MKQ9sV/LUbtLcU=":2,"YCITb6CU1HEkdv0e/aMbXU15Bsc=":2,"YCrDNn1PepBzFGwS4liz7EGhd20=":2,"YQX9fwmNvbp79I5BVuG+xSFVcjM=":2,"YXSX9V1CWZmwRJWSO+196koeC7Y=":2,"YeDH3FcQ76eu+6wKfeDV3Z75z4k=":2,"Yv9p2UhdpPR9HiQAehTqepmaOtk=":2,"Z1cXRToCPBewKDIjZhu01gzNgvM=":2,"ZAqPZQWJRYyqIy1vmo7cQCQVpEU=":2,"ZCc7WSp1R56ujdXzRr1nbB6X4PY=":2,"ZIWJDsMLDNK3inlfrSMccHToQv0=":2,"ZN87pxH8AlA0PR/ktFLGAjm8JDc=":2,"ZSyVOd9TPha8GxMzhtgZiF93aZg=":2,"ZcCE53MqfEUAG0OC9vuXsgNygQA=":2,"ZnEawSbVIhbrxvLmTU/51FR3PHc=":2,"Zntf7wQ+SmweAQbnVsys7KRLiCA=":2,"ZtCtW2Sze8YbTG1fS4loW1n3F4s=":2,"ZuT1OewLis1kVKZoBEacnH8c4oQ=":2,"a/06Cc1qCMoW5/jphsMeYfBLXrA=":2,"a13zReUtyPNWrTN/Br6vION4M9k=":2,"aMpjfejGmbvrz24NGwVgyoJWmB0=":2,"aOhZbO9SzuqTcdPglSVnbInJ0HY=":2,"akhZxq2ZypH9U+g8ptVEix2Ys30=":2,"b390+KlW3do0iY5dWxyw/Nvj8y8=":2,"b64LG8t0nZIMAH4frxWe0Xe0lVc=":2,"bAfivuZXv6xQHZCq1H7RdaBGeJs=":2,"bI1Vo/T/gZu6ziJq0A76h0bkQ3A=":2,"cDMRM41OYKodBqf5yPs7PXp7Ibc=":2,"cFGuCTImI1LKaInDPxQtiun7tc4=":2,"cFmOFeeAC2RTc++FepBrbqvZJu8=":2,"cS+k6IBZ28FX5Gu5yS+3rwfash0=":2,"cU2FpWNjt8mGURI0k5QPpMUA1p0=":2,"cVRrJOXl5PBGO3dbZJV8A5XlMQY=":2,"cYEiRDkwdEht5TZ8ftQ2T12/vmI=":2,"cZnefofy4yEnFmkQ0gaP7nfgGBQ=":2,"ccdh7Hta93FtJR+qwt7DElNPqBI=":2,"chKNF84vgaJ1RtQrKV4ytLiKjlk=":2,"ciN5+j5UQseF5/8p+leZThdpwLk=":2,"cnQYSsJdyO+otNbyW42g39tQHFw=":2,"dCGPD+ybLSgoWN7NZsSKWMIMa8g=":2,"dNRX77I/GjMbKJwIPRRuZQnef5k=":2,"dSgRwJW6QXt5Gyti9tvXKOSloVI=":2,"dULO06RXKgWKOnT+2EPWFhzSOzQ=":2,"ddNzLLovIOQAjI1Fuour/azCRPs=":2,"dfVd5Cks1FFJVdNmS+sD4zItmmQ=":2,"dhvEuIu4bREe/yc3n+uWgemDH+Y=":2,"e7QR994kinvEvNi5PEREfEgRBPk=":2,"eIkv3FutAxmGf2Mh7yo0HiDjrls=":2,"ePMMAHx9Ax6ezSppn6dpqbBnLhE=":2,"ex04CvLWFikDWXjGQ4RtjbOeRNM=":2,"f/TYvHakawJF91GiVgpjciGJPc8=":2,"f8cvQ/sraTsg0bwM+aS4D6pFT6I=":2,"f9ysKU6hcNbVfpf1njOmFQ2qbZ0=":2,"fBmcT7XNbLpOQsfKdgdXEdc1P38=":1,"fLj05EfmTLEt58m3jVUhPVS04sU=":2,"fMus4OBg1K5k+k8tLnAZyRBbnLY=":1,"fa4i6qfS5+dDiDVKFZDjsFnF6Rc=":2,"fnRvo3ItSPsvU3LKSXBRXJg0FUs=":2,"foNSnwHq8ph3wPaXJ8I88LehpI4=":2,"fuy5x4yKH5LLTw4kz6c1pnFiOcQ=":2,"g0FiO0sC3nMBLvy49sLKeESA7h0=":2,"gCgo0usJBkT8uf+0XUuS3gWGdoA=":2,"gJJSwVtCLng5e1xxugIzmlnHbd4=":2,"gJK5pMWuNFrD9OGNIgKananYNSo=":2,"gMpTRAKA7Ayl+W/YVqQhr5GU6x0=":2,"gSTTNamgmAMk2//gdb5jcZMN9AQ=":2,"gqGHjniFghep8E6txoAdzX/4h3M=":2,"gr0I/I6o5WZbCX3ANuvNoagEEe8=":2,"gsU6EH/i8w1ThrqsEm6GK79feO4=":2,"gtc5hvQud0vqhMhm2fmcV4S+Agc=":2,"gxn+9RnfotKjIQsWN6Ldd6tJFwQ=":2,"hGJJ6Hh+MF5i2sXu2g4Yz8nZwHs=":2,"hJM03Qy65rIixL+QnwFnZUWdS7o=":2,"hRdDjllKxVjmSDiRpxs+uOUT9Q4=":2,"hl/Ql44a9B+9BQR34dUhENrlIJ4=":2,"hmt01LHgEU30nJb6VMA41XMWRyg=":2,"ho9bP3IJ21F4d3qP3pTJA0JkTj4=":2,"htyr9QaPXQOHGsfVmr+97oeW0UM=":2,"iBD/DGiehe+56IosGVlv3c1wJ3Q=":2,"iD9S1fFa6FpM5DcIk5pQUCSj4aw=":2,"iGzRH+UPc4Ea3ApuY49us3/XCaA=":2,"iHnWlD4n0QHbXoUv3k90wclViuU=":2,"iXvZsH9NpG0qHURLzLTudfP9aeU=":2,"ihQuIV4rmAFYvTr6lwMV4HokREI=":2,"ihRhUKLVahjKDEOmS0BbYyhgv7k=":2,"ipbKvdY2LsQlCcEkuSqe8v4By6g=":2,"ipt6XYj9NCIb0hWN8BbyXEF5DfQ=":2,"irephTXGVO+MmlZ2AS7MbB5AyLM=":2,"j+6DOgEHMZgTIWSHhf9CjmUGIK8=":2,"j+GDvmG+Am9Xd+4Q/XDAlrQrFz4=":2,"jAuqNF/yhbdqVNpoWw+2Jo6e6bs=":2,"jEtGDH1uWCTHnmHc2bGPDrpEHCg=":2,"jODdAGIb3/eRchqP2DHAiTYlP0Y=":2,"jQfCzjp4d/PizsLo5UpdD2f8www=":2,"jUoBtJ03/Xr45tg4Dqenc5cYWAU=":2,"jVgQocUsQaH7V/2UxhLVOidhP20=":2,"jYhk6dm72WAaxSdjIvOSNJ3d0sE=":2,"jaMkW1knsztb/0+GNCKx6G/SzaI=":2,"jc0TPKjiMVneKPNeY2avjLI9KVY=":2,"jq2rGPyUu7grG5FJi/I/qcKLKg0=":2,"k/v8xkMt57fg28L2fh73gxXa4Yc=":2,"k1J17FRo3myPzj+UE+LXaZ9ohSc=":2,"k3bwFkPXH/EDNGF7Npn6kwKJu6Q=":2,"k7hYQMMCyKjXIXP+LR8U+d3GIzQ=":2,"kDJnIwokySTxXp20eRGTskuMM9c=":2,"kLV2LBNc1aIFljjZItvqx2bhY1U=":2,"kMGe+97jR78zimxmtL9Ak8a0OHw=":2,"kOvgd336AvzRZ7zhd6KqxVNyMiU=":2,"kYI0w0yGJWsEW8mUvaWKX5BGN34=":2,"kYvIeNAo8XJrY0sLt9RkQb/ArLk=":2,"kbEmSJ7AT4IyCib5dANoydcLsmE=":2,"kfmukk3rEZbsice63or/akfPSU0=":2,"l1TwOsy32JiZV/bM26UQ8oCnn7k=":2,"l1l9LWMnuHXWDBSLcfQ3Zp7bVVk=":2,"l38tVXmuuGlAgD9a3eXwX/jQwgI=":2,"lHA+dhHLhlHLq/O+0+Xz7buufJ4=":2,"lJcNksg20bY6CgGPqZu5aQHbRhE=":2,"lUbDYyCRhvBzS0nDrz8rx/nq0A0=":2,"lUfzJ3y5Jzs7p4PBHh4xhm+zoG8=":2,"lV/IOzmMkT+d9gXfFgqtsErYe2M=":2,"lY3+bGoDRF7A0eSICXUJ1yfpxo8=":2,"lZpQb3elaMvd1gsI/plZxcpUVwY=":2,"lcXOtK8KVWFVeHE4WLEewHPCWWY=":2,"ldPbP2/DX6N+AldQ7AFtoht/Bvc=":2,"lhONN2tBTBA/tSnMtizicuNBPLU=":2,"lmXhk+G6r/qbCMYZcVdJbnn+93I=":2,"mEj21qtta3LLlAxWkZ8sijZvktM=":2,"mSNPvAXxob4waWjBxs3ziW6z71g=":2,"mYby0t8WAz6jV9RgYIRRGyfeWB8=":2,"mg1zJLHTYaJIMLNxa+rsYWsZJhI=":2,"mh3wxEYkuAk6sRhlr/C5G/gxYRI=":2,"miEzBOZuMem0Cj9Uxw1LjN9S9cw=":2,"mlWAbHBzQ5Td8U9HSp8fcPdPyzQ=":2,"mmci9ejkh1yqEt/tTbqbQeSaSwI=":2,"mqI5UE1zlvwODeiYkE+97gk/N38=":2,"mv8xDlT2/YhQtkLKnptz6SQB5uM=":2,"nAj3Ny++JKnx/3X/T3HRcVSVXN0=":2,"nHcOob6uJ8APh5510nMH7Ikg5XI=":2,"nNG/hSMKDgXudXzByhmJ+8Udww8=":2,"o/9SDeB9XVwuJyTLitMsvbgaQKg=":2,"o1UwUqLfJmuxKSuFfNRz/EzR9So=":2,"oHsvpBtYgeqeqVOdT/DQDeNAyp8=":2,"oIsMwEfYOTvIzVNKHP6nF5RhkVk=":2,"oNPOB+kuiVqZt91Ceva1HD2babU=":2,"oNrKGepVbapve5qoLE4s7JK926I=":2,"oVr3ZmvWmeO5V4lOW6+8gEGE5YY=":2,"oX3Jk2hkzKOTCsCjIb8aBoEQxCY=":2,"oc/SFHoD/b37HnDYvDl1S+Ln8+o=":2,"odROFyqXD/frsFAhnWAK7yW9p1E=":2,"oibx5gMRdevDCHgIZ8xHbhmCom0=":2,"pCDKhit9yDclLI07LYJ2Arec58E=":2,"pFBMDn/qQ2sU3hbTzN0XE+gPNlE=":2,"pGtFao+Eqv0xw/MJ4ne73wMtss0=":2,"pKocQQbERh0k2bBtqqHvsL6IzwY=":2,"pQbhy62y3+Jaimld0fQXsr90MPU=":2,"pSQhqungWlpbyd5qgvoDgc7AE/E=":2,"pXA8CdPQ2YBCgTcuH0u3ji94FpM=":2,"pj/VyHVWYMY781dFmsalRMjAdng=":2,"po8Y5k48QpkNI3OQK3HJSajJvIk=":2,"qGuRw9GZC5DTd5qdb/ri4A76b90=":2,"qX+92itsZYUdfyqVnkRNS1z8pEg=":2,"qhL0XSJvQoCDATRSMO6uHK6s0AE=":2,"qnTJ6qWlEO3mGlvjzZxRj1SWBSg=":2,"qpvlQsYaJOxyL6Vr9sOa7itTjWg=":2,"qtTOojYzivrM05kWyMG+4B3oavI=":2,"qxM7PjtM2REivHU89TUKjRFyX/c=":2,"r2jAg5LKs99/R7UDy7n+RVExthw=":2,"r66x+/C7gcK0ek2UGYaFgDG9W7A=":2,"r7iJmf1gYZfcG8O+Vd5YotXOO98=":2,"r7yFRxkCgT3Oeq05RWA9OtXpSI0=":2,"rBvqswHFQGNJ+GA8LqPPr3KtF+8=":2,"rEMdbHd/v8VQAKMX0knLaZEP8KI=":2,"rFDwZivZT0u0vRe8Vj180HEOHEI=":2,"rPu9e+cSQCdzkKfYpDy9vIMdrTU=":2,"rSlZi3H7e2ESXD48TLSxA+uHjp0=":2,"rVsxB7wqXgKFK40cRaUdv5100/8=":2,"rXmjfschRYJInGVNNv9jGIRrjJ4=":2,"rXtx9FDQjNrYAd+Xt+sv0IjIaJ8=":2,"rc4WJXNzddjdAyW+WERMtKMaYYI=":2,"rjWw31OACJd76/zAEVPT3BCWrpE=":2,"rp1qUhW5AwlfEo+FC5F3v84IJG4=":2,"rzbYuWFx8KRHnLGL8HtL+0dJhIo=":2,"s6btLO7QAG1u99wehXlGkxKUb0Y=":2,"sD67cLr9VrAeoarwQnBVmBOjfCs=":2,"sDK4hQp8T2RmWpWvgnHk6FQ2iwM=":2,"sIl513b2C2/QeDrHSuBpH2c6C0k=":2,"sL1dde8EjkxF+Lb89yeCnsBYBOU=":2,"sNAV48ni/e0b7Gn9jEfM3Q3dVe8=":2,"sPTnbQmrNOyx/qW0Xu4g4cu0aR4=":2,"siHaDi1iEPE3xPgGISfpfwb8h4M=":2,"skXg82RVCi6BZHNRI23RIG2DRC4=":2,"sunLW+vgbWmbUrarV07NcFmnKgE=":2,"t/DNJowu9uHR/kBc3bc1Nm7+9lo=":2,"t97bh7mYz2gwiY6nU+/w1i9dgZU=":2,"tBZZ8SUVG/FjRUpROxHXX3KaCyw=":2,"tK2lpUcycitAF1Et7B+/ZhiQ0ZM=":2,"tPPRnCQb4zC0dD4BJYFC6KAMVWw=":1,"tR2fhHtwBTc9bKHWDB/g0JFDNBY=":2,"tWRGM3CFPPslofcsSqj6vpcd7JU=":2,"uBMu4M5/0KKZY37hxUNLN39LPtA=":2,"uKsvvIzSMOyMR+4LPExkT1A8iDA=":2,"uNmw0kNAuK13LWxLuTZiaO140LY=":2,"uSACpi6t4iSWKjHFN6UHju08OB4=":2,"uf4z/h0h9ZnyOD7ycAiwgx/aHFo=":2,"unvMMzjFrurZix1N1pOtUC+RriY=":2,"utfiG74gl+SIxnKipbPl66ZNVWc=":2,"v6RXBuPtNohU+Lb6MHwV9z4lt1w=":2,"vD6n6Z9JW8prLB0rVlTt/g+4cCI=":2,"vDX9FACK55aBjFOaPdncKX2o+Bg=":2,"vW9hX9bdu7jEnh05U+zjXI+SbA8=":2,"vWbt1X54cCEDXSdUl4qpqdCohNE=":2,"vYmUY3JR7HpU6cV3sp31ubgx+YE=":2,"vqAjPDOmXZGBn833qAWFeo1PtsU=":2,"vqaClXLm/YZ8MhiUAukne10yy/o=":2,"w1vJ0NG9kDFtUNHq8zCMuDUVb8M=":2,"wAsstIPtDtjSlxUGn0gh+TfTWmQ=":2,"wJ4TJKlTMrrNezUg8dfBuOltq4U=":2,"wK4Zb71UwMahGOQFp0E3RYeMfYw=":2,"wZinPjTdOm5rrXI4u1NIKiffmp8=":2,"wib9u5YbRGJAqT5Bvh9zNZp4iTY=":2,"wqO+JoG3Un7fd2bDdvRxfAhbJ3Q=":2,"wr9Z2KN9f3Tq2jo3nTe3DpsNr54=":2,"wv3DmXgFeOrq/dbsherqtSjmrO8=":2,"x4c04qHrWNjtvJ2XgQQpd7wW1rA=":2,"xFo5PPJwZaq7i4dWLyzXeywAOEY=":2,"xUJ/eh98DaHs0DxWLA9fYYD6PzM=":2,"xgEKSqpgWJmo+flxFClV2/NiOJk=":2,"xjHJCBSoTdEQjebMV8aARzFcEkA=":2,"xm3y8sIKteMNAiUYEkt/ocEG7VI=":2,"xoxgp5Cx/yYkyO4yumq70s+D5t0=":2,"xtOFUD8jRLwrgCyGI5QnD7K+CG0=":2,"y0OlqPMUw6B8jvG1d2F9DOvkDp4=":2,"y2Kf0efsIVsF8PYgZBOV7tc5AfM=":2,"y2QRZhLYezQlVyzaDO4PEKbAmAc=":2,"yAXQT+zYHydb1uUhkuwtxm5At5k=":2,"yAyH2ZVkhzgat7fcC+nSDXQ11jw=":2,"yKtZdPSQMfJNQVKjIJ6noDw07mQ=":2,"yM5jN0VjPFKIKpUqRuN5KyRPd94=":2,"yad44gD7FAnezf8DgiIRZiDGlRY=":2,"yekkHNxtLVYK9WvooNiEKVWXabA=":2,"ykSAQyJm33Umehd0Txp/8rpum2I=":2,"yx1gN4z+x0naLVTbhc4/HO1c7cY=":2,"yzJqCQsowhulZe3Hx/xsWOvlTbw=":2,"z6CTfToXHCMt/46aowVDcKOYuL8=":2,"zAmtiHUH7ncF4kcOLIROCCAtn5Y=":2,"zTzCkN1zga1linYQP6v2AyMYW5w=":2,"zd+y/4GyfV5LQFAHFVfIKQuur+U=":2,"zejOvMNeql2wesKjXICcANkzyPM=":2,"zvV3Pm+WpZE4xD79k4mjhrJu0gw=":2,"zwzjvFMamlRnd5MSmg2F0LgPqso=":2,"zydtsLKKSp4EUItk7o34H/+dTNQ=":2}}},"ukm":{"persisted_logs":[]},"uninstall_metrics":{"installation_date2":"1746408261"},"updateclientdata":{"apps":{"eeigpngbgcognadeebkilcpcaedhellh":{"cohort":"1:w59:","cohortname":"Auto","dlrc":6935,"fp":"1.4497d8060d0e53c12b4403aa9ebe7e827d4880bae3f4139a26a4feb7ed64c4a2","installdate":6698,"max_pv":"0.0.0.0","pf":"a7c41bd0-3c83-4e42-901d-169858797d5f","pv":"2025.6.13.84507"},"efniojlnjndmcbiieegkicadnoecjjef":{"cohort":"1:18ql:","cohortname":"Auto Stage3","dlrc":6935,"fp":"1.160aa782aaa39e7b812a5e5e2c7c2f8a56e21d5ea84b92cd1f493cb384b4fdbd","installdate":6698,"max_pv":"1517","pf":"cfb54599-5524-42a6-94a9-4eb611f3e3cc","pv":"1532"},"gcmjkmgdlgnkkcocmoeiminaijmmjnii":{"cohort":"1:bm1:","cohortname":"Stable","dlrc":6935,"fp":"1.ed43e732234cc8b79a137be0a0aab8f72b37ae6ca51caf1292986edbf618c1d1","installdate":6698,"max_pv":"9.62.0","pf":"70407188-dbff-4402-b862-c453dfa9314d","pv":"9.64.0"},"ggkkehgbnfjpeggfpleeakpidbkibbmn":{"cohort":"1:ut9/1a0f:","cohortname":"M108 and Above","dlrc":6935,"fp":"1.4795b6d7d7c2e0878abbb461850eb2068971273643901fd992814b9bfaff202b","installdate":6698,"max_pv":"2025.12.11.120","pf":"1b4be169-15e2-43bc-8bf6-d1d524b6b814","pv":"2025.12.25.121"},"giekcmmlnklenlaomppkphknjmnnpneh":{"cohort":"1:j5l:","cohortname":"Auto","dlrc":6935,"fp":"1.3eb16d6c28b502ac4cfee8f4a148df05f4d93229fa36a71db8b08d06329ff18a","installdate":6698,"max_pv":"0.0.0.0","pf":"7107732a-c529-4ad6-9b5b-328d37f7e207","pv":"7"},"gonpemdgkjcecdgbnaabipppbmgfggbe":{"cohort":"1:z1x:","cohortname":"Auto","dlrc":6935,"fp":"1.56c21927faa028be6ce18c931660eec37e41da4bfbfd47cafa48350f828c0dbd","installdate":6698,"max_pv":"0.0.0.0","pf":"20cde327-dc74-409f-9beb-1f9bf7fdd0ab","pv":"2025.7.24.0"},"hfnkpimlhhgieaddgfemjhofmfblmnib":{"cohort":"1:287f:","cohortname":"Auto full","dlrc":6935,"fp":"1.b8b379861bbe9bedb44834d307c8b3c21d4f55774170c504f18d0ab36bb712ca","installdate":6698,"max_pv":"10215","pf":"687bf388-c31f-48fa-bea6-96ccb0f88c32","pv":"10244"},"jamhcnnkihinmdlkakkaopbjbbcngflc":{"cohort":"1:wvr:","cohortname":"Auto","dlrc":6935,"fp":"1.c52c62a7c50daf7d3f73ec16977cd4b0ea401710807d5dbe3850941dd1b73a70","installdate":6698,"max_pv":"0.0.0.0","pf":"ff9c160a-d447-4bc7-80ab-16f8c90cb229","pv":"120.0.6050.0"},"jflhchccmppkfebkiaminageehmchikm":{"cohort":"1:26yf:","cohortname":"Stable","dlrc":6935,"fp":"1.4a06cfbce6f26c81ec6cba52e84985c0111183e0b06a953b14992b4511bb78af","installdate":6698,"max_pv":"2025.10.5.1","pf":"175712a1-15a0-463c-90d0-04c4d321d8d4","pv":"2025.10.7.1"},"jflookgnkcckhobaglndicnbbgbonegd":{"cohort":"1:s7x:","cohortname":"Auto","dlrc":6935,"fp":"1.220f906777a5ca8a28c0d7bb742912805fb6f93b262abc897f372213fc9841ef","installdate":6698,"max_pv":"3087","pf":"d7718e53-e47c-423a-935d-ab3fcaa74607","pv":"3088"},"khaoiebndkojlmppeemjhbpbandiljpe":{"cohort":"1:cux:","cohortname":"Auto","dlrc":6935,"fp":"1.03c55f4f45c2a62c467f5bcf7e3fbee31e9ec27abeccefed06adc927cdf52968","installdate":6698,"max_pv":"67","pf":"ad1225a1-3ff5-4d2c-a148-d5509cbe32fe","pv":"144.0.7512.1"},"kiabhabjdbkjdpjbpigfodbdjmbglcoo":{"cohort":"1:v3l:","cohortname":"Auto","dlrc":6935,"fp":"1.caf22da361a099ee7f504cfd6018872cff61e16946d0a5a57fb07c529bfa8072","installdate":6698,"max_pv":"2025.8.25.1","pf":"937aa6d2-50a6-42e7-b621-e70874ad8247","pv":"2025.9.29.1"},"laoigpblnllgcgjnjnllmfolckpjlhki":{"cohort":"1:10zr:","cohortname":"Auto","dlrc":6935,"fp":"1.e444ba601ac72b669514e6788bc458ac6b4a3f4400f18ad9b297a84eb27009e5","installdate":6698,"max_pv":"0.0.0.0","pf":"a49039e1-db95-4348-87a1-81ff5be2c18a","pv":"1.0.7.1744928549"},"llkgjffcdpffmhiakmfcdcblohccpfmo":{"cohort":"1::","cohortname":"","dlrc":6935,"fp":"1.2be74d0afadd4c9b2ee33695e1f81fc5ce5dc3016cd8a13cfa0e1f0b571834ea","installdate":6698,"max_pv":"1.0.0.18","pf":"5071b6f0-b254-4497-b7cd-f8cf157ce30d","pv":"1.0.0.19"},"lmelglejhemejginpboagddgdfbepgmp":{"cohort":"1:lwl:","cohortname":"Auto","dlrc":6935,"fp":"1.c1d25adf4d9c55d12a550bfe02e87c267561e95b35079841966ba57bf10a6d9e","installdate":6698,"max_pv":"587","pf":"212b94da-4152-4f6a-b78f-27282f9e1eb4","pv":"595"},"mfhmdacoffpmifoibamicehhklffanao":{"cohort":"1:1ge3:","cohortname":"Stable","dlrc":6935,"fp":"1.a10a891103c69736b97d77dd5fa1ce1ffd77a0c8bc3aea6162df0423933e0755","installdate":6698,"max_pv":"140.10","pf":"d0a1fb9b-e723-4545-bd2e-40993a35724a","pv":"140.12"},"niikhdgajlphfehepabhhblakbdgeefj":{"cohort":"1:1uh3:","cohortname":"Auto Main Cohort.","dlrc":6935,"fp":"1.38c89b12bb20a8f2751c9c7cd2e31c173a47af08c115e1ecccc2f5151a2cf2c6","installdate":6698,"max_pv":"0.0.0.0","pf":"94565b0e-974c-4509-bb20-8c29fd335abd","pv":"2025.6.16.0"},"obedbbhbpmojnkanicioggnmelmoomoc":{"cohort":"1:s6f:3cr3@0.025","cohortname":"Auto","dlrc":6935,"fp":"1.f0fac1ffee516ccd1505ec8a51acfa6d9c4fca45d78de2059eceaf3dde376216","installdate":6698,"max_pv":"0.0.0.0","pf":"9b5ea977-cbb8-4a67-953c-f3ea4b1c4c1c","pv":"20250629.778704241.14"},"oimompecagnajdejgnnjijobebaeigek":{"cohort":"1:3cjr:","cohortname":"Auto","dlrc":6935,"fp":"1.92ac4503d850d61341c2b89b0ca25ae45a875f85e7ab7018354cef9a0b37bba7","installdate":6698,"max_pv":"4.10.2891.0","pf":"697db852-419d-4b11-97da-b1a2a3e3a498","pv":"4.10.2934.0"},"ojhpjlocmbogdgmfpkhlaaeamibhnphh":{"cohort":"1:w0x:","cohortname":"All users","dlrc":6935,"fp":"1.545666a4efd056351597bb386aea1368105ededc976ed5650d8682daab9f37ff","installdate":6698,"max_pv":"0.0.0.0","pf":"e1d81574-5b38-40e0-9816-db9a7ed09aa6","pv":"3"}}},"user_experience_metrics":{"limited_entropy_randomization_source":"347548CD8B793C0E43E5BEDEBC391338","low_entropy_source3":5052,"machine_id":5183260,"pseudo_low_entropy_source":2545,"session_id":538,"stability":{"browser_last_live_timestamp":"13411378254176799","exited_cleanly":true,"stats_buildtime":"1736279424","stats_version":"132.0.6834.83-64-devel","system_crash_count":0}},"variations_crash_streak":594,"variations_google_groups":{"Default":[]},"variations_limited_entropy_synthetic_trial_seed_v2":"48","was":{"restarted":false}} \ No newline at end of file diff --git a/library/jcef/cache/OriginTrials/1.0.0.18/_metadata/verified_contents.json b/library/jcef/cache/OriginTrials/1.0.0.18/_metadata/verified_contents.json deleted file mode 100644 index c84dc72..0000000 --- a/library/jcef/cache/OriginTrials/1.0.0.18/_metadata/verified_contents.json +++ /dev/null @@ -1 +0,0 @@ -[{"description":"treehash per file","signed_content":{"payload":"eyJjb250ZW50X2hhc2hlcyI6W3siYmxvY2tfc2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2IiwiZmlsZXMiOlt7InBhdGgiOiJtYW5pZmVzdC5qc29uIiwicm9vdF9oYXNoIjoieGs1RjVGekZ2WktUWWl5ek8xblpPYkdWLTkyMzNHelZrclNKLUZjOFRFbyJ9XSwiZm9ybWF0IjoidHJlZWhhc2giLCJoYXNoX2Jsb2NrX3NpemUiOjQwOTZ9XSwiaXRlbV9pZCI6Imxsa2dqZmZjZHBmZm1oaWFrbWZjZGNibG9oY2NwZm1vIiwiaXRlbV92ZXJzaW9uIjoiMS4wLjAuMTgiLCJwcm90b2NvbF92ZXJzaW9uIjoxfQ","signatures":[{"header":{"kid":"publisher"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"QRaGELghjHv0t2GYRo6Eu7XbIzH-3hdNOy0iSCD1qEb1Pp4F0d3VbyP5bTgERZ2AzMb3TAvVZMK_sE_ts18EAdXwltdTXYeZHmroN2B_ixdQ-nFYH5KrsvCdmsjjTf19uocT3IlfAIcaf7dhqLX1tPV0pnJ2EXw3Iwb0FJl08MCtab9y1_WUwXKLqt_5aNppkB2X1ytWwgBWOfKFDvuiRi3ri5NwPPSJXeNal0Pr-d0e6mBg6PyM5C2_ZC8jtkDUam8IyxtTynjwy1oG52FD7zuV2091xxDzdRen12zMdxtuGakMsHRgBVYxXJBNJ3Z4DYqHVTCYSKS-3eo9jPUTcQ"},{"header":{"kid":"webstore"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"YQLDrVMllSZ2igdaPuiES_SDUFw5DuzQ-q_f405BYJflKZOSHpisX0ngu1j0_MKJtw2o6kM-LiIgvDodJRARG6hbtnJavtpdo1bxXbKTZxWZ6wg4LsT-9fqYYwbeytHmJ0spSEzu4Mu2zW7fjDHdDh7IxRfj_1JLo2nizsFDz9pFwA-z7w_Rw_az7o2wWJ_m7JA5f4V7cd3nUWe3dbrGVSA0akUSR2J2AcEwBwDZhWESCuyKD1ZlefARS451M3dUOsMUsz0U88wONFMnMQ89jorHLJi3EWB8f_Z83NRSjZ6L3cjrFPl_0MT0Inr8wcEP0E4MEYeswR_jsWSVVou9-g"}]}}] \ No newline at end of file diff --git a/library/jcef/cache/OriginTrials/1.0.0.18/manifest.fingerprint b/library/jcef/cache/OriginTrials/1.0.0.18/manifest.fingerprint deleted file mode 100644 index fdb2279..0000000 --- a/library/jcef/cache/OriginTrials/1.0.0.18/manifest.fingerprint +++ /dev/null @@ -1 +0,0 @@ -1.ee4b855eb4e00f150fe268baead4f478bf3f5a6b9b8b89026d71e09c368876f8 \ No newline at end of file diff --git a/library/jcef/cache/OriginTrials/1.0.0.18/manifest.json b/library/jcef/cache/OriginTrials/1.0.0.18/manifest.json deleted file mode 100644 index cf80078..0000000 --- a/library/jcef/cache/OriginTrials/1.0.0.18/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "description" : "Origin Trials public key updates and disabled features list", - "manifest_version" : 3, - "minimum_chrome_version" : "88", - "name" : "Origin Trials Updates", - "origin-trials" : - { - "disabled-features" : - [ - "CanvasTextNg" - ] - }, - "update_url" : "https://clients2.google.com/service/update2/crx", - "version" : "1.0.0.18" -} \ No newline at end of file diff --git a/library/jcef/cache/ShaderCache/data_1 b/library/jcef/cache/ShaderCache/data_1 index 165a414..6950627 100644 Binary files a/library/jcef/cache/ShaderCache/data_1 and b/library/jcef/cache/ShaderCache/data_1 differ diff --git a/library/jcef/cache/TpcdMetadata/2025.10.5.1/_metadata/verified_contents.json b/library/jcef/cache/TpcdMetadata/2025.10.5.1/_metadata/verified_contents.json deleted file mode 100644 index 5dbd7ca..0000000 --- a/library/jcef/cache/TpcdMetadata/2025.10.5.1/_metadata/verified_contents.json +++ /dev/null @@ -1 +0,0 @@ -[{"description":"treehash per file","signed_content":{"payload":"eyJjb250ZW50X2hhc2hlcyI6W3siYmxvY2tfc2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2IiwiZmlsZXMiOlt7InBhdGgiOiJtYW5pZmVzdC5qc29uIiwicm9vdF9oYXNoIjoiT29iUUNZTVdTSFVnN2IzdjAtSDBXNmVrTEVManlJeGpwOXQ5b1o5d0pNcyJ9LHsicGF0aCI6Im1ldGFkYXRhLnBiIiwicm9vdF9oYXNoIjoicjF3cHgtMy04Q1AzaFR1cTJrRXhDLXJwcDZJWk9iZXJDUXFMdjladVVxOCJ9XSwiZm9ybWF0IjoidHJlZWhhc2giLCJoYXNoX2Jsb2NrX3NpemUiOjQwOTZ9XSwiaXRlbV9pZCI6ImpmbGhjaGNjbXBwa2ZlYmtpYW1pbmFnZWVobWNoaWttIiwiaXRlbV92ZXJzaW9uIjoiMjAyNS4xMC41LjEiLCJwcm90b2NvbF92ZXJzaW9uIjoxfQ","signatures":[{"header":{"kid":"publisher"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"E53SrBTj8Fh4ZE0lHaOqjNsKfWHegDLUCH2fDmNFg7wJqJo-XFqeq480MzuzAMkADwy4abj-ll06YUH0Vy2DJjqOm_0YBe8pLoCmpX_kgGCRNS-GynWOrnXOoDq-r7YdMEJiwqtpz-RhcLB0i3mMI-OdSvX3TFeRMCkHlovJF_FEUVnLZgus3ITP5X8thlzVR6phXshphjmMm3tyWX3yOuFYqvCJ1qaXbVPdIntQ1fe7ShBY8ZX0OZS4acpmNsPnCTVpd4_gEPKPldf5VwblxHu1hdI8c9TGs0l2tlmVDY7am4Bf0S3q4_M7tY5TIyVSdhqtk_H2QJEgcxz6Xe6WQkUhjHQOFSpRTCjGbjA42tf-uieLr2g8mnXqWxUcCoUVRe2Ni-1DYhuQt6fgXdVm4numTnnXhTm8bozjJLLBkfitHC_1tsVVb-Sj5X0KFYedFihlX1PAkZrf7HMmAfpeN_6DiWR1-o5dErTecv1fM5c1MTtYvYRWeUz4pQe6wNfD4KPsl8FWV5Qc60ddr86dbNPmHqWvucWCvQa7kXCyXKkq_lwNqXsSQxNy62ENmUmVWNtBGrox2QGSbx0lkrv7rExRdnhw_MXpEHUHNMAjB9D89RbhAsJCq8JMdNqFPyI-NwwcSSRlR3fhV62I6Ljg_lgtUIm1-6OLc3eOFFX0abU"},{"header":{"kid":"webstore"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"MRD_RWRfn2WMAVlkUdOAympEslgx1sjomkqS_HwJkYjSCLfZFSEGyveDGkVKenylFuV8Sg4cyPwnk_z9XQoWmQsG2nASBLC85Pu1IkKdMOiGx8H3DFS68-bGIhxkjuEkvnXfDwWS_UiMmp-uERSlbKNaEFk5jm780gpoi_SmZqu9dWPzWqvX1Xle3xhpkLf6Es-6HlcVNNbBKaGKwqhHPn859MzJoDIWL7WGSiz5J8cxZoPN0SbzxU28wwTyzw6wZejdvC6FPvT7ESud8x8wsEKkI552fnMxDvs38s1iNWaGzHPVm2xC3EtToIAK75F27qt5Ijh1OK55C6dP4OY78Q"}]}}] \ No newline at end of file diff --git a/library/jcef/cache/TpcdMetadata/2025.10.5.1/manifest.fingerprint b/library/jcef/cache/TpcdMetadata/2025.10.5.1/manifest.fingerprint deleted file mode 100644 index afc136d..0000000 --- a/library/jcef/cache/TpcdMetadata/2025.10.5.1/manifest.fingerprint +++ /dev/null @@ -1 +0,0 @@ -1.b911063da66cd283f0cd92d5b40fa36d891e285ffd0b67e7af79219db69ee5c7 \ No newline at end of file diff --git a/library/jcef/cache/TpcdMetadata/2025.10.5.1/manifest.json b/library/jcef/cache/TpcdMetadata/2025.10.5.1/manifest.json deleted file mode 100644 index c6bfc25..0000000 --- a/library/jcef/cache/TpcdMetadata/2025.10.5.1/manifest.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "manifest_version": 2, - "name": "Third-Party Cookie Deprecation Metadata", - "version": "2025.10.5.1" -} \ No newline at end of file diff --git a/library/jcef/cache/TpcdMetadata/2025.10.5.1/metadata.pb b/library/jcef/cache/TpcdMetadata/2025.10.5.1/metadata.pb deleted file mode 100644 index 486c697..0000000 Binary files a/library/jcef/cache/TpcdMetadata/2025.10.5.1/metadata.pb and /dev/null differ diff --git a/library/jcef/cache/Variations b/library/jcef/cache/Variations index 1a810a0..1fc8283 100644 --- a/library/jcef/cache/Variations +++ b/library/jcef/cache/Variations @@ -1 +1 @@ -{"user_experience_metrics.stability.exited_cleanly":false,"variations_crash_streak":386} \ No newline at end of file +{"user_experience_metrics.stability.exited_cleanly":false,"variations_crash_streak":594} \ No newline at end of file diff --git a/library/jcef/cache/WidevineCdm/4.10.2891.0/LICENSE b/library/jcef/cache/WidevineCdm/4.10.2891.0/LICENSE deleted file mode 100644 index 36f59db..0000000 --- a/library/jcef/cache/WidevineCdm/4.10.2891.0/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -Google LLC and its affiliates ("Google") own all legal right, title and -interest in and to the content decryption module software ("Software") and -related documentation, including any intellectual property rights in the -Software. You may not use, modify, sell, or otherwise distribute the Software -without a separate license agreement with Google. The Software is not open -source software. - -If you are interested in licensing the Software, please contact -www.widevine.com diff --git a/library/jcef/cache/WidevineCdm/4.10.2891.0/_metadata/verified_contents.json b/library/jcef/cache/WidevineCdm/4.10.2891.0/_metadata/verified_contents.json deleted file mode 100644 index 0e9e107..0000000 --- a/library/jcef/cache/WidevineCdm/4.10.2891.0/_metadata/verified_contents.json +++ /dev/null @@ -1 +0,0 @@ -[{"description":"treehash per file","signed_content":{"payload":"eyJjb250ZW50X2hhc2hlcyI6W3siYmxvY2tfc2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2IiwiZmlsZXMiOlt7InBhdGgiOiJMSUNFTlNFIiwicm9vdF9oYXNoIjoicjdVVTVDYVZsQ05MTXNoenVpelR6SWlTNkRhR0VUZTFNYVFLRWpLQ0RGayJ9LHsicGF0aCI6Il9wbGF0Zm9ybV9zcGVjaWZpYy93aW5feDY0L3dpZGV2aW5lY2RtLmRsbCIsInJvb3RfaGFzaCI6IjN2S2p0YmhOVEh1THVWZGdCQjZmbmxCcWZvUkQ3cjB1a3E0S0kzdEhSOE0ifSx7InBhdGgiOiJfcGxhdGZvcm1fc3BlY2lmaWMvd2luX3g2NC93aWRldmluZWNkbS5kbGwuc2lnIiwicm9vdF9oYXNoIjoidmE1N2JDM2pXSEtDZ0o4dTRiQjlzTEtYYWQ0czZhN0FiME9aYUZ4M0tiYyJ9LHsicGF0aCI6Im1hbmlmZXN0Lmpzb24iLCJyb290X2hhc2giOiJUdFBHT0o5dmZObE50YzBQaHd3MG9wYjhEZU94NXdmOHp3RmtXMFZYa000In1dLCJmb3JtYXQiOiJ0cmVlaGFzaCIsImhhc2hfYmxvY2tfc2l6ZSI6NDA5Nn1dLCJpdGVtX2lkIjoib2ltb21wZWNhZ25hamRlamdubmppam9iZWJhZWlnZWsiLCJpdGVtX3ZlcnNpb24iOiI0LjEwLjI4OTEuMCIsInByb3RvY29sX3ZlcnNpb24iOjF9","signatures":[{"header":{"kid":"publisher"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"l6vfhExGU7tsRAr-IwvL55nkUuou1seQjI2-S7bAMkOIk9q4kBgJgjOWZHzIzt3HNaAvyrdz6LQiYBJl4wRtHLPfNUxbC5IC6CBD9S7teWVzZCFOjCHIhxFldn9lRKf0SXuYcc9aHkIEj6mbbouTzBBUZ-pNDri0TKQ1vGvM7OM"},{"header":{"kid":"webstore"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"AqEnL9Iqt_GjxwZruiCa5pTUNX5SB1IachQtE86LWb7n2FpVzeP32wcmqro2DZT_oZvmVAR6PVndi4BAuH2a8IC3m0EQFNPzpUv5w7dXRpjVklia3KygW7cDZLZP0WdqGhsHWHqLV4AHIbjnvgvTwAUjrEfqQW1h2_fTu2dIyg2lr7MUrrmNWkoJT2ewbA7xMXsfvnH_eI89IJbDwVX1EOdKSKlbiHrN2CtI-ZsFQ9z59l56pSUyA6lrFKTlkgVzq0f9cRtDFOtQDc0ILWa-vuBj-OxmFON_ShB1B4Ye0PXJgfULfuYaO6EtJs-D4oImI8Ti4H5Ie-61mIg1AyFmVA"}]}}] \ No newline at end of file diff --git a/library/jcef/cache/WidevineCdm/4.10.2891.0/_platform_specific/win_x64/widevinecdm.dll b/library/jcef/cache/WidevineCdm/4.10.2891.0/_platform_specific/win_x64/widevinecdm.dll deleted file mode 100644 index b2191b7..0000000 Binary files a/library/jcef/cache/WidevineCdm/4.10.2891.0/_platform_specific/win_x64/widevinecdm.dll and /dev/null differ diff --git a/library/jcef/cache/WidevineCdm/4.10.2891.0/_platform_specific/win_x64/widevinecdm.dll.sig b/library/jcef/cache/WidevineCdm/4.10.2891.0/_platform_specific/win_x64/widevinecdm.dll.sig deleted file mode 100644 index 708c9e7..0000000 Binary files a/library/jcef/cache/WidevineCdm/4.10.2891.0/_platform_specific/win_x64/widevinecdm.dll.sig and /dev/null differ diff --git a/library/jcef/cache/WidevineCdm/4.10.2891.0/manifest.fingerprint b/library/jcef/cache/WidevineCdm/4.10.2891.0/manifest.fingerprint deleted file mode 100644 index 6081aa2..0000000 --- a/library/jcef/cache/WidevineCdm/4.10.2891.0/manifest.fingerprint +++ /dev/null @@ -1 +0,0 @@ -1.a6af95a209b2e652ed6766804b9b8ad6b6a68f2c610b8f14713cd40df0d62bf9 \ No newline at end of file diff --git a/library/jcef/cache/WidevineCdm/4.10.2891.0/manifest.json b/library/jcef/cache/WidevineCdm/4.10.2891.0/manifest.json deleted file mode 100644 index c04fa3d..0000000 --- a/library/jcef/cache/WidevineCdm/4.10.2891.0/manifest.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "manifest_version": 2, - "update_url": "https://clients2.google.com/service/update2/crx", - "name": "WidevineCdm", - "description": "Widevine Content Decryption Module", - "version": "4.10.2891.0", - "minimum_chrome_version": "68.0.3430.0", - "x-cdm-module-versions": "4", - "x-cdm-interface-versions": "10", - "x-cdm-host-versions": "10", - "x-cdm-codecs": "vp8,vp09,avc1,av01", - "x-cdm-persistent-license-support": true, - "x-cdm-supported-encryption-schemes": [ - "cenc", - "cbcs" - ], - "icons": { - "16": "imgs/icon-128x128.png", - "128": "imgs/icon-128x128.png" - }, - "platforms": [ - { - "os": "win", - "arch": "x64", - "sub_package_path": "_platform_specific/win_x64/" - }, - { - "os": "win", - "arch": "x86", - "sub_package_path": "_platform_specific/win_x86/" - }, - { - "os": "win", - "arch": "arm64", - "sub_package_path": "_platform_specific/win_arm64/" - } - ], - "accept_arch": [ - "x64", - "x86_64", - "x86_64h" - ] -} \ No newline at end of file diff --git a/library/jcef/cache/chrome_debug.log b/library/jcef/cache/chrome_debug.log index 9d547f1..cef689d 100644 --- a/library/jcef/cache/chrome_debug.log +++ b/library/jcef/cache/chrome_debug.log @@ -1,72 +1,6 @@ -[25176:20700:1116/134535.058:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[25176:20700:1116/134535.093:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[24656:20712:1116/134535.315:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[6424:3776:1116/134735.388:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[25116:23752:1116/134835.102:ERROR:gpu_blocklist.cc(71)] Unable to get gpu adapter -[25176:20700:1116/134835.103:ERROR:service_client.cc(36)] Unexpected on_device_model service disconnect: The device's GPU is not supported. -[25012:3556:1116/135030.390:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[25012:3556:1116/135030.422:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[25416:22984:1116/135030.626:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[22104:4432:1116/135101.126:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[22104:4432:1116/135101.158:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[4292:15180:1116/135101.362:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[8940:2448:1116/135102.484:WARNING:backend_impl.cc(1757)] Destroying invalid entry. -[24120:23064:1116/135131.279:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[24120:23064:1116/135131.313:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[3128:22532:1116/135131.526:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[24680:4656:1116/135334.771:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[24680:4656:1116/135334.802:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[18180:2464:1116/135335.025:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[22568:8176:1116/135603.988:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[22568:8176:1116/135604.019:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[1740:19120:1116/135604.262:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[24072:24800:1116/135804.268:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[24628:17756:1116/135904.034:ERROR:gpu_blocklist.cc(71)] Unable to get gpu adapter -[22568:8176:1116/135904.034:ERROR:service_client.cc(36)] Unexpected on_device_model service disconnect: The device's GPU is not supported. -[22968:14560:1116/140111.527:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[22968:14560:1116/140111.574:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[15972:3044:1116/140111.769:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[23484:25224:1116/140142.940:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[23484:25224:1116/140142.970:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[9076:18484:1116/140143.178:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[13380:24952:1116/140143.506:WARNING:backend_impl.cc(1757)] Destroying invalid entry. -[13380:24952:1116/140143.521:WARNING:backend_impl.cc(1757)] Destroying invalid entry. -[13380:24952:1116/140143.522:WARNING:backend_impl.cc(1757)] Destroying invalid entry. -[13380:24952:1116/140143.522:WARNING:backend_impl.cc(1757)] Destroying invalid entry. -[22408:24528:1116/140324.353:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[22408:24528:1116/140324.387:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[4316:23620:1116/140324.585:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[20956:24116:1116/140524.561:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[2332:23256:1116/140529.242:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[2332:23256:1116/140529.277:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[21212:22652:1116/140529.475:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[24336:23236:1116/140612.851:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[24336:23236:1116/140612.890:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[22856:13480:1116/140613.091:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[23048:12868:1116/140707.334:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[23048:12868:1116/140707.371:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[20416:10836:1116/140707.573:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[9900:24972:1116/140708.787:WARNING:backend_impl.cc(1757)] Destroying invalid entry. -[9900:24972:1116/140708.788:WARNING:backend_impl.cc(1757)] Destroying invalid entry. -[24012:8024:1116/140818.429:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[24012:8024:1116/140818.460:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[12504:7372:1116/140818.650:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[20088:24612:1116/140921.658:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[20088:24612:1116/140921.690:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[17892:22072:1116/140921.900:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[23516:10028:1116/141013.800:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[23516:10028:1116/141013.833:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[15276:23988:1116/141014.056:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[21372:25472:1116/141108.344:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[21372:25472:1116/141108.378:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[9164:25548:1116/141108.569:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[19976:8024:1116/141220.067:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[19976:8024:1116/141220.104:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[20268:19948:1116/141220.294:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[22624:21192:1116/141420.243:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[13532:6284:1116/141454.556:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[13532:6284:1116/141454.590:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[25364:4332:1116/141454.798:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) -[3564:24164:1116/153613.078:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. -[3564:24164:1116/153613.112:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. -[10268:6444:1116/153613.586:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) +[12284:16860:1228/145054.244:WARNING:account_consistency_mode_manager.cc(77)] Desktop Identity Consistency cannot be enabled as no OAuth client ID and client secret have been configured. +[12284:16860:1228/145054.280:WARNING:extension_service.cc(2065)] Found external version of extension ncennffkjdiamlpmcbajkmaiiiddgioothat is older than current version. Current version is: 3.52.14. New version is: 3.52.5. Keeping current version. +[8880:16484:1228/145054.563:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) +[13676:18520:1228/145254.442:WARNING:viz_main_impl.cc(85)] VizNullHypothesis is disabled (not a warning) +[14320:9004:1228/145354.607:ERROR:gpu_blocklist.cc(71)] Unable to get gpu adapter +[12284:16860:1228/145354.607:ERROR:service_client.cc(36)] Unexpected on_device_model service disconnect: The device's GPU is not supported. diff --git a/library/jcef/cache/component_crx_cache/ggkkehgbnfjpeggfpleeakpidbkibbmn_1.0bd17169e41bf80771e71e625ed9469c4006d08a33caa457e184caa55174f67b b/library/jcef/cache/component_crx_cache/ggkkehgbnfjpeggfpleeakpidbkibbmn_1.0bd17169e41bf80771e71e625ed9469c4006d08a33caa457e184caa55174f67b deleted file mode 100644 index 6e06d77..0000000 Binary files a/library/jcef/cache/component_crx_cache/ggkkehgbnfjpeggfpleeakpidbkibbmn_1.0bd17169e41bf80771e71e625ed9469c4006d08a33caa457e184caa55174f67b and /dev/null differ diff --git a/library/jcef/cache/component_crx_cache/jflhchccmppkfebkiaminageehmchikm_1.b911063da66cd283f0cd92d5b40fa36d891e285ffd0b67e7af79219db69ee5c7 b/library/jcef/cache/component_crx_cache/jflhchccmppkfebkiaminageehmchikm_1.b911063da66cd283f0cd92d5b40fa36d891e285ffd0b67e7af79219db69ee5c7 deleted file mode 100644 index 8fcd1d6..0000000 Binary files a/library/jcef/cache/component_crx_cache/jflhchccmppkfebkiaminageehmchikm_1.b911063da66cd283f0cd92d5b40fa36d891e285ffd0b67e7af79219db69ee5c7 and /dev/null differ diff --git a/library/jcef/cache/component_crx_cache/llkgjffcdpffmhiakmfcdcblohccpfmo_1.ee4b855eb4e00f150fe268baead4f478bf3f5a6b9b8b89026d71e09c368876f8 b/library/jcef/cache/component_crx_cache/llkgjffcdpffmhiakmfcdcblohccpfmo_1.ee4b855eb4e00f150fe268baead4f478bf3f5a6b9b8b89026d71e09c368876f8 deleted file mode 100644 index 6864f30..0000000 Binary files a/library/jcef/cache/component_crx_cache/llkgjffcdpffmhiakmfcdcblohccpfmo_1.ee4b855eb4e00f150fe268baead4f478bf3f5a6b9b8b89026d71e09c368876f8 and /dev/null differ diff --git a/library/jcef/cache/component_crx_cache/mfhmdacoffpmifoibamicehhklffanao_1.10670cf2529aaff93f703c397dc379c450546c4d2669c5d364dd6ceba8ac3f63 b/library/jcef/cache/component_crx_cache/mfhmdacoffpmifoibamicehhklffanao_1.10670cf2529aaff93f703c397dc379c450546c4d2669c5d364dd6ceba8ac3f63 deleted file mode 100644 index e6506a8..0000000 Binary files a/library/jcef/cache/component_crx_cache/mfhmdacoffpmifoibamicehhklffanao_1.10670cf2529aaff93f703c397dc379c450546c4d2669c5d364dd6ceba8ac3f63 and /dev/null differ diff --git a/library/jcef/cache/component_crx_cache/oimompecagnajdejgnnjijobebaeigek_1.a6af95a209b2e652ed6766804b9b8ad6b6a68f2c610b8f14713cd40df0d62bf9 b/library/jcef/cache/component_crx_cache/oimompecagnajdejgnnjijobebaeigek_1.a6af95a209b2e652ed6766804b9b8ad6b6a68f2c610b8f14713cd40df0d62bf9 deleted file mode 100644 index 843c449..0000000 Binary files a/library/jcef/cache/component_crx_cache/oimompecagnajdejgnnjijobebaeigek_1.a6af95a209b2e652ed6766804b9b8ad6b6a68f2c610b8f14713cd40df0d62bf9 and /dev/null differ diff --git a/library/jcef/cache/first_party_sets.db b/library/jcef/cache/first_party_sets.db index e6abbb1..987c09d 100644 Binary files a/library/jcef/cache/first_party_sets.db and b/library/jcef/cache/first_party_sets.db differ diff --git a/library/jcef/cache/segmentation_platform/ukm_db b/library/jcef/cache/segmentation_platform/ukm_db index 9e2db23..28fa077 100644 Binary files a/library/jcef/cache/segmentation_platform/ukm_db and b/library/jcef/cache/segmentation_platform/ukm_db differ diff --git a/library/jcef/cache/segmentation_platform/ukm_db-journal b/library/jcef/cache/segmentation_platform/ukm_db-journal index e69de29..a52d19a 100644 Binary files a/library/jcef/cache/segmentation_platform/ukm_db-journal and b/library/jcef/cache/segmentation_platform/ukm_db-journal differ diff --git a/sql_learning.db b/sql_learning.db new file mode 100644 index 0000000..db9cb64 Binary files /dev/null and b/sql_learning.db differ diff --git a/sql_learning.lock.db b/sql_learning.lock.db new file mode 100644 index 0000000..e5b03ab --- /dev/null +++ b/sql_learning.lock.db @@ -0,0 +1,6 @@ +#FileLock +#Sun Dec 28 14:52:28 CST 2025 +hostName=192.168.116.1 +id=19b63bad12a8483d76995f9c5100715a8b474ee84cc +method=file +server=192.168.116.1\:64976 diff --git a/sql_learning.trace.db b/sql_learning.trace.db new file mode 100644 index 0000000..4163caf --- /dev/null +++ b/sql_learning.trace.db @@ -0,0 +1,200 @@ +2025-12-12 17:22:54 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "[*]ues students"; expected "UPDATE, USE"; SQL statement: +ues students [42001-220] +2025-12-12 17:23:23 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "STUDENTS" not found; SQL statement: +SELECT students [42122-220] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:514) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.expression.ExpressionColumn.getColumnException(ExpressionColumn.java:244) + at org.h2.expression.ExpressionColumn.optimizeOther(ExpressionColumn.java:226) + at org.h2.expression.ExpressionColumn.optimize(ExpressionColumn.java:213) + at org.h2.command.query.Select.optimizeExpressionsAndPreserveAliases(Select.java:1285) + at org.h2.command.query.Select.prepareExpressions(Select.java:1167) + at org.h2.command.query.Query.prepare(Query.java:218) + at org.h2.command.Parser.prepareCommand(Parser.java:583) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:634) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:557) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1116) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:237) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:223) + at com.axis.innovators.box.browser.MainApplication.executeRealSQL(MainApplication.java:136) + at com.axis.innovators.box.browser.MainApplication$1.onQuery(MainApplication.java:265) +2025-12-12 17:24:37 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Schema "STUDENTS" not found; SQL statement: +SET SCHEMA students [90079-220] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:644) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.engine.Database.getSchema(Database.java:1548) + at org.h2.command.dml.Set.update(Set.java:397) + at org.h2.command.CommandContainer.update(CommandContainer.java:169) + at org.h2.command.Command.executeUpdate(Command.java:252) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:252) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:223) + at com.axis.innovators.box.browser.MainApplication.executeRealSQL(MainApplication.java:136) + at com.axis.innovators.box.browser.MainApplication$1.onQuery(MainApplication.java:265) +2025-12-12 21:13:44 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "[*]mysql"; expected "MERGE"; SQL statement: +mysql [42001-220] +2025-12-12 21:13:50 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "?[*]"; expected "="; SQL statement: +? [42001-220] +2025-12-12 21:14:03 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "[*]EXIT"; expected "EXPLAIN, EXECUTE"; SQL statement: +EXIT [42001-220] +2025-12-12 21:14:07 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "[*]exit"; expected "EXPLAIN, EXECUTE"; SQL statement: +exit [42001-220] +2025-12-12 21:14:21 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "[*]cls"; expected "COMMIT, CREATE, CALL, CHECKPOINT, COMMENT"; SQL statement: +cls [42001-220] +2025-12-12 21:14:31 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "[*]quit"; SQL statement: +quit [42000-220] +2025-12-12 21:15:15 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Function "DATABASE" not found; SQL statement: +SELECT * FROM database() [90022-220] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:644) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.command.Parser.getFunctionAliasWithinPath(Parser.java:2576) + at org.h2.command.Parser.readTableFunction(Parser.java:2060) + at org.h2.command.Parser.readTablePrimary(Parser.java:1908) + at org.h2.command.Parser.readTableReference(Parser.java:2391) + at org.h2.command.Parser.parseSelectFromPart(Parser.java:2844) + at org.h2.command.Parser.parseSelect(Parser.java:2950) + at org.h2.command.Parser.parseQueryPrimary(Parser.java:2834) + at org.h2.command.Parser.parseQueryTerm(Parser.java:2690) + at org.h2.command.Parser.parseQueryExpressionBody(Parser.java:2669) + at org.h2.command.Parser.parseQueryExpressionBodyAndEndOfQuery(Parser.java:2662) + at org.h2.command.Parser.parseQueryExpression(Parser.java:2655) + at org.h2.command.Parser.parseQuery(Parser.java:2624) + at org.h2.command.Parser.parsePrepared(Parser.java:732) + at org.h2.command.Parser.parse(Parser.java:697) + at org.h2.command.Parser.parse(Parser.java:669) + at org.h2.command.Parser.prepareCommand(Parser.java:577) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:634) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:557) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1116) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:237) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:223) + at com.axis.innovators.box.browser.MainApplication.executeRealSQL(MainApplication.java:136) + at com.axis.innovators.box.browser.MainApplication$1.onQuery(MainApplication.java:265) +2025-12-12 21:15:26 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Function "DATABASES" not found; SQL statement: +SELECT * FROM databases() [90022-220] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:644) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.command.Parser.getFunctionAliasWithinPath(Parser.java:2576) + at org.h2.command.Parser.readTableFunction(Parser.java:2060) + at org.h2.command.Parser.readTablePrimary(Parser.java:1908) + at org.h2.command.Parser.readTableReference(Parser.java:2391) + at org.h2.command.Parser.parseSelectFromPart(Parser.java:2844) + at org.h2.command.Parser.parseSelect(Parser.java:2950) + at org.h2.command.Parser.parseQueryPrimary(Parser.java:2834) + at org.h2.command.Parser.parseQueryTerm(Parser.java:2690) + at org.h2.command.Parser.parseQueryExpressionBody(Parser.java:2669) + at org.h2.command.Parser.parseQueryExpressionBodyAndEndOfQuery(Parser.java:2662) + at org.h2.command.Parser.parseQueryExpression(Parser.java:2655) + at org.h2.command.Parser.parseQuery(Parser.java:2624) + at org.h2.command.Parser.parsePrepared(Parser.java:732) + at org.h2.command.Parser.parse(Parser.java:697) + at org.h2.command.Parser.parse(Parser.java:669) + at org.h2.command.Parser.prepareCommand(Parser.java:577) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:634) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:557) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1116) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:237) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:223) + at com.axis.innovators.box.browser.MainApplication.executeRealSQL(MainApplication.java:136) + at com.axis.innovators.box.browser.MainApplication$1.onQuery(MainApplication.java:265) +2025-12-12 21:16:23 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Schema "ADS" not found; SQL statement: +SET SCHEMA ads [90079-220] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:644) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.engine.Database.getSchema(Database.java:1548) + at org.h2.command.dml.Set.update(Set.java:397) + at org.h2.command.CommandContainer.update(CommandContainer.java:169) + at org.h2.command.Command.executeUpdate(Command.java:252) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:252) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:223) + at com.axis.innovators.box.browser.MainApplication.executeRealSQL(MainApplication.java:136) + at com.axis.innovators.box.browser.MainApplication$1.onQuery(MainApplication.java:265) +2025-12-12 21:16:37 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Schema "ADS" not found; SQL statement: +SET SCHEMA ads [90079-220] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:644) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.engine.Database.getSchema(Database.java:1548) + at org.h2.command.dml.Set.update(Set.java:397) + at org.h2.command.CommandContainer.update(CommandContainer.java:169) + at org.h2.command.Command.executeUpdate(Command.java:252) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:252) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:223) + at com.axis.innovators.box.browser.MainApplication.executeRealSQL(MainApplication.java:136) + at com.axis.innovators.box.browser.MainApplication$1.onQuery(MainApplication.java:265) +2025-12-12 21:17:05 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "10ada" not found; SQL statement: +SELECT "10ada"*100 [42122-220] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:514) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.expression.ExpressionColumn.getColumnException(ExpressionColumn.java:244) + at org.h2.expression.ExpressionColumn.optimizeOther(ExpressionColumn.java:226) + at org.h2.expression.ExpressionColumn.optimize(ExpressionColumn.java:213) + at org.h2.expression.BinaryOperation.optimize(BinaryOperation.java:131) + at org.h2.command.query.Select.optimizeExpressionsAndPreserveAliases(Select.java:1285) + at org.h2.command.query.Select.prepareExpressions(Select.java:1167) + at org.h2.command.query.Query.prepare(Query.java:218) + at org.h2.command.Parser.prepareCommand(Parser.java:583) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:634) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:557) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1116) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:237) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:223) + at com.axis.innovators.box.browser.MainApplication.executeRealSQL(MainApplication.java:136) + at com.axis.innovators.box.browser.MainApplication$1.onQuery(MainApplication.java:265) +2025-12-12 21:17:09 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "10ada" not found; SQL statement: +SELECT "10ada" * 100 [42122-220] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:514) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + at org.h2.message.DbException.get(DbException.java:223) + at org.h2.message.DbException.get(DbException.java:199) + at org.h2.expression.ExpressionColumn.getColumnException(ExpressionColumn.java:244) + at org.h2.expression.ExpressionColumn.optimizeOther(ExpressionColumn.java:226) + at org.h2.expression.ExpressionColumn.optimize(ExpressionColumn.java:213) + at org.h2.expression.BinaryOperation.optimize(BinaryOperation.java:131) + at org.h2.command.query.Select.optimizeExpressionsAndPreserveAliases(Select.java:1285) + at org.h2.command.query.Select.prepareExpressions(Select.java:1167) + at org.h2.command.query.Query.prepare(Query.java:218) + at org.h2.command.Parser.prepareCommand(Parser.java:583) + at org.h2.engine.SessionLocal.prepareLocal(SessionLocal.java:634) + at org.h2.engine.SessionLocal.prepareCommand(SessionLocal.java:557) + at org.h2.jdbc.JdbcConnection.prepareCommand(JdbcConnection.java:1116) + at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:237) + at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:223) + at com.axis.innovators.box.browser.MainApplication.executeRealSQL(MainApplication.java:136) + at com.axis.innovators.box.browser.MainApplication$1.onQuery(MainApplication.java:265) +2025-12-15 10:39:15 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "DELETE add[*]"; expected "., FROM"; SQL statement: +DELETE add [42001-220] +2025-12-15 10:39:23 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "DELETE [*]TABLE add"; expected "identifier"; SQL statement: +DELETE TABLE add [42001-220] +2025-12-15 10:39:48 jdbc[3]: exception +org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "DELETE [*]/?"; expected "identifier"; SQL statement: +DELETE /? [42001-220] diff --git a/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java b/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java index c489708..ad98e04 100644 --- a/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java +++ b/src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java @@ -96,8 +96,6 @@ public class AxisInnovatorsBox { private final boolean isDebug; private static DebugWindow debugWindow; - private static LoginData loginData; - public AxisInnovatorsBox(String[] args, boolean isDebug, boolean quickStart) { this.args = args; this.isDebug = isDebug; @@ -145,6 +143,7 @@ public class AxisInnovatorsBox { token = null; } } + LoginData loginData; if (token == null || token.isEmpty()) { LoginResult loginResult = CasdoorLoginWindow.showLoginDialogAndGetLoginResult(); if (loginResult == null) { @@ -1073,20 +1072,12 @@ public class AxisInnovatorsBox { // Set the exception handler for EDT(event dispatcher thread) System.setProperty("sun.awt.exception.handler", EDTCrashHandler.class.getName()); - // Check if AxisInnovatorsBox is started - // If it's started, and it's not a quick start, don't allow it - // because it's already started - // Stop loading if the current running context is the quickStart context - if (AxisInnovatorsBox.getMain() != null - && !AxisInnovatorsBox.getMain().getQuickStart() || quickStart) { - // Manually created if it is a quickStart context and the AxisInnovatorsBox instance in the context is empty - if (AxisInnovatorsBox.getMain() == null && quickStart) { - new AxisInnovatorsBox(args,isDebug,true); - } + main = new AxisInnovatorsBox(args,isDebug,quickStart); + + if (quickStart) { return; } - main = new AxisInnovatorsBox(args,isDebug,false); try { main.initLog4j2(); main.setTopic(); diff --git a/src/main/java/com/axis/innovators/box/Main.java b/src/main/java/com/axis/innovators/box/Main.java index 51c06a2..8f3288c 100644 --- a/src/main/java/com/axis/innovators/box/Main.java +++ b/src/main/java/com/axis/innovators/box/Main.java @@ -74,11 +74,7 @@ public class Main { String path = fileInfo.get("path"); if (".jar".equals(extension)) { SwingUtilities.invokeLater(() -> { - try { - UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarculaLaf()); - } catch (Exception ignored) {} - ModernJarViewer viewer = new ModernJarViewer(null, path); - viewer.setVisible(true); + ModernJarViewer.popupSimulatingWindow(null,path); }); releaseLock(); // 释放锁(窗口模式) quickStart = true; @@ -139,7 +135,6 @@ public class Main { if (lockFile != null) { lockFile.close(); } - // 可选:删除锁文件 new File(LOCK_FILE).delete(); } catch (IOException e) { e.printStackTrace(); diff --git a/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java b/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java index 2633201..5fb6c1d 100644 --- a/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java +++ b/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java @@ -10,10 +10,7 @@ import org.cef.CefSettings; import org.cef.browser.CefBrowser; import org.cef.browser.CefFrame; import org.cef.browser.CefMessageRouter; -import org.cef.callback.CefContextMenuParams; -import org.cef.callback.CefJSDialogCallback; -import org.cef.callback.CefMenuModel; -import org.cef.callback.CefQueryCallback; +import org.cef.callback.*; import org.cef.handler.*; import org.cef.misc.BoolRef; import org.cef.network.CefRequest; @@ -307,44 +304,30 @@ public class BrowserWindow extends JFrame { }); } - client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() { - @Override - public boolean onBeforePopup(CefBrowser browser, CefFrame frame, - String targetUrl, String targetFrameName) { - // 处理弹出窗口:根据配置决定打开方式 - if (builder.openLinksInExternalBrowser) { - // 使用默认浏览器打开 - try { - Desktop.getDesktop().browse(new URI(targetUrl)); - } catch (Exception e) { - System.out.println("Failed to open external browser: " + e.getMessage()); - } - return true; // 拦截弹窗 - } else { - // 在当前浏览器中打开 - browser.loadURL(targetUrl); - return true; // 拦截弹窗并在当前窗口打开 - } - } - }); - client.addRequestHandler(new CefRequestHandlerAdapter() { @Override public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame, CefRequest request, boolean userGesture, boolean isRedirect) { - // 处理主窗口导航 + + String url = request.getURL(); + + // 【关键判断】如果是 data: 协议,绝对禁止调用 Desktop.browse + // 返回 false 让 CEF 内核自己渲染这个 Base64 内容 + if (url != null && url.toLowerCase().startsWith("data:")) { + return false; + } + + // 处理其他普通链接 (http/https/file) if (userGesture) { if (builder.openLinksInExternalBrowser) { - // 使用默认浏览器打开 try { - Desktop.getDesktop().browse(new URI(request.getURL())); - return true; // 取消内置浏览器导航 + Desktop.getDesktop().browse(new URI(url)); + return true; // 拦截,交给系统 } catch (Exception e) { - System.out.println("Failed to open external browser: " + e.getMessage()); + System.out.println("外部浏览器打开失败: " + e.getMessage()); } } else { - // 允许在当前浏览器中打开 - return false; + return false; // 允许内部跳转 } } return false; @@ -426,10 +409,26 @@ public class BrowserWindow extends JFrame { } }); + client.addDownloadHandler(new org.cef.handler.CefDownloadHandler() { + @Override + public void onBeforeDownload(org.cef.browser.CefBrowser browser, + org.cef.callback.CefDownloadItem downloadItem, + String suggestedName, + org.cef.callback.CefBeforeDownloadCallback callback) { + callback.Continue(suggestedName, false); + } + + @Override + public void onDownloadUpdated(org.cef.browser.CefBrowser browser, + org.cef.callback.CefDownloadItem downloadItem, + org.cef.callback.CefDownloadItemCallback callback) { + } + }); + client.addJSDialogHandler(new CefJSDialogHandlerAdapter() { @Override - public boolean onJSDialog(CefBrowser browser, String origin_url, CefJSDialogHandler.JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) { - if (dialog_type == CefJSDialogHandler.JSDialogType.JSDIALOGTYPE_ALERT) { + public boolean onJSDialog(CefBrowser browser, String origin_url, JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) { + if (dialog_type == JSDialogType.JSDIALOGTYPE_ALERT) { SwingUtilities.invokeLater(() -> { JOptionPane.showMessageDialog( BrowserWindow.this, @@ -437,23 +436,81 @@ public class BrowserWindow extends JFrame { "警告", JOptionPane.INFORMATION_MESSAGE ); + callback.Continue(true, ""); + }); + return true; + } else if (dialog_type == JSDialogType.JSDIALOGTYPE_CONFIRM) { // 处理 confirm() + SwingUtilities.invokeLater(() -> { + int result = JOptionPane.showConfirmDialog( + BrowserWindow.this, + message_text, + "确认", + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE + ); + + // 如果用户点击 YES (确定),则传回 true + boolean confirmed = (result == JOptionPane.YES_OPTION); + callback.Continue(confirmed, ""); + }); + return true; + } else if (dialog_type == JSDialogType.JSDIALOGTYPE_PROMPT) { + SwingUtilities.invokeLater(() -> { + Object result = JOptionPane.showInputDialog( + BrowserWindow.this, + message_text, + "输入", + JOptionPane.QUESTION_MESSAGE, + null, + null, + default_prompt_text + ); + String input = (result != null) ? result.toString() : null; + if (input != null) { + callback.Continue(true, input); + } else { + callback.Continue(false, ""); + } }); - callback.Continue(true, ""); return true; } + // 默认行为:如果不是以上三种类型,交给 CEF 默认处理 return false; } }); // 3. 拦截所有新窗口(关键修复点!) - //client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() { - // @Override - // public boolean onBeforePopup(CefBrowser browser, - // CefFrame frame, String target_url, String target_frame_name) { - // return true; // 返回true表示拦截弹窗 - // } - //}); + client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() { + @Override + public boolean onBeforePopup(CefBrowser browser, CefFrame frame, + String targetUrl, String targetFrameName) { + boolean isDataProtocol = targetUrl != null && targetUrl.toLowerCase().startsWith("data:"); + if (builder.openLinksInExternalBrowser && !isDataProtocol) { + try { + Desktop.getDesktop().browse(new URI(targetUrl)); + } catch (Exception e) { + System.out.println("外部浏览器打开失败: " + e.getMessage()); + } + return true; // 拦截默认行为 + } + SwingUtilities.invokeLater(() -> { + String popupWindowId = windowId + "_popup_" + System.currentTimeMillis(); + WindowRegistry.getInstance().createNewWindow(popupWindowId, popupBuilder -> { + popupBuilder.title(getTitle()) // 继承标题 + .size(getWidth(), getHeight()) // 继承大小 + .htmlUrl(targetUrl) // 传入 data: URL + .icon(builder.icon) // 继承图标 + .openLinksInBrowser(true); // 新窗口内链接强制内部打开 + if (builder.operationHandler != null) { + popupBuilder.operationHandler(builder.operationHandler); + } + }); + }); + + return true; // 拦截 CEF 默认弹窗,由 Java Swing 窗口接管 + } + }); Thread.currentThread().setName("BrowserRenderThread"); @@ -582,8 +639,10 @@ public class BrowserWindow extends JFrame { public void updateTheme() { // 1. 获取Java字体信息 String fontInfo = getSystemFontsInfo(); - boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode(); - injectFontInfoToPage(browser, fontInfo, isDarkTheme); + if (AxisInnovatorsBox.getMain() != null) { + boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode(); + injectFontInfoToPage(browser, fontInfo, isDarkTheme); + } // 2. 注入主题信息 //injectThemeInfoToPage(browser, isDarkTheme); diff --git a/src/main/java/com/axis/innovators/box/browser/MainApplication.java b/src/main/java/com/axis/innovators/box/browser/MainApplication.java index 60432f6..c1709cb 100644 --- a/src/main/java/com/axis/innovators/box/browser/MainApplication.java +++ b/src/main/java/com/axis/innovators/box/browser/MainApplication.java @@ -3,6 +3,7 @@ package com.axis.innovators.box.browser; import com.axis.innovators.box.AxisInnovatorsBox; import com.axis.innovators.box.browser.util.CodeExecutor; import com.axis.innovators.box.browser.util.DatabaseConnectionManager; +import com.axis.innovators.box.browser.util.TerminalManager; import com.axis.innovators.box.tools.FolderCreator; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -36,19 +37,254 @@ import java.util.concurrent.atomic.AtomicReference; public class MainApplication { private static final ExecutorService executor = Executors.newCachedThreadPool(); + private static Connection dbConnection = null; private static long modelHandle; private static long ctxHandle; private static boolean isSystem = true; public static void main(String[] args) { - AtomicReference window = new AtomicReference<>(); - WindowRegistry.getInstance().createNewWindow("main", builder -> - window.set(builder.title("Axis Innovators Box AI 工具箱") - .size(1280, 720) - .htmlUrl("https://www.bilibili.com/") - .openLinksInBrowser(true) - .operationHandler(createOperationHandler()) - .build()) - ); + TerminalManager.popupRealLinuxWindow(); + } + + public static void popupSimulatingLinuxWindow(JFrame parent){ + + } + /** + * 初始化数据库连接 + */ + private static void initDatabase() { + try { + if (dbConnection != null && !dbConnection.isClosed()) { + return; + } + + // 加载 H2 驱动 + Class.forName("org.h2.Driver"); + + // 核心配置: + // 1. MODE=MySQL : 开启 MySQL 语法兼容 (支持 AUTO_INCREMENT, ENUM 等) + // 2. IGNORE_UNKNOWN_SETTINGS=TRUE : 忽略 ENGINE=InnoDB, CHARSET=utf8 等 H2 不懂的配置,不报错 + // 3. CASE_INSENSITIVE_IDENTIFIERS=TRUE : 忽略大小写 + // 4. ~ : 代表用户主目录,数据库文件名为 sql_learning.mv.db + String url = "jdbc:h2:./sql_learning;" + + "MODE=MySQL;" + + "IGNORE_UNKNOWN_SETTINGS=TRUE;" + + "CASE_INSENSITIVE_IDENTIFIERS=TRUE;" + + "AUTO_SERVER=TRUE"; // 允许自动混合模式 + + dbConnection = DriverManager.getConnection(url, "sa", ""); + + System.out.println("Connected to H2 Database in MySQL Mode"); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 真实的 SQL 执行逻辑 + * 替换了之前的 executeMockSQL + */ + private static JSONObject executeRealSQL(String sql) { + JSONObject response = new JSONObject(); + String originSql = sql.trim(); + // 移除末尾分号,防止 JDBC 报错 + if (originSql.endsWith(";")) { + originSql = originSql.substring(0, originSql.length() - 1); + } + String upperSql = originSql.toUpperCase(); + + // 1. 拦截 HELP (前端功能) + if (upperSql.equals("HELP")) { + response.put("status", "success"); + response.put("type", "text"); + response.put("output", "Available commands:\n" + + " Any valid MySQL SQL syntax\n" + + " CLS / CLEAR : Clear screen\n" + + " EXIT : Close connection"); + return response; + } + + // 2. 语义映射:将 MySQL 的【库】概念映射为 H2 的【模式(Schema)】 + // H2 是单数据库文件模式,为了模拟多库,我们用 Schema 代替。 + // CREATE DATABASE xxx -> CREATE SCHEMA xxx + if (upperSql.startsWith("CREATE DATABASE ")) { + originSql = "CREATE SCHEMA " + originSql.substring(16); + } else if (upperSql.startsWith("USE ")) { + // USE xxx -> SET SCHEMA xxx + originSql = "SET SCHEMA " + originSql.substring(4); + } else if (upperSql.startsWith("DROP DATABASE ")) { + originSql = "DROP SCHEMA " + originSql.substring(14); + } else if (upperSql.equals("SHOW DATABASES")) { + originSql = "SHOW SCHEMAS"; + } + + // 确保连接 + if (dbConnection == null) { + initDatabase(); + if (dbConnection == null) { + response.put("status", "error"); + response.put("output", "Error: Could not connect to database."); + return response; + } + } + + long startTime = System.currentTimeMillis(); + Statement stmt = null; + ResultSet rs = null; + + try { + stmt = dbConnection.createStatement(); + + // 3. 直接执行! + // 此时 H2 的 MySQL 模式会原生处理 AUTO_INCREMENT, ENUM, ENGINE=InnoDB (被忽略) + boolean isResultSet = stmt.execute(originSql); + + long endTime = System.currentTimeMillis(); + double duration = (endTime - startTime) / 1000.0; + + if (isResultSet) { + // --- 结果集处理 --- + response.put("status", "success"); + response.put("type", "table"); + rs = stmt.getResultSet(); + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + + JSONArray headers = new JSONArray(); + for (int i = 1; i <= columnCount; i++) headers.put(metaData.getColumnLabel(i)); + response.put("headers", headers); + + JSONArray rows = new JSONArray(); + int rowCount = 0; + while (rs.next()) { + JSONArray row = new JSONArray(); + for (int i = 1; i <= columnCount; i++) { + Object val = rs.getObject(i); + row.put(val == null ? "NULL" : val.toString()); + } + rows.put(row); + rowCount++; + } + response.put("rows", rows); + response.put("info", String.format("%d rows in set (%.2f sec)", rowCount, duration)); + + } else { + // --- 更新/DDL 处理 --- + int updateCount = stmt.getUpdateCount(); + response.put("status", "success"); + response.put("type", "text"); + response.put("output", String.format("Query OK, %d row(s) affected (%.2f sec)", updateCount, duration)); + } + + } catch (SQLException e) { + response.put("status", "error"); + // H2 的报错信息通常很清晰 + response.put("output", "ERROR " + e.getErrorCode() + ": " + e.getMessage()); + } finally { + try { if (rs != null) rs.close(); } catch (Exception e) {} + try { if (stmt != null) stmt.close(); } catch (Exception e) {} + } + + return response; + } + + /** + * 获取数据库的所有表和列信息,用于前端自动补全 + */ + private static JSONObject getDatabaseSchema() { + JSONObject schema = new JSONObject(); + JSONArray tables = new JSONArray(); + JSONObject columnsMap = new JSONObject(); // Key: tableName, Value: ["col1", "col2"] + + // 确保连接存在 + if (dbConnection == null) initDatabase(); + + try { + DatabaseMetaData meta = dbConnection.getMetaData(); + // 获取所有表 + ResultSet rs = meta.getTables(null, null, "%", new String[]{"TABLE"}); + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + tables.put(tableName); + + // 获取该表的所有列 + JSONArray cols = new JSONArray(); + ResultSet rsCols = meta.getColumns(null, null, tableName, "%"); + while (rsCols.next()) { + cols.put(rsCols.getString("COLUMN_NAME")); + } + columnsMap.put(tableName, cols); + rsCols.close(); + } + rs.close(); + + schema.put("status", "success"); + schema.put("tables", tables); + schema.put("columns", columnsMap); + + } catch (SQLException e) { + schema.put("status", "error"); + schema.put("message", e.getMessage()); + } + return schema; + } + + /** + * 弹出模拟 SQL 命令行窗口 + * @param parent 父窗口 + */ + public static void popupSimulatingSQLWindow(JFrame parent) { + AtomicReference window = new AtomicReference<>(); + initDatabase(); + SwingUtilities.invokeLater(() -> { + WindowRegistry.getInstance().createNewChildWindow("main", builder -> + window.set(builder.title("SQL Command Line Client") + .parentFrame(parent) + .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) + .size(900, 600) + .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "SQLTerminal.html") + .operationHandler(createOperationHandler()) + .build()) + ); + + CefMessageRouter msgRouter = window.get().getMsgRouter(); + if (msgRouter != null) { + msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { + // 在 popupSimulatingSQLWindow 方法的 msgRouter.addHandler 内部: + + public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, + String request, boolean persistent, CefQueryCallback callback) { + try { + JSONObject requestJson = new JSONObject(request); + String type = requestJson.optString("type"); + + if ("initSchema".equals(type)) { + JSONObject schema = getDatabaseSchema(); + callback.success(schema.toString()); + return true; + } + + if ("executeCommand".equals(type)) { + String cmd = requestJson.optString("command"); + JSONObject response = executeRealSQL(cmd); + callback.success(response.toString()); + return true; + } + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("output", "Error: " + e.getMessage()); + callback.failure(500, error.toString()); + } + return false; + } + + @Override + public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { } + }, true); + } + }); } @@ -349,7 +585,7 @@ public class MainApplication { } } - private static WindowOperationHandler createOperationHandler() { + public static WindowOperationHandler createOperationHandler() { return new WindowOperationHandler.Builder() .withDefaultOperations() .build(); diff --git a/src/main/java/com/axis/innovators/box/browser/util/TerminalManager.java b/src/main/java/com/axis/innovators/box/browser/util/TerminalManager.java new file mode 100644 index 0000000..7bc8ea2 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/browser/util/TerminalManager.java @@ -0,0 +1,226 @@ +package com.axis.innovators.box.browser.util; + +import com.axis.innovators.box.browser.BrowserWindow; +import com.axis.innovators.box.browser.BrowserWindowJDialog; +import com.axis.innovators.box.browser.WindowRegistry; +import com.axis.innovators.box.tools.FolderCreator; +import com.pty4j.PtyProcess; +import com.pty4j.PtyProcessBuilder; +import com.pty4j.WinSize; +import org.cef.browser.CefBrowser; +import org.cef.browser.CefFrame; +import org.cef.browser.CefMessageRouter; +import org.cef.callback.CefQueryCallback; +import org.cef.handler.CefMessageRouterHandlerAdapter; +import org.json.JSONObject; + +import javax.swing.*; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static com.axis.innovators.box.browser.MainApplication.createOperationHandler; + +public class TerminalManager { + + // 保存 PTY 进程引用 + private static PtyProcess ptyProcess; + private static OutputStream ptyInput; + private static Thread outputReaderThread; + + /** + * 启动真实的终端窗口 + */ + public static void popupRealLinuxWindow() { + AtomicReference window = new AtomicReference<>(); + + SwingUtilities.invokeLater(() -> { + // 1. 创建窗口 + WindowRegistry.getInstance().createNewWindow("real_terminal", builder -> + window.set(builder.title("Terminal Linux") + .size(900, 600) + .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "LinuxTerminal.html") + .operationHandler(createOperationHandler()) + .build()) + ); + + // 2. 初始化 PTY 进程 + startPtyProcess(window.get().getBrowser()); + + // 3. 注册 JCEF 消息处理器 + CefMessageRouter msgRouter = window.get().getMsgRouter(); + if (msgRouter != null) { + msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { + @Override + public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, + String request, boolean persistent, CefQueryCallback callback) { + try { + JSONObject json = new JSONObject(request); + if ("terminalInput".equals(json.optString("type"))) { + // 接收前端的按键数据 + String data = json.getString("data"); + if (ptyInput != null) { + // 写入到 Shell 进程的标准输入 + ptyInput.write(data.getBytes(StandardCharsets.UTF_8)); + ptyInput.flush(); + } + callback.success(""); + return true; + } + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + @Override + public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) {} + }, true); + } + + // 窗口关闭时杀死进程 + window.get().addWindowListener(new java.awt.event.WindowAdapter() { + @Override + public void windowClosed(java.awt.event.WindowEvent windowEvent) { + stopPtyProcess(); + } + }); + }); + } + + private static void startPtyProcess(CefBrowser browser) { + try { + // 1. 确定要运行的 Shell 命令 + String[] cmd = determineShellCommand(); + + // 2. 设置环境变量 + Map envs = new HashMap<>(System.getenv()); + envs.put("TERM", "xterm-256color"); // 告诉 Shell 我们支持颜色 + + // 3. 启动 PTY 进程 + ptyProcess = new PtyProcessBuilder(cmd) + .setEnvironment(envs) + .setRedirectErrorStream(true) + .start(); + + // 设置初始窗口大小 (列, 行) + ptyProcess.setWinSize(new WinSize(80, 24)); + + ptyInput = ptyProcess.getOutputStream(); + InputStream ptyOutput = ptyProcess.getInputStream(); + + // 4. 开启后台线程,不断读取 Shell 的输出,并发给前端 + outputReaderThread = new Thread(() -> { + byte[] buffer = new byte[1024]; + int read; + try { + while ((read = ptyOutput.read(buffer)) != -1) { + // 读取原生字节流 + byte[] chunk = new byte[read]; + System.arraycopy(buffer, 0, chunk, 0, read); + + // 转为 Base64 避免特殊字符搞崩 JS + String base64Data = Base64.getEncoder().encodeToString(chunk); + + // 调用前端 JS: writeToTerminal('Base64Str') + String js = "writeToTerminal('" + base64Data + "');"; + + SwingUtilities.invokeLater(() -> browser.executeJavaScript(js, "", 0)); + } + } catch (Exception e) { + // 进程结束或流关闭 + } + }); + outputReaderThread.setDaemon(true); + outputReaderThread.start(); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void stopPtyProcess() { + if (ptyProcess != null && ptyProcess.isAlive()) { + ptyProcess.destroy(); + } + } + + /** + * 检查WSL是否已安装 + * @return 如果wsl.exe存在且能执行,则返回true + */ + private static boolean isWslInstalled() { + try { + Process process = new ProcessBuilder("wsl", "--status").start(); + int exitCode = process.waitFor(); + return true; + } catch (IOException e) { + return false; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + /** + * 尝试以管理员权限启动一个进程来安装WSL + */ + private static void tryInstallWsl() { + String message = "您似乎没有安装WSL (Windows Subsystem for Linux)。\n" + + "WSL提供了更完整的Linux体验,强烈建议您安装。\n\n" + + "是否现在尝试自动安装?\n" + + "(这将会弹出一个UAC窗口请求管理员权限)"; + + int choice = JOptionPane.showConfirmDialog(null, message, "安装WSL", + JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE); + + if (choice == JOptionPane.YES_OPTION) { + try { + // 在Windows上,使用 "runas" 谓词(verb)可以通过ShellExecute来请求提权。 + // Java没有直接访问ShellExecute的方法,但我们可以通过PowerShell的Start-Process命令来间接实现。 + // Start-Process -Verb runAs 可以触发UAC弹窗。 + String command = "powershell.exe -Command \"Start-Process wsl -ArgumentList '--install' -Verb runAs\""; + Runtime.getRuntime().exec(command); + } catch (IOException e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(null, + "启动安装程序失败,请尝试手动以管理员身份运行 'wsl --install'。", + "错误", JOptionPane.ERROR_MESSAGE); + } + } + } + + /** + * 根据操作系统决定启动什么 Shell + */ + private static String[] determineShellCommand() { + String os = System.getProperty("os.name").toLowerCase(); + + if (os.contains("win")) { + // 1. 检查WSL是否安装 + if (isWslInstalled()) { + System.out.println("检测到WSL,将使用WSL。"); + return new String[]{"wsl.exe"}; + } + + // 2. 如果没有安装,提示用户安装 + System.out.println("未检测到WSL,提示用户安装。"); + // 在Swing线程中弹出对话框 + SwingUtilities.invokeLater(TerminalManager::tryInstallWsl); + + // 3. 无论用户是否选择安装,本次都先回退到PowerShell,避免阻塞程序 + // 用户安装完毕并重启应用后,下次启动就会自动使用WSL了。 + System.out.println("本次将回退到PowerShell。"); + return new String[]{"powershell.exe"}; + + } else { + // Mac / Linux + return new String[]{"/bin/bash", "-i"}; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/axis/innovators/box/decompilation/gui/ModernJarViewer.java b/src/main/java/com/axis/innovators/box/decompilation/gui/ModernJarViewer.java index 57881c6..b2c16b9 100644 --- a/src/main/java/com/axis/innovators/box/decompilation/gui/ModernJarViewer.java +++ b/src/main/java/com/axis/innovators/box/decompilation/gui/ModernJarViewer.java @@ -1,3656 +1,775 @@ package com.axis.innovators.box.decompilation.gui; -import com.axis.innovators.box.AxisInnovatorsBox; -import com.axis.innovators.box.window.LoadIcon; +import com.axis.innovators.box.browser.*; +import com.axis.innovators.box.tools.FolderCreator; import com.axis.innovators.box.util.AdvancedJFileChooser; -import com.github.javaparser.JavaParser; -import com.github.javaparser.ParseResult; -import com.github.javaparser.Range; -import com.github.javaparser.ast.CompilationUnit; -import com.github.javaparser.ast.body.FieldDeclaration; -import com.github.javaparser.ast.body.MethodDeclaration; -import com.github.javaparser.ast.expr.*; -import com.strobel.assembler.metadata.Buffer; -import com.strobel.assembler.metadata.ITypeLoader; -import com.strobel.decompiler.Decompiler; -import com.strobel.decompiler.DecompilerSettings; -import com.strobel.decompiler.PlainTextOutput; -import org.apache.commons.compress.utils.IOUtils; import org.benf.cfr.reader.api.CfrDriver; -import org.fife.ui.rsyntaxtextarea.*; -import org.fife.ui.rtextarea.RTextScrollPane; +import org.benf.cfr.reader.api.OutputSinkFactory; +import org.cef.browser.CefBrowser; +import org.cef.browser.CefFrame; +import org.cef.browser.CefMessageRouter; +import org.cef.callback.CefQueryCallback; +import org.cef.handler.CefMessageRouterHandlerAdapter; +import org.json.JSONArray; +import org.json.JSONObject; -import javax.sound.sampled.*; import javax.swing.*; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; -import javax.swing.text.BadLocationException; -import javax.swing.text.Document; -import javax.swing.tree.*; -import java.awt.*; -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.Transferable; -import java.awt.datatransfer.UnsupportedFlavorException; -import java.awt.event.*; -import java.awt.image.BufferedImage; +import javax.swing.filechooser.FileNameExtensionFilter; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.*; -import java.util.List; -import java.util.concurrent.*; -import java.util.jar.*; -import java.util.regex.Matcher; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; import java.util.regex.Pattern; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipInputStream; -import java.util.zip.ZipOutputStream; -import javax.imageio.ImageIO; -import java.util.Base64; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -public class ModernJarViewer extends JFrame { - // UI - private JTree fileTree; - private JTabbedPane openTabs; - private DefaultMutableTreeNode root; +public class ModernJarViewer { - // 数据 - private static File currentJarFile; - private final Map methodIndex = new ConcurrentHashMap<>(); - private final Map> globalIndex = new ConcurrentHashMap<>(); - private final Map deobfMap = new ConcurrentHashMap<>(); - private final Map overrideContents = new ConcurrentHashMap<>(); // entryName -> content (text) - private final Map fullCodeCache = new ConcurrentHashMap<>(); // entryName -> full code (decompiled/source) - private final Map binaryContentCache = new ConcurrentHashMap<>(); // 二进制文件内容缓存 + // 全局状态 + private static JarFile currentJarFile; + private static File currentJarFileObj; // 保存File对象以便重载 + private static final SymbolIndex globalIndex = new SymbolIndex(); + // 临时文件缓存,用于拖拽导出 + private static final Map tempFileCache = new ConcurrentHashMap<>(); - // open editors - private final Map openEditors = new ConcurrentHashMap<>(); - private final Map entryToComponent = new ConcurrentHashMap<>(); - private final Map imageViewers = new ConcurrentHashMap<>(); - - /** 索引文件位置(可以由用户设置) */ - private File indexFileLocation = new File(System.getProperty("user.home"), ".modernjarviewer.index"); - /** 配置文件(保存 indexFileLocation 的路径等) */ - private final File configPropsFile = new File(System.getProperty("user.home"), ".modernjarviewer.properties"); - - // JavaParser - private final JavaParser javaParser = new JavaParser(); - - // last mouse point for popup usage - private Point lastMousePoint = null; - - private String currentDecompiler = "CFR"; - - private volatile List deobfPatterns = Collections.emptyList(); - private volatile List deobfReplacements = Collections.emptyList(); - private final Map> inlineAnnotations = new ConcurrentHashMap<>(); - private final Object annotationsLock = new Object(); - - public ModernJarViewer(Frame owner) { - setTitle("Jar反编译工具"); - setDefaultCloseOperation(DISPOSE_ON_CLOSE); - initComponents(); + // --- Main Entry --- + public static void main(String[] args) { + // 支持命令行参数启动,或者默认为 null + String startPath = (args.length > 0) ? args[0] : null; + popupSimulatingWindow(null, startPath); } - public ModernJarViewer(Frame owner, String jarFile) { - setTitle("Jar反编译工具"); - setDefaultCloseOperation(EXIT_ON_CLOSE); - initComponents(); - if (jarFile != null && !jarFile.isEmpty()) { - // 在后台加载 jar,避免主线程卡死 - runBackground("加载 JAR...", () -> loadJar(new File(jarFile))); - } - } + // --- CEF Window Initialization --- + // 修改:增加 jarPath 参数 + public static void popupSimulatingWindow(JFrame parent, String jarPath) { + AtomicReference windowRef = new AtomicReference<>(); - private void initComponents() { - setSize(1280, 800); - setLocationRelativeTo(null); - setLayout(new BorderLayout()); - try { - setIconImage(LoadIcon.loadIcon("logo.png", 32).getImage()); - } catch (Exception ignored) {} - - // 菜单 - JMenuBar menuBar = new JMenuBar(); - JMenu fileMenu = new JMenu("文件"); - JMenuItem openItem = new JMenuItem("打开 JAR..."); - openItem.addActionListener(e -> openJarFile()); - fileMenu.add(openItem); - menuBar.add(fileMenu); - - // 新增设置菜单 - JMenu settingsMenu = new JMenu("设置"); - - // 索引子菜单 - JMenu indexMenu = new JMenu("索引"); - - JMenuItem clearIndexItem = new JMenuItem("清除索引"); - clearIndexItem.addActionListener(e -> clearIndex()); - indexMenu.add(clearIndexItem); - - JMenuItem indexSizeItem = new JMenuItem("索引文件大小"); - indexSizeItem.addActionListener(e -> showIndexSize()); - indexMenu.add(indexSizeItem); - - settingsMenu.add(indexMenu); - - // 反混淆器子菜单 - JMenu deobfuscatorMenu = new JMenu("反混淆器"); - ButtonGroup decompilerGroup = new ButtonGroup(); - - JRadioButtonMenuItem cfrItem = new JRadioButtonMenuItem("CFR 0.152"); - cfrItem.setSelected(true); - cfrItem.addActionListener(e -> setDecompiler("CFR")); - decompilerGroup.add(cfrItem); - deobfuscatorMenu.add(cfrItem); - - // 预留其他反混淆器选项 - JRadioButtonMenuItem fernflowerItem = new JRadioButtonMenuItem("Fernflower"); - fernflowerItem.setEnabled(true); - fernflowerItem.addActionListener(e -> setDecompiler("Fernflower")); - decompilerGroup.add(fernflowerItem); - deobfuscatorMenu.add(fernflowerItem); - - JRadioButtonMenuItem procyonItem = new JRadioButtonMenuItem("Procyon"); - procyonItem.setEnabled(true); - procyonItem.addActionListener(e -> setDecompiler("Procyon")); - decompilerGroup.add(procyonItem); - deobfuscatorMenu.add(procyonItem); - - settingsMenu.add(deobfuscatorMenu); - - // 帮助菜单项 - JMenuItem helpItem = new JMenuItem("帮助"); - helpItem.addActionListener(e -> showHelp()); - settingsMenu.add(helpItem); - - menuBar.add(settingsMenu); - - JMenu tools = new JMenu("工具"); - JMenuItem buildIndex = new JMenuItem("构建索引"); - buildIndex.addActionListener(e -> runBackground("构建索引...", this::buildGlobalIndexWithCache)); - tools.add(buildIndex); - - JMenuItem loadMap = new JMenuItem("加载混淆表..."); - loadMap.addActionListener(e -> { - JFileChooser chooser = new JFileChooser(); - if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { - File f = chooser.getSelectedFile(); - runBackground("加载混淆表并应用...", () -> loadObfuscationMapBlocking(f)); - } - }); - tools.add(loadMap); - - JMenuItem setIndexLocation = new JMenuItem("设置索引文件位置..."); - setIndexLocation.addActionListener(e -> { - JFileChooser chooser = new JFileChooser(); - chooser.setDialogTitle("选择索引文件保存位置(会覆盖或新建)"); - chooser.setSelectedFile(indexFileLocation); - if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { - indexFileLocation = chooser.getSelectedFile(); - saveConfigProperties(); - JOptionPane.showMessageDialog(this, "索引文件位置已保存为:\n" + indexFileLocation.getAbsolutePath(), "已保存", JOptionPane.INFORMATION_MESSAGE); - } - }); - tools.add(setIndexLocation); - - JMenuItem clearIndex = new JMenuItem("清理索引文件(删除)"); - clearIndex.addActionListener(e -> { - if (indexFileLocation != null && indexFileLocation.exists()) { - if (JOptionPane.showConfirmDialog(this, "确定删除索引文件?", "确认", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { - if (indexFileLocation.delete()) { - JOptionPane.showMessageDialog(this, "索引文件删除成功", "完成", JOptionPane.INFORMATION_MESSAGE); - } else { - JOptionPane.showMessageDialog(this, "索引文件删除失败,请手动删除: " + indexFileLocation.getAbsolutePath(), "失败", JOptionPane.ERROR_MESSAGE); - } - } - } else { - JOptionPane.showMessageDialog(this, "没有可删除的索引文件", "信息", JOptionPane.INFORMATION_MESSAGE); - } - }); - tools.add(clearIndex); - - JMenuItem exportWorkspace = new JMenuItem("导出 Java 工作间..."); - exportWorkspace.addActionListener(e -> { - JFileChooser chooser = new JFileChooser(); - chooser.setSelectedFile(new File("workspace.zip")); - if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { - File out = chooser.getSelectedFile(); - runBackground("导出 Java 工作间...", () -> { - try { - exportJavaWorkspaceBlocking(out); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - }); - } - }); - tools.add(exportWorkspace); - - menuBar.add(tools); - setJMenuBar(menuBar); - - // 左侧树 - root = new DefaultMutableTreeNode("根目录"); - fileTree = new JTree(new DefaultTreeModel(root)); - fileTree.setRootVisible(false); - fileTree.setShowsRootHandles(true); // 显示折叠句柄 - fileTree.setRowHeight(20); - fileTree.addTreeSelectionListener(new TreeSelectionHandler()); // 只响应选择(不自动打开) - - // 启用拖拽支持 - fileTree.setDragEnabled(true); - fileTree.setTransferHandler(new TreeTransferHandler()); - - // 双击行为 & 树右键(双击打开文件/折叠目录) - fileTree.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - TreePath path = fileTree.getPathForLocation(e.getX(), e.getY()); - if (path == null) return; - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - - if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e)) { - if (node.getChildCount() > 0) { - if (fileTree.isExpanded(path)) fileTree.collapsePath(path); - else fileTree.expandPath(path); - } else { - // 文件:打开 - String entryPath = buildEntryPath(path); - openEntryInTab(entryPath); - } - } - super.mouseClicked(e); - } - }); - registerFileIcons(); - - JScrollPane treeScroll = new JScrollPane(fileTree); - treeScroll.setPreferredSize(new Dimension(320, 700)); - - // 右侧 tabs - openTabs = new JTabbedPane(); - openTabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); - - openTabs.addChangeListener(e -> { - // 在 EDT 中执行 - SwingUtilities.invokeLater(() -> { - RSyntaxTextArea ed = getCurrentEditor(); - if (ed == null) return; - String entry = getEntryNameForEditor(ed); - if (entry != null) { - applyAnnotationsToEditor(entry, ed); - } + SwingUtilities.invokeLater(() -> { + // 1. 创建浏览器窗口 + WindowRegistry.getInstance().createNewWindow("main", builder -> { + BrowserWindow window = builder.title("Java Decompiler Pro") + .size(1400, 900) + .htmlPath(FolderCreator.getJavaScriptFolder() + File.separator + "HtmlJarViewer.html") + .operationHandler(new WindowOperationHandler.Builder().withDefaultOperations().build()) + .build(); + windowRef.set(window); }); - }); - JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, treeScroll, openTabs); - split.setDividerLocation(300); - add(split, BorderLayout.CENTER); + // 2. 配置消息路由 (JS -> Java Bridge) + BrowserWindow window = windowRef.get(); + CefMessageRouter msgRouter = window.getMsgRouter(); - setupKeyBindings(); - } + if (msgRouter != null) { + msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { + @Override + public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, + String request, boolean persistent, CefQueryCallback callback) { + try { + JSONObject json = new JSONObject(request); + String type = json.optString("type"); - private static boolean isDarkTheme() { - if (AxisInnovatorsBox.getMain() == null){ - return false; - } - return AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode(); - } - private File getAnnotationsFileForJar() { - if (currentJarFile == null) { - // fallback 存到 config 文件同目录或者用户家目录 - File parent = configPropsFile.getParentFile(); - if (parent == null) parent = new File(System.getProperty("user.home")); - return new File(parent, "annotations_global.properties"); - } - File parent = configPropsFile.getParentFile(); - if (parent == null) parent = new File(System.getProperty("user.home")); - String key = sha1Hex(currentJarFile.getAbsolutePath()); - return new File(parent, "annotations_" + key + ".properties"); - } - - private String sha1Hex(String s) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-1"); - byte[] b = md.digest(s.getBytes(StandardCharsets.UTF_8)); - StringBuilder sb = new StringBuilder(b.length * 2); - for (byte by : b) { - sb.append(String.format("%02x", by & 0xff)); - } - return sb.toString(); - } catch (NoSuchAlgorithmException ex) { - // 不太可能发生 - return Integer.toHexString(s.hashCode()); - } - } - - - private void saveAnnotationsToDisk() { - File f = getAnnotationsFileForJar(); - synchronized (annotationsLock) { - Properties p = new Properties(); - try { - for (Map.Entry> fe : inlineAnnotations.entrySet()) { - String entryName = fe.getKey(); - String enc = Base64.getUrlEncoder().withoutPadding().encodeToString(entryName.getBytes(StandardCharsets.UTF_8)); - for (Map.Entry le : fe.getValue().entrySet()) { - String key = enc + "#" + le.getKey(); - String val = le.getValue(); - if (val == null) val = ""; - p.setProperty(key, val); - } - } - try (FileOutputStream fos = new FileOutputStream(f)) { - p.store(fos, "ModernJarViewer Annotations for jar: " + (currentJarFile == null ? "" : currentJarFile.getAbsolutePath())); - } - } catch (Exception ex) { - ex.printStackTrace(); - } - } - } - - private void loadAnnotationsFromDisk() { - File f = getAnnotationsFileForJar(); - if (!f.exists()) return; - synchronized (annotationsLock) { - Properties p = new Properties(); - try (FileInputStream fis = new FileInputStream(f)) { - p.load(fis); - inlineAnnotations.clear(); - for (String k : p.stringPropertyNames()) { - String v = p.getProperty(k, ""); - int sharp = k.indexOf('#'); - if (sharp < 0) continue; - String enc = k.substring(0, sharp); - String lineStr = k.substring(sharp + 1); - int line = 0; - try { line = Integer.parseInt(lineStr); } catch (Exception ex) { continue; } - String entryName; - try { - entryName = new String(Base64.getUrlDecoder().decode(enc), StandardCharsets.UTF_8); - } catch (Exception ex) { - continue; - } - Map fm = inlineAnnotations.computeIfAbsent(entryName, e -> new ConcurrentHashMap<>()); - fm.put(line, v); - } - // 在 EDT 中应用到当前已打开的编辑器(如果有) - SwingUtilities.invokeLater(() -> { - try { - for (Map.Entry e : openEditors.entrySet()) { - String entry = e.getKey(); - RSyntaxTextArea editor = e.getValue(); - if (entry != null && editor != null) { - applyAnnotationsToEditor(entry, editor); + switch (type) { + case "openJar": + // 支持从前端传路径(拖拽进入)或弹窗选择 + String path = json.optString("path", null); + handleOpenJar(parent, path, callback); + return true; + case "getFile": + handleGetFile(json.optString("path"), callback); + return true; + case "prepareDrag": // 新增:预处理拖拽导出 + handlePrepareDrag(json.optString("path"), callback); + return true; + case "searchContent": + String q = json.optString("query"); + String m = json.optString("mode", "content"); + handleSearchContent(q, m, callback); + return true; + case "findDefinition": + handleFindDefinition(json.optString("word"), callback); + return true; + default: + return false; } + } catch (Exception e) { + e.printStackTrace(); + callback.failure(500, "Java Error: " + e.getMessage()); + return true; } - } catch (Throwable ex) { - ex.printStackTrace(); } - }); - } catch (Exception ex) { - ex.printStackTrace(); + }, true); + + // 3. 如果启动时传入了路径,等待页面加载完成后自动打开 + if (jarPath != null && !jarPath.isEmpty()) { + // 简单的延迟加载,或者监听 onLoadEnd (这里简化处理) + new Thread(() -> { + try { Thread.sleep(1000); } catch (InterruptedException e) {} + File f = new File(jarPath); + if (f.exists()) { + loadJarAndRespond(f, null); // null callback means push event via browser.executeJavaScript if needed + // 由于这里是初始化,我们通过 JS 注入来触发前端刷新 + SwingUtilities.invokeLater(() -> { + // 构造模拟的请求来复用逻辑,或者让前端就绪后主动查询 + // 这里为了简单,我们假设前端有个 initLoad 方法,或者通过 openJar 接口再次触发 + // 更好的方式是前端 ready 后调用 java,这里不作复杂处理,仅展示逻辑 + System.out.println("Auto opening: " + jarPath); + }); + } + }).start(); + } } - } + }); } - private void applyAnnotationsToEditor(String entryName, RSyntaxTextArea editor) { - if (entryName == null || editor == null) return; - Map fileMap = inlineAnnotations.get(entryName); - if (fileMap == null || fileMap.isEmpty()) return; - // 按行号从小到大插入(避免插入早期注解改变后续行号) - List lines = new ArrayList<>(fileMap.keySet()); - Collections.sort(lines); - // 需要在 EDT 操作文档 + // ========================================== + // Request Handlers + // ========================================== + + /** + * 打开 JAR 文件 + * @param manualPath 如果不为null,则直接打开该路径(用于拖拽进入或启动参数) + */ + private static void handleOpenJar(JFrame parent, String manualPath, CefQueryCallback callback) { SwingUtilities.invokeLater(() -> { try { - Document doc = editor.getDocument(); - // 为避免行号错误,采用偏移累加的方式:插入之前记录每次插入后偏移增量 - int delta = 0; - for (int line : lines) { - String text = fileMap.get(line); - if (text == null) continue; - int targetLine = line; - try { - // 如果目标行超出当前行数,则追加到文档末尾 - if (targetLine >= editor.getLineCount()) targetLine = editor.getLineCount() - 1; - if (targetLine < 0) targetLine = 0; - int lineStart = editor.getLineStartOffset(targetLine); - int lineEnd = editor.getLineEndOffset(targetLine); - String lineText = doc.getText(lineStart, Math.max(0, lineEnd - lineStart)); - - // 先尝试删除已存在注释块(避免重复) - // 这里复用之前的删除逻辑:查找同行 // 并删除连续对齐注释 - int idx = lineText.indexOf("//"); - if (idx >= 0) { - int commentStart = lineStart + idx; - int removeStart = commentStart; - int scanLine = targetLine + 1; - int removeEnd = lineEnd; - while (scanLine <= editor.getLineCount() - 1) { - int s = editor.getLineStartOffset(scanLine); - int e = editor.getLineEndOffset(scanLine); - String t = doc.getText(s, Math.max(0, e - s)); - int nonSpace = 0; - while (nonSpace < t.length() && (t.charAt(nonSpace) == ' ' || t.charAt(nonSpace) == '\t')) nonSpace++; - if (t.substring(nonSpace).startsWith("//")) { - removeEnd = e; - scanLine++; - } else break; - } - if (removeEnd > removeStart) { - doc.remove(removeStart, removeEnd - removeStart); - } - } - // 插入注解(复用 buildInlineCommentString) - String insert = buildInlineCommentString(editor, lineText, text); - int insertPos = Math.min(doc.getLength(), lineStart + rtrim(lineText).length()); - doc.insertString(insertPos, insert, null); - // 更新 delta 并继续 - } catch (BadLocationException ex) { - ex.printStackTrace(); - } - } - } catch (Exception ex) { - ex.printStackTrace(); - } - }); - } - - - private void precompileDeobfPatterns() { - Map map = deobfMap; // 快照引用 - if (map == null || map.isEmpty()) { - deobfPatterns = Collections.emptyList(); - deobfReplacements = Collections.emptyList(); - return; - } - List ps = new ArrayList<>(map.size()); - List rs = new ArrayList<>(map.size()); - for (Map.Entry e : map.entrySet()) { - String k = e.getKey(); - String v = e.getValue(); - if (k == null || k.isEmpty() || v == null) continue; - try { - // 只匹配完整单词(类名/标识符替换常用) - Pattern p = Pattern.compile("\\b" + Pattern.quote(k) + "\\b"); - ps.add(p); - rs.add(v); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - deobfPatterns = ps; - deobfReplacements = rs; - } - - private void setDecompiler(String decompiler) { - currentDecompiler = decompiler; - loadJar(currentJarFile); - JOptionPane.showMessageDialog(this, "已设置反混淆器为: " + decompiler, "设置", JOptionPane.INFORMATION_MESSAGE); - } - - // ---------- 4) 新增:exportJavaWorkspaceBlocking 实现(把 .java / 反编译结果写入 zip,扩展名为 .java) ---------- - private void exportJavaWorkspaceBlocking(File out) throws IOException { - if (currentJarFile == null) throw new IOException("当前没有打开的 JAR"); - - // 预编译反混淆规则(仅做一次) - precompileDeobfPatterns(); - - // 收集条目并排序(稳定顺序) - List entries; - try (JarFile jf = new JarFile(currentJarFile)) { - entries = Collections.list(jf.entries()); - } - - entries.sort(Comparator.comparing(JarEntry::getName)); - - // 线程池(保守使用 CPU-1,不要完全占满机器) - int cpus = Runtime.getRuntime().availableProcessors(); - int threads = Math.max(1, Math.min(cpus - 1, 4)); // 限制到 4,避免反编译器线程安全问题 - ExecutorService pool = Executors.newFixedThreadPool(threads); - - // 为每个条目提交任务,任务负责读取/反编译/反混淆并返回 ExportResult - List> futures = new ArrayList<>(entries.size()); - try (JarFile jar = new JarFile(currentJarFile); - FileOutputStream fos = new FileOutputStream(out); - BufferedOutputStream bos = new BufferedOutputStream(fos); - ZipOutputStream zos = new ZipOutputStream(bos)) { - - for (JarEntry je : entries) { - // 提交任务 - futures.add(pool.submit(() -> { - String name = je.getName().replace('\\', '/'); - if (je.isDirectory()) { - // 目录条目,data 为 null - String dirName = name.endsWith("/") ? name : name + "/"; - return new ExportResult(dirName, null, je.getTime() >= 0 ? je.getTime() : System.currentTimeMillis()); - } - - // 处理 .java(原始) .class(反编译) 其它资源(原样) - try { - if (name.endsWith(".java")) { - String src = getContentForEntry(name, jar); - if (src == null) src = ""; - // 反混淆内容与路径 - src = applyDeobfToContent(src); - String outName = applyDeobfToPath(name); - return new ExportResult(outName, src.getBytes(StandardCharsets.UTF_8), - je.getTime() >= 0 ? je.getTime() : System.currentTimeMillis()); - } else if (name.endsWith(".class")) { - String logical = name.substring(0, name.length() - 6); // path without .class - // 去除尾部纯数字匿名类标识($10, $10$1 ...),把匿名类归并到外部类,减少重复 - String stripped = logical.replaceAll("\\$\\d+(?:\\$\\d+)*$", ""); - String targetPath = stripped + ".java"; - // 优先从 fullCodeCache 取,若无则走反编译 getContentForEntry(你的方法应能反编译) - String content = fullCodeCache.get(name); - if (content == null) content = getContentForEntry(name, jar); - if (content == null) content = ""; - content = applyDeobfToContent(content); - targetPath = applyDeobfToPath(targetPath); - return new ExportResult(targetPath, content.getBytes(StandardCharsets.UTF_8), - je.getTime() >= 0 ? je.getTime() : System.currentTimeMillis()); - } else { - // 资源文件:二进制读取 - try (InputStream is = jar.getInputStream(je); - ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - if (is != null) { - byte[] buf = new byte[8192]; - int r; - while ((r = is.read(buf)) != -1) baos.write(buf, 0, r); - } - byte[] data = baos.toByteArray(); - // 资源路径也尝试做简单反混淆 - String outName = applyDeobfToPath(name); - return new ExportResult(outName, data, je.getTime() >= 0 ? je.getTime() : System.currentTimeMillis()); - } - } - } catch (Throwable ex) { - // 单条目处理失败时返回一个空文件,以不中断整个导出流程(同时记录异常) - ex.printStackTrace(); - String fallbackName = applyDeobfToPath(name); - return new ExportResult(fallbackName, new byte[0], System.currentTimeMillis()); - } - })); - } - - // 顺序写入 Zip(保持 entries 排序与目录结构),并避免重复写入同一名字 - Set seen = new HashSet<>(); - for (int i = 0; i < futures.size(); i++) { - Future f = futures.get(i); - ExportResult er; - try { - er = f.get(); // 等待该条目处理完成(但其它条目已在并行处理中) - } catch (Exception ex) { - ex.printStackTrace(); - continue; - } - if (er == null) continue; - String outName = er.entryName.replace('\\', '/'); - if (er.data == null) { - // 目录 - String dirName = outName.endsWith("/") ? outName : outName + "/"; - if (!seen.contains(dirName)) { - ZipEntry ze = new ZipEntry(dirName); - ze.setTime(er.time); - zos.putNextEntry(ze); - zos.closeEntry(); - seen.add(dirName); - } + File file; + if (manualPath != null && !manualPath.isEmpty()) { + file = new File(manualPath); } else { - // 避免匿名类导致的重复(如果路径是由 class stripping 产生的重复,跳过重复匿名) - boolean wasAnonymous = outName.matches(".*\\$\\d+(?:\\$\\d+)*\\.java$"); - if (seen.contains(outName)) { - if (wasAnonymous) { - // 匿名类已被合并到外部类,跳过 - continue; - } - // 冲突则加 _dupN 后缀 - String base = outName; - int dot = base.lastIndexOf('.'); - String fallback; - int dup = 1; - do { - if (dot > 0) fallback = base.substring(0, dot) + "_dup" + dup + base.substring(dot); - else fallback = base + "_dup" + dup; - dup++; - } while (seen.contains(fallback)); - outName = fallback; + AdvancedJFileChooser chooser = new AdvancedJFileChooser(); + chooser.setFileFilter(new FileNameExtensionFilter("JAR Files", "jar", "zip", "war")); + chooser.setDialogTitle("Select JAR"); + if (chooser.showOpenDialog(parent) != JFileChooser.APPROVE_OPTION) { + if (callback != null) callback.failure(404, "Cancelled"); + return; } - ZipEntry ze = new ZipEntry(outName); - ze.setTime(er.time); - zos.putNextEntry(ze); - if (er.data.length > 0) zos.write(er.data); - zos.closeEntry(); - seen.add(outName); + file = chooser.getSelectedFile(); } - } - } finally { - // 关闭线程池 - pool.shutdownNow(); - try { pool.awaitTermination(2, TimeUnit.SECONDS); } catch (InterruptedException ignored) {} - } - } - /** 对源码文本执行反混淆替换(安全替换,使用单词边界) */ - private String applyDeobfToContent(String content) { - if (content == null || content.isEmpty()) return content; - List ps = deobfPatterns; - List rs = deobfReplacements; - if (ps == null || ps.isEmpty()) return content; - StringBuilder sb = null; - String working = content; - // 使用 Matcher 的 appendReplacement/appendTail 以减少 string 创建(对大文本更友好) - for (int i = 0; i < ps.size(); i++) { - Pattern p = ps.get(i); - String repl = rs.get(i); - try { - Matcher m = p.matcher(working); - if (!m.find()) continue; - if (sb == null) sb = new StringBuilder(working.length() + 128); - sb.setLength(0); - m.reset(); - while (m.find()) { - m.appendReplacement(sb, Matcher.quoteReplacement(repl)); - } - m.appendTail(sb); - working = sb.toString(); - // 重用 sb(下一轮可能继续) - } catch (Exception ex) { - ex.printStackTrace(); - } - } - return working; - } + loadJarAndRespond(file, callback); - private static class ExportResult { - final String entryName; - final byte[] data; // null 表示目录条目 - final long time; - ExportResult(String entryName, byte[] data, long time) { - this.entryName = entryName; - this.data = data; - this.time = time; - } - } - - /** 对路径/文件名做简单的反混淆(尽量只替换包名或类名,不破坏路径结构) */ - private String applyDeobfToPath(String path) { - if (path == null || deobfMap == null || deobfMap.isEmpty()) return path; - String p = path.replace('\\', '/'); - // 先替换典型的包/类出现位置 - for (Map.Entry me : deobfMap.entrySet()) { - String k = me.getKey(); - String v = me.getValue(); - if (k == null || k.isEmpty() || v == null) continue; - try { - // /k/ -> /v/ - p = p.replace("/" + k + "/", "/" + v + "/"); - // /k.java -> /v.java - p = p.replace("/" + k + ".java", "/" + v + ".java"); - // k.java -> v.java - p = p.replace(k + ".java", v + ".java"); - // /k$Inner -> /v$Inner - p = p.replace("/" + k + "$", "/" + v + "$"); - // 如果类名正好在路径末尾 - if (p.endsWith("/" + k)) p = p.substring(0, p.length() - k.length()) + v; - } catch (Exception ex) { - ex.printStackTrace(); - } - } - return p; - } - - // ---------- 3) 新增:配置文件读取/保存方法(用于记住索引文件位置) ---------- - private void loadConfigProperties() { - if (!configPropsFile.exists()) return; - Properties p = new Properties(); - try (FileInputStream fis = new FileInputStream(configPropsFile)) { - p.load(fis); - String idxPath = p.getProperty("indexFilePath"); - if (idxPath != null && !idxPath.isEmpty()) { - indexFileLocation = new File(idxPath); - } - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - private void saveConfigProperties() { - Properties p = new Properties(); - p.setProperty("indexFilePath", indexFileLocation == null ? "" : indexFileLocation.getAbsolutePath()); - try (FileOutputStream fos = new FileOutputStream(configPropsFile)) { - p.store(fos, "ModernJarViewer Config"); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - // ---------- 3) 新增:索引保存(简单文本属性格式) ---------- - private void saveIndexToDisk(File idxFile) throws IOException { - if (idxFile == null) return; - Properties p = new Properties(); - // 记录当前 jar 路径以便匹配 - p.setProperty("jarPath", currentJarFile == null ? "" : currentJarFile.getAbsolutePath()); - p.setProperty("time", String.valueOf(System.currentTimeMillis())); - - // 把 globalIndex 序列化为 properties 键 gidx.=file@pos@text;file@pos@text;... - for (Map.Entry> e : globalIndex.entrySet()) { - String term = e.getKey(); - StringBuilder sb = new StringBuilder(); - for (SearchResult sr : e.getValue()) { - if (sb.length() > 0) sb.append(";"); - sb.append(escapeForIndex(sr.filePath)).append("@").append(sr.position).append("@").append(escapeForIndex(sr.matchText)); - } - p.setProperty("gidx." + term, sb.toString()); - } - - try (FileOutputStream fos = new FileOutputStream(idxFile)) { - p.store(fos, "ModernJarViewer Index"); - } - } - - private Map> loadIndexFromDisk(File idxFile) throws IOException { - Map> loaded = new HashMap<>(); - if (idxFile == null || !idxFile.exists()) return loaded; - Properties p = new Properties(); - try (FileInputStream fis = new FileInputStream(idxFile)) { - p.load(fis); - } - - String jarPath = p.getProperty("jarPath", ""); - // 如果索引关联的 jar 与当前打开的不一致,则返回空(调用方决定是否使用) - if (currentJarFile == null || !currentJarFile.getAbsolutePath().equals(jarPath)) { - return loaded; - } - - for (String name : p.stringPropertyNames()) { - if (!name.startsWith("gidx.")) continue; - String term = name.substring(5); - String value = p.getProperty(name, ""); - if (value.isEmpty()) continue; - String[] items = value.split(";"); - List list = new ArrayList<>(); - for (String it : items) { - String[] parts = it.split("@", 3); - if (parts.length >= 3) { - String fpath = unescapeFromIndex(parts[0]); - int pos = 0; - try { pos = Integer.parseInt(parts[1]); } catch (Exception ex) {} - String mt = unescapeFromIndex(parts[2]); - list.add(new SearchResult(fpath, pos, mt)); - } - } - if (!list.isEmpty()) loaded.put(term, list); - } - return loaded; - } - - // 简单的转义/反转义(用于 index 属性值) - private String escapeForIndex(String s) { - if (s == null) return ""; - return s.replace("\\", "\\\\").replace("@", "\\@").replace(";", "\\;"); - } - private String unescapeFromIndex(String s) { - if (s == null) return ""; - StringBuilder sb = new StringBuilder(); - boolean esc = false; - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (esc) { - sb.append(c); - esc = false; - } else { - if (c == '\\') esc = true; - else sb.append(c); - } - } - return sb.toString(); - } - - - private void clearIndex() { - int result = JOptionPane.showConfirmDialog(this, - "确定要清除所有索引吗?这可能会影响搜索和导航功能。", - "清除索引", - JOptionPane.YES_NO_OPTION); - - if (result == JOptionPane.YES_OPTION) { - methodIndex.clear(); - globalIndex.clear(); - JOptionPane.showMessageDialog(this, "索引已清除", "操作完成", JOptionPane.INFORMATION_MESSAGE); - } - } - - private void showIndexSize() { - long methodIndexSize = estimateSize(methodIndex); - long globalIndexSize = estimateSize(globalIndex); - long totalSize = methodIndexSize + globalIndexSize; - - String sizeInfo = String.format( - "索引大小统计:
" + - "方法索引: %s
" + - "全局索引: %s
" + - "总大小: %s", - formatSize(methodIndexSize), - formatSize(globalIndexSize), - formatSize(totalSize) - ); - - JOptionPane.showMessageDialog(this, sizeInfo, "索引大小", JOptionPane.INFORMATION_MESSAGE); - } - - // 估算对象大小(简化版) - private long estimateSize(Map map) { - // 每个键值对大约100字节的估算(简化处理) - return map.size() * 100L; - } - - // 格式化大小显示 - private String formatSize(long bytes) { - if (bytes < 1024) return bytes + " B"; - int exp = (int) (Math.log(bytes) / Math.log(1024)); - char unit = "KMGTPE".charAt(exp-1); - return String.format("%.1f %sB", bytes / Math.pow(1024, exp), unit); - } - - // 实现帮助功能 - private void showHelp() { - String helpText = "
" + - "

设置帮助

" + - "

索引

" + - "

清除索引: 删除所有已构建的索引数据,释放内存。

" + - "

索引文件大小: 显示当前索引占用的内存大小。

" + - "

反混淆器

" + - "

选择用于反编译.class文件的工具:

" + - "
    " + - "
  • CFR 0.152: 当前支持的反混淆器
  • " + - "
  • Fernflower: IntelliJ IDEA使用的反混淆器(预留)
  • " + - "
  • Procyon: 另一个开源反混淆器
  • " + - "
"; - - JOptionPane.showMessageDialog(this, helpText, "设置帮助", JOptionPane.INFORMATION_MESSAGE); - } - - private void registerFileIcons() { - fileTree.setCellRenderer(new DefaultTreeCellRenderer() { - private final Icon jarIcon = loadThemeAwareIcon("programming/JarApiViewer/file_jar.png", 18); - private final Icon classIcon = loadThemeAwareIcon("programming/JarApiViewer/java_file.png", 18); - private final Icon zipIcon = loadThemeAwareIcon("programming/JarApiViewer/zip_file.png", 12); - private final Icon mcmetaIcon = loadThemeAwareIcon("programming/JarApiViewer/mcmeta_file.png", 12); - private final Icon cfgIcon = loadThemeAwareIcon("programming/JarApiViewer/cfg_file.png", 12); - private final Icon tomlIcon = loadThemeAwareIcon("programming/JarApiViewer/toml_file.png", 12); - private final Icon exeIcon = loadThemeAwareIcon("programming/JarApiViewer/exe_file.png", 12); - private final Icon fileIcon = loadThemeAwareIcon("programming/JarApiViewer/file.png", 12); - - private final Icon folderIcon = UIManager.getIcon("Tree.closedIcon"); - - private Icon loadThemeAwareIcon(String path, int size) { - String actualPath = !isDarkTheme() ? - path.replace(".png", "_dark.png") : - path; - return new ImageIcon(LoadIcon.loadIcon(actualPath, size).getImage()); - } - - @Override - public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, - boolean leaf, int row, boolean hasFocus) { - super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); - DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; - String name = node.getUserObject().toString(); - - if (!leaf && node.getChildCount() > 0) { - setIcon(folderIcon); - } - else if (name.toLowerCase().endsWith(".jar")) { - setIcon(jarIcon); - } else if (name.toLowerCase().endsWith(".class")) { - setIcon(classIcon); - } else if (name.toLowerCase().endsWith(".zip")) { - setIcon(zipIcon); - } else if (name.toLowerCase().endsWith(".mcmeta")) { - setIcon(mcmetaIcon); - } else if (name.toLowerCase().endsWith(".cfg")) { - setIcon(cfgIcon); - } else if (name.toLowerCase().endsWith(".toml")) { - setIcon(tomlIcon); - } else if (name.toLowerCase().endsWith(".exe") || name.toLowerCase().endsWith(".dll")) { - setIcon(exeIcon); - } - else { - setIcon(fileIcon); - } - return this; + } catch (Exception e) { + if (callback != null) callback.failure(500, e.getMessage()); } }); } - // 实现拖拽功能的TransferHandler - private class TreeTransferHandler extends TransferHandler { - @Override - public int getSourceActions(JComponent c) { - return COPY; - } + // 提取公共加载逻辑 + private static void loadJarAndRespond(File file, CefQueryCallback callback) { + try { + if (currentJarFile != null) try { currentJarFile.close(); } catch (Exception ignored) {} + // 清理旧的临时文件 + tempFileCache.clear(); - @Override - protected Transferable createTransferable(JComponent c) { - if (c instanceof JTree) { - JTree tree = (JTree) c; - TreePath path = tree.getSelectionPath(); - if (path != null) { - String entryPath = buildEntryPath(path); - return new FileTransferable(entryPath); + currentJarFileObj = file; + currentJarFile = new JarFile(file); + + JSONObject root = new JSONObject(); + JSONArray files = new JSONArray(); + List classEntries = new ArrayList<>(); + + Enumeration entries = currentJarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.isDirectory()) continue; + + JSONObject item = new JSONObject(); + String entryName = entry.getName(); + item.put("path", entryName); + String name = entryName.contains("/") ? entryName.substring(entryName.lastIndexOf('/') + 1) : entryName; + item.put("name", name); + files.put(item); + + if (name.endsWith(".class")) { + classEntries.add(entry); } } - return null; + root.put("files", files); + root.put("jarName", file.getName()); + root.put("fullPath", file.getAbsolutePath()); // 回传给前端显示 + + // --- 启动后台分析 (构建索引) --- + new Thread(() -> JarAnalyzer.analyze(currentJarFile, classEntries, globalIndex)).start(); + + if (callback != null) callback.success(root.toString()); + } catch (IOException e) { + if (callback != null) callback.failure(500, "Failed to open JAR: " + e.getMessage()); } } - private static class FileTransferable implements Transferable { - private final String entryPath; + /** + * 获取文件内容: + * 修改:增加对音频、视频、图片的支持 + */ + private static void handleGetFile(String path, CefQueryCallback callback) { + new Thread(() -> { + try { + if (currentJarFile == null) throw new IOException("No JAR opened"); + JarEntry entry = currentJarFile.getJarEntry(path); + if (entry == null) { callback.failure(404, "File not found"); return; } - public FileTransferable(String entryPath) { - this.entryPath = entryPath; - } + JSONObject res = new JSONObject(); + res.put("path", path); + String lowerPath = path.toLowerCase(); + String ext = path.contains(".") ? path.substring(path.lastIndexOf('.') + 1).toLowerCase() : ""; - @Override - public DataFlavor[] getTransferDataFlavors() { - return new DataFlavor[]{DataFlavor.javaFileListFlavor}; - } - - @Override - public boolean isDataFlavorSupported(DataFlavor flavor) { - return flavor.equals(DataFlavor.javaFileListFlavor); - } - - @Override - public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { - if (!isDataFlavorSupported(flavor)) { - throw new UnsupportedFlavorException(flavor); - } - - // 创建临时文件并返回 - File tempFile = File.createTempFile("export_", "_" + new File(entryPath).getName()); - try (FileOutputStream fos = new FileOutputStream(tempFile)) { - byte[] data = getEntryContent(entryPath); - if (data != null) { - fos.write(data); + // 1. Class 反编译 + if (ext.equals("class")) { + res.put("type", "java"); + res.put("content", decompileClass(path)); } - } - return Collections.singletonList(tempFile); - } - - private byte[] getEntryContent(String entryPath) { - // 实现获取JAR/ZIP条目内容的逻辑 - if (currentJarFile == null) return null; - - try (JarFile jar = new JarFile(currentJarFile)) { - JarEntry je = jar.getJarEntry(entryPath); - if (je != null) { - try (InputStream is = jar.getInputStream(je)) { - return IOUtils.toByteArray(is); + // 2. 图片 + else if (Arrays.asList("png", "jpg", "jpeg", "gif", "bmp", "ico", "svg").contains(ext)) { + res.put("type", "image"); + res.put("mime", getMimeType(ext)); + res.put("content", readResourceAsBase64(entry)); + } + // 3. 音频 + else if (Arrays.asList("mp3", "wav", "ogg").contains(ext)) { + res.put("type", "audio"); + res.put("mime", getMimeType(ext)); + res.put("content", readResourceAsBase64(entry)); + } + // 4. 视频 + else if (Arrays.asList("mp4", "webm", "mkv").contains(ext)) { + res.put("type", "video"); + res.put("mime", getMimeType(ext)); + // 注意:大视频转 Base64 会很慢且消耗内存,这里仅适用于小型资源 + // 生产环境建议提取到临时文件后返回 file:// 协议路径 + if (entry.getSize() > 10 * 1024 * 1024) { // > 10MB + res.put("type", "text"); + res.put("content", "// Video file is too large to preview directly (" + (entry.getSize()/1024/1024) + "MB).\n// Please drag it out to view."); + } else { + res.put("content", readResourceAsBase64(entry)); } } + // 5. 默认文本 + else { + res.put("type", "text"); + // 尝试检测是否为二进制 + if (isBinary(entry)) { + res.put("content", "// Binary file detected (" + ext + ").\n// Preview not available."); + } else { + res.put("content", readResourceAsText(entry)); + } + } + callback.success(res.toString()); } catch (Exception e) { e.printStackTrace(); - } - return null; - } - } - - // ---------- 通用后台任务 ---------- - private void runBackground(String title, Runnable task) { - final JDialog dlg = new JDialog(this, title, false); - dlg.setLayout(new BorderLayout()); - dlg.add(new JLabel("请稍候..."), BorderLayout.NORTH); - JProgressBar pb = new JProgressBar(); - pb.setIndeterminate(true); - dlg.add(pb, BorderLayout.CENTER); - dlg.setSize(300, 90); - dlg.setLocationRelativeTo(this); - - SwingWorker sw = new SwingWorker<>() { - Exception ex = null; - @Override - protected Void doInBackground() { - try { task.run(); } - catch (Exception e) { ex = e; e.printStackTrace(); } - return null; - } - @Override - protected void done() { - dlg.dispose(); - if (ex != null) { - JOptionPane.showMessageDialog(ModernJarViewer.this, "后台任务失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - } else { - applyMappingToOpenEditors(); - updateTabTitles(); - } - } - }; - sw.execute(); - dlg.setVisible(true); - } - - private void setupKeyBindings() { - getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) - .put(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK), "localSearch"); - getRootPane().getActionMap().put("localSearch", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { showLocalSearchDialog(); } - }); - - getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) - .put(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK), "globalSearch"); - getRootPane().getActionMap().put("globalSearch", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { showGlobalSearchDialog(); } - }); - - getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) - .put(KeyStroke.getKeyStroke(KeyEvent.VK_SLASH, 0), "toggleInlineAnnotation"); - getRootPane().getActionMap().put("toggleInlineAnnotation", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - RSyntaxTextArea ed = getCurrentEditor(); - if (ed == null) { - Toolkit.getDefaultToolkit().beep(); - return; - } - try { - handleToggleInlineAnnotation(ed); - } catch (Exception ex) { - ex.printStackTrace(); - Toolkit.getDefaultToolkit().beep(); - } - } - }); - } - - // ---------- 2) 获取当前编辑器的 entryName(路径/条目名),用于 map key(放到方法区) ---------- - private String getEntryNameForEditor(RSyntaxTextArea editor) { - if (editor == null) return null; - for (Map.Entry e : openEditors.entrySet()) { - if (e.getValue() == editor) return e.getKey(); - } - // 退路:尝试从 entryToComponent 查找 - for (Map.Entry ce : entryToComponent.entrySet()) { - Component c = ce.getValue(); - if (c == null) continue; - if (SwingUtilities.isDescendingFrom(editor, c) || SwingUtilities.isDescendingFrom(c, editor)) { - return ce.getKey(); - } - } - return null; - } - - // ---------- 3) 主处理函数:插入或编辑当前行的注解(放到方法区) ---------- - private void handleToggleInlineAnnotation(RSyntaxTextArea editor) throws BadLocationException { - String entryName = getEntryNameForEditor(editor); - if (entryName == null) { - // 无法识别文件名,仍允许临时注解但不持久化 - entryName = ""; - } - int caret = editor.getCaretPosition(); - int line = editor.getLineOfOffset(caret); - // 尝试从缓存中读取已有注解 - Map fileMap = inlineAnnotations.computeIfAbsent(entryName, k -> new ConcurrentHashMap<>()); - String existing = fileMap.get(line); - - - // 如果存在,则编辑;否则新建 - String initial = existing == null ? "" : existing; - String edited = showInlineAnnotationEditor(initial); - if (edited == null) { - // 用户取消,不做任何操作 - return; - } - - // 编辑为空 -> 删除注解;否则插入/替换 - if (edited.trim().isEmpty()) { - // 删除现有注解块(如果存在) - removeAnnotationBlockIfExists(editor, line); - fileMap.remove(line); - saveAnnotationsToDisk(); - return; - } else { - // 插入或替换注解块(更新文档并更新缓存) - insertOrReplaceAnnotationBlock(editor, line, edited); - fileMap.put(line, edited); - saveAnnotationsToDisk(); - } - } - - // ---------- 4) 显示多行注解编辑对话框(返回 null 表示取消,空字符串表示删除) ---------- - private String showInlineAnnotationEditor(String initialText) { - JDialog dlg = new JDialog(this, "编辑注解(Ctrl+Enter 提交;留空将删除)", true); - dlg.setLayout(new BorderLayout()); - JTextArea ta = new JTextArea(8, 60); - ta.setLineWrap(true); - ta.setWrapStyleWord(true); - ta.setText(initialText); - - // Ctrl+Enter 提交 - ta.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK), "submit"); - ta.getActionMap().put("submit", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - Window w = SwingUtilities.getWindowAncestor((Component) e.getSource()); - if (w instanceof JDialog) ((JDialog) w).dispose(); - } - }); - - JPanel center = new JPanel(new BorderLayout()); - center.setBorder(BorderFactory.createEmptyBorder(8,8,8,8)); - center.add(new JScrollPane(ta), BorderLayout.CENTER); - - JPanel bottom = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - JButton ok = new JButton("确定"); - JButton cancel = new JButton("取消"); - bottom.add(cancel); - bottom.add(ok); - - ok.addActionListener(ae -> dlg.dispose()); - cancel.addActionListener(ae -> { - ta.setText(null); - dlg.dispose(); - }); - - dlg.add(center, BorderLayout.CENTER); - dlg.add(bottom, BorderLayout.SOUTH); - dlg.pack(); - dlg.setLocationRelativeTo(this); - dlg.setVisible(true); - - // 返回 null 代表取消 - if (ta.getText() == null && initialText != null) return null; - return ta.getText(); - } - - // ---------- 5) 删除当前行已有注解块(按 ///block 规则查找并删除) ---------- - private void removeAnnotationBlockIfExists(RSyntaxTextArea editor, int line) { - try { - Document doc = editor.getDocument(); - int lineStart = editor.getLineStartOffset(line); - int lineEnd = editor.getLineEndOffset(line); - String lineText = doc.getText(lineStart, Math.max(0, lineEnd - lineStart)); - int idx = lineText.indexOf("//"); - if (idx < 0) { - // 没有在同一行找到 //,尝试从下一行查找以防文档被手动修改 - // 如果下一行以空格+// 开头,则从下一行开始删除连续注释 - int scanLine = line + 1; - boolean found = false; - int removeStart = -1; - int removeEnd = -1; - while (true) { - if (scanLine > editor.getLineCount() - 1) break; - int s = editor.getLineStartOffset(scanLine); - int e = editor.getLineEndOffset(scanLine); - String t = doc.getText(s, Math.max(0, e - s)); - if (t.trim().startsWith("//")) { - if (!found) { removeStart = s; found = true; } - removeEnd = e; - scanLine++; - continue; - } - break; - } - if (found) { - doc.remove(removeStart, removeEnd - removeStart); - } - return; - } else { - // 找到同行的 //,从该列开始删除该行及后续连续以相同缩进包含 // 的行 - int commentStart = lineStart + idx; - int removeStart = commentStart; - int scanLine = line + 1; - int removeEnd = lineEnd; - while (scanLine <= editor.getLineCount() - 1) { - int s = editor.getLineStartOffset(scanLine); - int e = editor.getLineEndOffset(scanLine); - String t = doc.getText(s, Math.max(0, e - s)); - // 判断该行是否为对齐注释(前面若有空格再紧接 "//") - int nonSpace = 0; - while (nonSpace < t.length() && (t.charAt(nonSpace) == ' ' || t.charAt(nonSpace) == '\t')) nonSpace++; - if (t.substring(nonSpace).startsWith("//")) { - removeEnd = e; - scanLine++; - continue; - } - break; - } - if (removeEnd > removeStart) { - doc.remove(removeStart, removeEnd - removeStart); - } - } - } catch (BadLocationException ex) { - ex.printStackTrace(); - } - } - - // ---------- 6) 插入或替换注解块(核心:找到行尾插入或找到已有注释块并替换) ---------- - private void insertOrReplaceAnnotationBlock(RSyntaxTextArea editor, int line, String editedText) { - try { - Document doc = editor.getDocument(); - int lineStart = editor.getLineStartOffset(line); - int lineEnd = editor.getLineEndOffset(line); - String lineText = doc.getText(lineStart, Math.max(0, lineEnd - lineStart)); - - // 先查找当前行内是否已有 '//' 注释(简单匹配) - int idx = lineText.indexOf("//"); - if (idx >= 0) { - // 计算旧注释块的开始/结束位置(包括后续连续对齐的注释行) - int commentStart = lineStart + idx; - int removeStart = commentStart; - int scanLine = line + 1; - int removeEnd = lineEnd; - while (scanLine <= editor.getLineCount() - 1) { - int s = editor.getLineStartOffset(scanLine); - int e = editor.getLineEndOffset(scanLine); - String t = doc.getText(s, Math.max(0, e - s)); - int nonSpace = 0; - while (nonSpace < t.length() && (t.charAt(nonSpace) == ' ' || t.charAt(nonSpace) == '\t')) nonSpace++; - if (t.substring(nonSpace).startsWith("//")) { - removeEnd = e; - scanLine++; - } else { - break; - } - } - // 删除旧注解块 - doc.remove(removeStart, removeEnd - removeStart); - - // 在原注释开始处插入新的注解(即替换) - String insert = buildInlineCommentString(editor, lineText, editedText); - doc.insertString(removeStart, insert, null); - - // 将光标放在新注解最后 - int finalCaret = removeStart + insert.length(); - if (finalCaret <= doc.getLength()) editor.setCaretPosition(finalCaret); - return; - } else { - // 无已有注释:我们要在"最后一个非空字符"之后插入注释,避免插入到换行位置 - String trimmedLine = rtrim(lineText); - int insertPos = lineStart + trimmedLine.length(); // 在最后非空字符之后插入 - String insert = buildInlineCommentString(editor, lineText, editedText); - doc.insertString(insertPos, insert, null); - - int finalCaret = insertPos + insert.length(); - if (finalCaret <= doc.getLength()) editor.setCaretPosition(finalCaret); - return; - } - } catch (BadLocationException ex) { - ex.printStackTrace(); - } - } - - // ---------- 替换:buildInlineCommentString ---------- - private String buildInlineCommentString(RSyntaxTextArea editor, String lineText, String editedText) { - // editedText 可能包含多行 - String[] lines = editedText.replace("\r\n", "\n").replace("\r", "\n").split("\n", -1); - - // 计算 trimmedLine(不包含行尾空白),用于确定第一行注释插入列 - String trimmedLine = rtrim(lineText); - // 首个 // 的列:紧跟在 trimmedLine 之后,加一个空格 - int commentCol = Math.max(0, trimmedLine.length()) + 1; - - // 构造注释:首行直接紧跟在当前行末(以 " // ..." 开始) - StringBuilder sb = new StringBuilder(); - sb.append(" // "); - if (lines.length > 0) sb.append(lines[0]); - - // 后续行:换行 + 若当前行前面有缩进,则保留缩进 + 使 // 对齐到 commentCol 列 - // 先构造 prefix:由 commentCol 个空格构成(这是从行首到 // 的列数) - String prefixSpaces = repeat(' ', commentCol); - for (int i = 1; i < lines.length; i++) { - sb.append("\n"); - sb.append(prefixSpaces); - sb.append("// "); - sb.append(lines[i]); - } - // 最后加一个换行,保证插入后光标不粘到下一代码行 - sb.append("\n"); - return sb.toString(); - } - - - // ---------- 8) 小工具方法:去掉行尾空白 与 重复字符 ---------- - private static String rtrim(String s) { - if (s == null) return ""; - int i = s.length() - 1; - while (i >= 0 && Character.isWhitespace(s.charAt(i))) i--; - return s.substring(0, i + 1); - } - private static String repeat(char c, int n) { - if (n <= 0) return ""; - char[] arr = new char[n]; - Arrays.fill(arr, c); - return new String(arr); - } - - - // 2) 新增:获取当前活动编辑器(放到类的方法区) - private RSyntaxTextArea getCurrentEditor() { - if (openTabs == null) return null; - Component sel = openTabs.getSelectedComponent(); - if (sel == null) return null; - - // 常见情况:选中的是 JScrollPane,里面是 RSyntaxTextArea - if (sel instanceof JScrollPane) { - JViewport vp = ((JScrollPane) sel).getViewport(); - Component view = vp.getView(); - if (view instanceof RSyntaxTextArea) return (RSyntaxTextArea) view; - } - - // 若选项卡中嵌套结构比较复杂,则从 openEditors 中查找第一个位于选中组件里的编辑器 - for (RSyntaxTextArea ta : openEditors.values()) { - if (ta == null) continue; - if (SwingUtilities.isDescendingFrom(ta, sel)) return ta; - } - - // 最后回退:如果 openTabs 的组件直接就是编辑器 - if (sel instanceof RSyntaxTextArea) return (RSyntaxTextArea) sel; - return null; - } - - // 3) 新增:弹出注解输入对话框(多行),支持 Ctrl+Enter 提交 - private void insertAnnotationDialog(RSyntaxTextArea editor) { - JDialog dlg = new JDialog(this, "添加注解", true); - dlg.setLayout(new BorderLayout()); - JTextArea ta = new JTextArea(6, 60); - ta.setLineWrap(true); - ta.setWrapStyleWord(true); - - // 允许用户用 Ctrl+Enter 提交 - ta.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.CTRL_DOWN_MASK), "submit"); - ta.getActionMap().put("submit", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - Window w = SwingUtilities.getWindowAncestor((Component) e.getSource()); - if (w instanceof JDialog) ((JDialog) w).dispose(); - } - }); - - JPanel center = new JPanel(new BorderLayout(4,4)); - center.setBorder(BorderFactory.createEmptyBorder(8,8,8,8)); - center.add(new JScrollPane(ta), BorderLayout.CENTER); - - JPanel bottom = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - JButton ok = new JButton("确定"); - JButton cancel = new JButton("取消"); - bottom.add(cancel); - bottom.add(ok); - - ok.addActionListener(ae -> dlg.dispose()); - cancel.addActionListener(ae -> { - ta.setText(null); - dlg.dispose(); - }); - - dlg.add(center, BorderLayout.CENTER); - dlg.add(bottom, BorderLayout.SOUTH); - - dlg.pack(); - dlg.setLocationRelativeTo(this); - dlg.setVisible(true); - - String text = ta.getText(); - if (text == null || text.trim().isEmpty()) return; - - // 在 EDT 之外做少量处理,然后在 EDT 插入文本 - final String annotation = buildBlockCommentForText(editor, text); - SwingUtilities.invokeLater(() -> { - try { - insertAnnotationAtCurrentLine(editor, annotation); - } catch (Exception ex) { - ex.printStackTrace(); - Toolkit.getDefaultToolkit().beep(); - } - }); - } - - // 4) 新增:把用户输入的多行注解格式化为 Java 样式的多行注释,并保留缩进 - private String buildBlockCommentForText(RSyntaxTextArea editor, String rawText) { - // 将输入按行拆分,去掉尾部/头部多余空行 - String[] lines = rawText.replace("\r\n", "\n").replace("\r", "\n").split("\n"); - // 获取当前行缩进 - int caret = editor.getCaretPosition(); - String indent = ""; - try { - int line = editor.getLineOfOffset(caret); - int lineStart = editor.getLineStartOffset(line); - int lineEnd = editor.getLineEndOffset(line); - String lineText = editor.getText(lineStart, Math.max(0, Math.min(lineEnd - lineStart, 2000))); - // 提取行首空白 - int idx = 0; - while (idx < lineText.length() && (lineText.charAt(idx) == ' ' || lineText.charAt(idx) == '\t')) idx++; - indent = lineText.substring(0, idx); - } catch (BadLocationException ignored) {} - - StringBuilder sb = new StringBuilder(); - sb.append("\n"); // 在当前行后插入新行开始注解 - sb.append(indent).append("/*\n"); - for (String l : lines) { - // 去掉末尾空格以保持整洁 - String trimmed = l.replaceAll("\\s+$", ""); - sb.append(indent).append(" * "); - sb.append(trimmed); - sb.append("\n"); - } - sb.append(indent).append(" */"); - sb.append("\n"); - return sb.toString(); - } - - // 5) 新增:实际在当前行后插入注解文本 - private void insertAnnotationAtCurrentLine(RSyntaxTextArea editor, String annotation) throws BadLocationException { - if (annotation == null || annotation.isEmpty()) return; - int caret = editor.getCaretPosition(); - int line = editor.getLineOfOffset(caret); - int lineEnd = editor.getLineEndOffset(line); // 插入到行尾(在该行之后) - // 将注解插入文档 - Document doc = editor.getDocument(); - doc.insertString(lineEnd, annotation, null); - // 将光标移动到注解结束处 - int newPos = lineEnd + annotation.length(); - if (newPos <= doc.getLength()) editor.setCaretPosition(newPos); - } - - // ---------- 打开 JAR,并构建树(目录在前、文件在后;隐掉被顶层源码覆盖的内部类) ---------- - private void openJarFile() { - AdvancedJFileChooser chooser = new AdvancedJFileChooser(); - chooser.setFileFilter(new javax.swing.filechooser.FileFilter() { - public boolean accept(File f) { return f.isDirectory() || f.getName().toLowerCase().endsWith(".jar") || f.getName().toLowerCase().endsWith(".zip"); } - public String getDescription() { return "JAR/ZIP Files (*.jar, *.zip)"; } - }); - if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { - loadJar(chooser.getSelectedFile()); - } - } - - private void loadJar(File jarFile) { - currentJarFile = jarFile; - methodIndex.clear(); - globalIndex.clear(); - overrideContents.clear(); - fullCodeCache.clear(); - deobfMap.clear(); - openEditors.clear(); - entryToComponent.clear(); - imageViewers.clear(); - binaryContentCache.clear(); - openTabs.removeAll(); - root.removeAllChildren(); - - - try (JarFile jar = new JarFile(jarFile)) { - Set names = new HashSet<>(); - Enumeration en = jar.entries(); - while (en.hasMoreElements()) { - JarEntry je = en.nextElement(); - if (!je.isDirectory()) names.add(je.getName()); - } - - Set topLevelBases = new HashSet<>(); - for (String n : names) { - if (n.endsWith(".class") && !n.contains("$")) topLevelBases.add(n.substring(0, n.length() - 6)); - } - - List sorted = new ArrayList<>(names); - Collections.sort(sorted); - - for (String name : sorted) { - if (name.endsWith(".class")) { - String base = name.substring(0, name.length() - 6); - if (base.contains("$")) { - String top = base.substring(0, base.indexOf('$')); - if (topLevelBases.contains(top)) continue; // skip inner class when top-level exists - } - String javaCandidate = base + ".java"; - if (names.contains(javaCandidate)) continue; // prefer .java - } - addEntryToTree(root, name); - } - - sortTreeRecursively(root); - - ((DefaultTreeModel) fileTree.getModel()).reload(); - fileTree.expandRow(0); - loadAnnotationsFromDisk(); - - // 异步构建索引并缓存全部可文本内容(避免后续卡顿) - runBackground("构建索引并缓存全jar代码...", this::buildGlobalIndexWithCache); - } catch (Exception ex) { - ex.printStackTrace(); - showError("加载 JAR 失败", ex.getMessage()); - } - } - - private void addEntryToTree(DefaultMutableTreeNode parent, String path) { - String[] segs = path.split("/"); - DefaultMutableTreeNode cur = parent; - for (String s : segs) { - DefaultMutableTreeNode child = findChild(cur, s); - if (child == null) { - child = new DefaultMutableTreeNode(s); - cur.add(child); - } - cur = child; - } - } - - private DefaultMutableTreeNode findChild(DefaultMutableTreeNode parent, String name) { - Enumeration en = parent.children(); - while (en.hasMoreElements()) { - DefaultMutableTreeNode n = (DefaultMutableTreeNode) en.nextElement(); - if (name.equals(n.getUserObject())) return n; - } - return null; - } - - private void sortTreeRecursively(DefaultMutableTreeNode node) { - int childCount = node.getChildCount(); - if (childCount == 0) return; - List children = new ArrayList<>(); - Enumeration en = node.children(); - while (en.hasMoreElements()) children.add((DefaultMutableTreeNode) en.nextElement()); - - children.sort((a, b) -> { - boolean aIsDir = a.getChildCount() > 0; - boolean bIsDir = b.getChildCount() > 0; - if (aIsDir && !bIsDir) return -1; - if (!aIsDir && bIsDir) return 1; - return a.getUserObject().toString().compareToIgnoreCase(b.getUserObject().toString()); - }); - - node.removeAllChildren(); - for (DefaultMutableTreeNode c : children) { - node.add(c); - sortTreeRecursively(c); - } - } - - // ---------- 树选择:不自动打开,只负责选中 ---------- - private class TreeSelectionHandler implements TreeSelectionListener { - public void valueChanged(TreeSelectionEvent e) { - // 不在这里打开文件,避免单击就触发打开(改为双击打开) - } - } - - // ---------- 打开 entry 到可关闭 tab(优先用 fullCodeCache) ---------- - private void openEntryInTab(String entryPath) { - try (JarFile jar = new JarFile(currentJarFile)) { - if (entryToComponent.containsKey(entryPath)) { - Component comp = entryToComponent.get(entryPath); - int idx = openTabs.indexOfComponent(comp); - if (idx >= 0) openTabs.setSelectedIndex(idx); - return; - } - - // 检查文件类型 - if (isImageFile(entryPath)) { - openImageEntry(entryPath, jar); - return; - } else if (isBinaryFile(entryPath)) { - openBinaryEntry(entryPath, jar); - return; - } else if (entryPath.endsWith(".zip")) { - openZipEntry(entryPath, jar); - return; - } else if (isAudioFile(entryPath)) { // 新增音频文件处理分支 - openAudioEntry(entryPath, jar); - return; - } - - String content = null; - if (fullCodeCache.containsKey(entryPath)) content = fullCodeCache.get(entryPath); - else content = getContentForEntry(entryPath, jar); - - if (content == null) content = ""; - - RSyntaxTextArea editor = createEditorForEntry(entryPath, content); - openEditors.put(entryPath, editor); - - RTextScrollPane scroller = new RTextScrollPane(editor); - scroller.setLineNumbersEnabled(true); - - openTabs.addTab(entryPath, scroller); - int idx = openTabs.indexOfComponent(scroller); - openTabs.setTabComponentAt(idx, makeTabComponent(openTabs, entryPath)); - entryToComponent.put(entryPath, scroller); - - updateTabTitles(); - - openTabs.setSelectedIndex(idx); - - // 异步索引单文件并确保缓存 - runBackground("索引 " + entryPath + " ...", () -> { - buildMethodIndex(entryPath); - if (!fullCodeCache.containsKey(entryPath)) { - try (JarFile j = new JarFile(currentJarFile)) { - String c = getContentForEntry(entryPath, j); - if (c != null) fullCodeCache.put(entryPath, c); - } catch (Exception ignored) {} - } - }); - } catch (Exception ex) { - ex.printStackTrace(); - showError("打开文件失败", ex.getMessage()); - } - } - - private void openAudioEntry(String entryPath, JarFile jar) { - try { - JarEntry je = jar.getJarEntry(entryPath); - if (je == null) return; - - try (InputStream is = jar.getInputStream(je)) { - // 创建临时文件用于播放 - File tempFile = createTempAudioFile(entryPath, is); - - // 获取音频文件信息 - AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(tempFile); - Map properties = fileFormat.properties(); - AudioFormat format = fileFormat.getFormat(); - - // 创建主面板 - JPanel mainPanel = new JPanel(new BorderLayout()); - mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - // 创建音频信息面板 - JPanel infoPanel = createAudioInfoPanel(entryPath, fileFormat, format, properties); - mainPanel.add(infoPanel, BorderLayout.NORTH); - - // 创建可视化面板 - JPanel visualizationPanel = new JPanel() { - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - drawAudioVisualization(g, getWidth(), getHeight()); - } - }; - visualizationPanel.setPreferredSize(new Dimension(400, 150)); - visualizationPanel.setBackground(new Color(240, 240, 245)); - mainPanel.add(visualizationPanel, BorderLayout.CENTER); - - // 创建播放控制面板 - JPanel controlPanel = createAudioControlPanel(tempFile, visualizationPanel); - mainPanel.add(controlPanel, BorderLayout.SOUTH); - - // 添加标签页 - openTabs.addTab(entryPath, mainPanel); - int idx = openTabs.getTabCount() - 1; - openTabs.setTabComponentAt(idx, makeTabComponent(openTabs, entryPath)); - entryToComponent.put(entryPath, mainPanel); - openTabs.setSelectedIndex(idx); - } - } catch (Exception e) { - e.printStackTrace(); - showError("音频加载失败", e.getMessage()); - } - } - - private File createTempAudioFile(String entryPath, InputStream is) throws IOException { - String ext = entryPath.substring(entryPath.lastIndexOf('.') + 1); - File tempFile = File.createTempFile("audio_", "." + ext); - tempFile.deleteOnExit(); - - try (FileOutputStream out = new FileOutputStream(tempFile)) { - byte[] buffer = new byte[1024]; - int bytesRead; - while ((bytesRead = is.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } - } - return tempFile; - } - - private JPanel createAudioInfoPanel(String entryPath, AudioFileFormat fileFormat, - AudioFormat format, Map properties) { - JPanel infoPanel = new JPanel(new GridLayout(0, 2, 5, 5)); - infoPanel.setBorder(BorderFactory.createTitledBorder("音频信息")); - - // 基本文件信息 - addInfoRow(infoPanel, "文件名:", entryPath.substring(entryPath.lastIndexOf('/') + 1)); - addInfoRow(infoPanel, "格式类型:", fileFormat.getType().toString()); - addInfoRow(infoPanel, "文件大小:", formatFileSize(fileFormat.getByteLength())); - - // 音频格式信息 - addInfoRow(infoPanel, "编码类型:", format.getEncoding().toString()); - addInfoRow(infoPanel, "采样率:", format.getSampleRate() + " Hz"); - addInfoRow(infoPanel, "采样大小:", format.getSampleSizeInBits() + " bits"); - addInfoRow(infoPanel, "通道数:", format.getChannels() + (format.getChannels() > 1 ? " (立体声)" : " (单声道)")); - addInfoRow(infoPanel, "帧大小:", format.getFrameSize() + " bytes"); - addInfoRow(infoPanel, "帧率:", String.format("%.1f fps", format.getFrameRate())); - - // 元数据信息 - if (properties != null) { - if (properties.containsKey("duration")) { - long duration = (Long) properties.get("duration") / 1000000; // 纳秒转毫秒 - addInfoRow(infoPanel, "时长:", formatDuration(duration)); - } - if (properties.containsKey("title")) { - addInfoRow(infoPanel, "标题:", properties.get("title").toString()); - } - if (properties.containsKey("author")) { - addInfoRow(infoPanel, "作者:", properties.get("author").toString()); - } - if (properties.containsKey("album")) { - addInfoRow(infoPanel, "专辑:", properties.get("album").toString()); - } - if (properties.containsKey("year")) { - addInfoRow(infoPanel, "年份:", properties.get("year").toString()); - } - } - - return infoPanel; - } - - private void addInfoRow(JPanel panel, String label, String value) { - JLabel lbl = new JLabel(label); - lbl.setFont(lbl.getFont().deriveFont(Font.BOLD)); - panel.add(lbl); - - JLabel val = new JLabel(value); - panel.add(val); - } - - private JPanel createAudioControlPanel(File audioFile, JPanel visualizationPanel) { - JPanel controlPanel = new JPanel(new BorderLayout()); - controlPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0)); - - // 进度条 - JSlider progressSlider = new JSlider(0, 100, 0); - progressSlider.setEnabled(false); - - // 时间标签 - JLabel timeLabel = new JLabel("00:00 / 00:00"); - timeLabel.setHorizontalAlignment(SwingConstants.CENTER); - - JPanel progressPanel = new JPanel(new BorderLayout()); - progressPanel.add(progressSlider, BorderLayout.CENTER); - progressPanel.add(timeLabel, BorderLayout.SOUTH); - controlPanel.add(progressPanel, BorderLayout.CENTER); - - // 控制按钮 - JButton playBtn = new JButton("播放"); - JButton pauseBtn = new JButton("暂停"); - JButton stopBtn = new JButton("停止"); - - pauseBtn.setEnabled(false); - - JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 0)); - btnPanel.add(playBtn); - btnPanel.add(pauseBtn); - btnPanel.add(stopBtn); - controlPanel.add(btnPanel, BorderLayout.SOUTH); - - // 音量控制 - JSlider volumeSlider = new JSlider(0, 100, 80); - volumeSlider.setPreferredSize(new Dimension(80, 20)); - volumeSlider.setToolTipText("音量控制"); - - JPanel volumePanel = new JPanel(new BorderLayout()); - volumePanel.add(new JLabel("音量:"), BorderLayout.WEST); - volumePanel.add(volumeSlider, BorderLayout.CENTER); - controlPanel.add(volumePanel, BorderLayout.EAST); - - // 播放器逻辑 - Clip audioClip = null; - try { - AudioInputStream audioStream = AudioSystem.getAudioInputStream(audioFile); - audioClip = AudioSystem.getClip(); - audioClip.open(audioStream); - - // 设置初始时间标签 - long duration = audioClip.getMicrosecondLength() / 1000; - timeLabel.setText("00:00 / " + formatDuration(duration)); - } catch (Exception e) { - showError("音频初始化失败", e.getMessage()); - playBtn.setEnabled(false); - } - - Clip finalClip = audioClip; - - // 播放按钮事件 - playBtn.addActionListener(e -> { - if (finalClip != null) { - finalClip.start(); - playBtn.setEnabled(false); - pauseBtn.setEnabled(true); - stopBtn.setEnabled(true); - progressSlider.setEnabled(true); - startProgressUpdater(finalClip, progressSlider, timeLabel, visualizationPanel); - } - }); - - // 暂停按钮事件 - pauseBtn.addActionListener(e -> { - if (finalClip != null && finalClip.isRunning()) { - finalClip.stop(); - pauseBtn.setEnabled(false); - playBtn.setEnabled(true); - } - }); - - // 停止按钮事件 - stopBtn.addActionListener(e -> { - if (finalClip != null) { - finalClip.stop(); - finalClip.setFramePosition(0); - pauseBtn.setEnabled(false); - playBtn.setEnabled(true); - stopBtn.setEnabled(false); - progressSlider.setValue(0); - updateTimeLabel(0, finalClip.getMicrosecondLength() / 1000, timeLabel); - } - }); - - // 进度条事件 - progressSlider.addChangeListener(e -> { - if (!progressSlider.getValueIsAdjusting() && finalClip != null) { - int value = progressSlider.getValue(); - long position = (long) (finalClip.getMicrosecondLength() * (value / 100.0)); - finalClip.setMicrosecondPosition(position); - } - }); - - // 音量控制事件 - volumeSlider.addChangeListener(e -> { - if (finalClip != null) { - float volume = volumeSlider.getValue() / 100f; - FloatControl gainControl = (FloatControl) finalClip.getControl(FloatControl.Type.MASTER_GAIN); - float dB = (float) (Math.log(volume) / Math.log(10.0) * 20.0); - gainControl.setValue(dB); - } - }); - - return controlPanel; - } - - private void startProgressUpdater(Clip clip, JSlider slider, JLabel timeLabel, JPanel visualizationPanel) { - new Thread(() -> { - long duration = clip.getMicrosecondLength() / 1000; // 毫秒 - while (clip.isRunning() || clip.getFramePosition() < clip.getFrameLength()) { - long position = clip.getMicrosecondPosition() / 1000; // 毫秒 - - SwingUtilities.invokeLater(() -> { - int progress = (int) ((double) position / duration * 100); - slider.setValue(progress); - updateTimeLabel(position, duration, timeLabel); - visualizationPanel.repaint(); // 更新可视化 - }); - - try { - Thread.sleep(100); - } catch (InterruptedException ex) { - break; - } + callback.failure(500, "Read Error: " + e.getMessage()); } }).start(); } - private void updateTimeLabel(long positionMs, long durationMs, JLabel label) { - String posStr = formatDuration(positionMs); - String durStr = formatDuration(durationMs); - label.setText(posStr + " / " + durStr); - } + /** + * 新增:预处理拖拽导出 + * 将 JAR 内文件解压到临时目录,并返回物理路径,供前端构建 DownloadURL + */ + private static void handlePrepareDrag(String path, CefQueryCallback callback) { + new Thread(() -> { + try { + if (currentJarFile == null) { callback.failure(404, "No JAR"); return; } - private String formatDuration(long millis) { - long seconds = millis / 1000; - long minutes = seconds / 60; - seconds = seconds % 60; - return String.format("%02d:%02d", minutes, seconds); - } + // 检查缓存 + if (tempFileCache.containsKey(path) && tempFileCache.get(path).exists()) { + JSONObject res = new JSONObject(); + res.put("osPath", tempFileCache.get(path).getAbsolutePath()); + res.put("url", tempFileCache.get(path).toURI().toString()); + callback.success(res.toString()); + return; + } - private String formatFileSize(long bytes) { - if (bytes < 1024) return bytes + " B"; - int exp = (int) (Math.log(bytes) / Math.log(1024)); - char unit = "KMGTPE".charAt(exp-1); - return String.format("%.1f %sB", bytes / Math.pow(1024, exp), unit); - } + JarEntry entry = currentJarFile.getJarEntry(path); + if (entry == null) { callback.failure(404, "Entry not found"); return; } - private void drawAudioVisualization(Graphics g, int width, int height) { - // 简单的波形可视化 - Graphics2D g2d = (Graphics2D) g; - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + // 创建临时文件 + String fileName = new File(path).getName(); + // 使用特定的临时文件夹,避免混乱 + File tempDir = new File(System.getProperty("java.io.tmpdir"), "JDPro_Extracts"); + if (!tempDir.exists()) tempDir.mkdirs(); - // 背景 - g2d.setColor(new Color(220, 230, 245)); - g2d.fillRect(0, 0, width, height); + File tempFile = new File(tempDir, fileName); + tempFile.deleteOnExit(); - // 网格线 - g2d.setColor(new Color(200, 200, 210)); - for (int i = 1; i < 5; i++) { - int y = height * i / 5; - g2d.drawLine(0, y, width, y); - } + try (InputStream is = currentJarFile.getInputStream(entry)) { + Files.copy(is, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } - // 简单的波形 - g2d.setColor(new Color(70, 130, 180)); // 钢蓝色 - int centerY = height / 2; - int amplitude = height / 3; + tempFileCache.put(path, tempFile); - for (int x = 0; x < width; x += 3) { - double phase = System.currentTimeMillis() / 1000.0 + x * 0.05; - double wave1 = Math.sin(phase); - double wave2 = Math.sin(phase * 1.7 + 2); - double wave3 = Math.sin(phase * 0.7 + 5); - - int y = centerY + (int) (amplitude * (wave1 * 0.5 + wave2 * 0.3 + wave3 * 0.2)); - - int size = 2 + (int) (3 * Math.abs(wave1)); - g2d.fillOval(x, y, size, size); - } - } - - private boolean isAudioFile(String entryPath) { - if (entryPath == null) return false; - - String[] audioExtensions = { - ".mp3", ".wav", ".ogg", ".flac", ".aac", - ".m4a", ".wma", ".aiff", ".mid", ".midi" - }; - - String lowerPath = entryPath.toLowerCase(); - for (String ext : audioExtensions) { - if (lowerPath.endsWith(ext)) { - return true; + JSONObject res = new JSONObject(); + res.put("osPath", tempFile.getAbsolutePath()); + // Windows 下 file:///C:/... 格式 + res.put("url", tempFile.toURI().toString()); + callback.success(res.toString()); + } catch (Exception e) { + callback.failure(500, "Extract failed: " + e.getMessage()); } + }).start(); + } + + private static String getMimeType(String ext) { + switch (ext) { + case "png": return "image/png"; + case "jpg": case "jpeg": return "image/jpeg"; + case "gif": return "image/gif"; + case "svg": return "image/svg+xml"; + case "mp3": return "audio/mpeg"; + case "wav": return "audio/wav"; + case "ogg": return "audio/ogg"; + case "mp4": return "video/mp4"; + case "webm": return "video/webm"; + default: return "application/octet-stream"; + } + } + + private static boolean isBinary(JarEntry entry) { + String name = entry.getName().toLowerCase(); + return !name.endsWith(".java") && !name.endsWith(".xml") && !name.endsWith(".txt") + && !name.endsWith(".json") && !name.endsWith(".yml") && !name.endsWith(".properties") + && !name.endsWith(".md") && !name.endsWith(".mf") && !name.endsWith(".mcmeta") + && !name.endsWith(".toml") && !name.endsWith(".cfg") && !name.endsWith(".fsh") + && !name.endsWith(".vsh"); + } + + private static void handleFindDefinition(String word, CefQueryCallback callback) { + new Thread(() -> { + List defs = globalIndex.find(word); + JSONObject resp = new JSONObject(); + JSONArray arr = new JSONArray(); + Set visited = new HashSet<>(); + for (SymbolDef def : defs) { + String key = def.filePath + "::" + def.name; + if (visited.contains(key)) continue; + visited.add(key); + JSONObject item = new JSONObject(); + item.put("path", def.filePath); + item.put("className", def.className); + item.put("name", def.name); + item.put("type", def.type); + arr.put(item); + } + resp.put("definitions", arr); + callback.success(resp.toString()); + }).start(); + } + + /** + * 高性能字符串常量搜索 + * 直接解析 Class 文件的 Constant Pool,不进行反编译 + */ + private static void searchStringConstants(String target, CefQueryCallback callback) { + try { + JSONObject response = new JSONObject(); + JSONArray results = new JSONArray(); + String targetLower = target.toLowerCase(); + + Enumeration entries = currentJarFile.entries(); + int count = 0; + + // 缓冲区复用 + byte[] buffer = new byte[4096]; + + while (entries.hasMoreElements() && count < 100) { + JarEntry entry = entries.nextElement(); + if (!entry.getName().endsWith(".class")) continue; + + try (DataInputStream in = new DataInputStream(new BufferedInputStream(currentJarFile.getInputStream(entry)))) { + // 1. Check Magic (CAFEBABE) + if (in.readInt() != 0xCAFEBABE) continue; + in.skipBytes(4); // Minor, Major version + + // 2. Read Constant Pool Count + int poolCount = in.readUnsignedShort(); + + // 3. Iterate Pool + for (int i = 1; i < poolCount; i++) { + int tag = in.readUnsignedByte(); + + // 我们只关心 UTF8 (Tag 1) 类型,因为 String Constant (Tag 8) 最终指向 UTF8 + if (tag == 1) { + String utf8 = in.readUTF(); + if (utf8.toLowerCase().contains(targetLower)) { + JSONObject res = new JSONObject(); + res.put("path", entry.getName()); + res.put("line", 0); // 常量池搜索无法确定行号 + res.put("text", "Constant: \"" + utf8 + "\""); + results.put(res); + count++; + break; // 一个文件找到一处即可(避免重复),也可以去掉 break 显示所有 + } + } else { + // 跳过其他类型的字节 + switch (tag) { + case 7: case 8: case 16: case 19: case 20: // u2 + in.skipBytes(2); break; + case 3: case 4: case 9: case 10: case 11: case 12: case 17: case 18: // u4 + in.skipBytes(4); break; + case 5: case 6: // u8 (Long/Double) takes 2 slots + in.skipBytes(8); + i++; // 特殊规则:Long/Double 占用两个索引位置 + break; + case 15: // u3 (MethodHandle) + in.skipBytes(3); break; + default: + // 遇到无法解析的 Tag,为安全起见跳过此文件剩余部分 + i = poolCount; + break; + } + } + } + } catch (Exception ignored) { + // 个别文件解析失败不影响整体 + } + } + + response.put("results", results); + callback.success(response.toString()); + + } catch (Exception e) { + callback.failure(500, "String search failed: " + e.getMessage()); + } + } + + private static void handleSearchContent(String query, String mode, CefQueryCallback callback) { + new Thread(() -> { + if (currentJarFile == null || query.isEmpty()) { callback.success("{}"); return; } + + // 模式 1: "string" - 纯常量池搜索 (极速,无行号) + if ("string".equals(mode)) { + searchStringConstants(query, callback); + return; + } + + try { + JSONObject response = new JSONObject(); + JSONArray results = new JSONArray(); + String queryLower = query.toLowerCase(); // 用于忽略大小写匹配 + + // 编译正则 (如果需要忽略大小写) + Pattern pattern = Pattern.compile(Pattern.quote(query), Pattern.CASE_INSENSITIVE); + + Enumeration entries = currentJarFile.entries(); + int count = 0; + + // 缓冲区 + byte[] buffer = new byte[8192]; + + while (entries.hasMoreElements() && count < 100) { + JarEntry entry = entries.nextElement(); + if (entry.isDirectory()) continue; + String name = entry.getName(); + + // 只处理 Class 文件 (文本文件直接读,二进制跳过) + boolean isClass = name.endsWith(".class"); + if (isBinary(entry) && !isClass) continue; + + boolean shouldDecompile = false; + + // --- 核心优化逻辑 --- + if (isClass) { + // 如果是 "content" 模式 (全局代码),我们必须反编译,因为搜索词可能是变量名,不在常量池里 + // 如果是 "full_string" 模式 (全字符串),我们先快速扫描常量池 + if ("full_string".equals(mode)) { + // 预检:如果常量池里没有这个字符串,就绝对不需要反编译这个文件! + // 这将性能提升 10-50 倍 + if (!checkConstantPoolForString(entry, queryLower)) { + continue; + } + shouldDecompile = true; + } else { + // mode == "content" + // content 模式下,我们也可以先做字节码扫描 (checkBytes) + // 但为了准确性(变量名等),通常只能强行反编译或做更复杂的字节码分析。 + // 这里为了代码演示,content 模式保持暴力反编译,或者可以用字节包含预检 + shouldDecompile = true; + } + } else { + // 普通文本文件,总是读取 + shouldDecompile = false; + } + + // 获取内容 + String content; + if (isClass && shouldDecompile) { + content = decompileClass(name); + } else { + // 文本文件直接读,或者 Class 文件在 checkConstantPool 失败后不会走到这里 + content = readResourceAsText(entry); + } + + // --- 执行行号搜索 --- + String[] lines = content.split("\\r?\\n"); + for (int i = 0; i < lines.length; i++) { + // 忽略大小写的包含检查 + if (lines[i].toLowerCase().contains(queryLower)) { + JSONObject res = new JSONObject(); + res.put("path", name); + res.put("line", i + 1); // 这里的行号是反编译后的真实行号 + res.put("text", lines[i].trim()); + results.put(res); + count++; + // 为了避免结果过多,每个文件只取前 3 个匹配,或者只要有一个匹配就 break (取决于需求) + // 这里不 break,允许一个文件有多个匹配 + if (results.length() >= 80) break; + } + } + if (results.length() >= 80) break; + } + response.put("results", results); + callback.success(response.toString()); + } catch (Exception e) { + e.printStackTrace(); + callback.failure(500, e.getMessage()); + } + }).start(); + } + + /** + * 新增辅助方法:快速检查 Class 文件的常量池中是否包含特定字符串 + * 不进行反编译,只读取头部的 Constant Pool + */ + private static boolean checkConstantPoolForString(JarEntry entry, String queryLower) { + try (DataInputStream in = new DataInputStream(new BufferedInputStream(currentJarFile.getInputStream(entry)))) { + // Magic + if (in.readInt() != 0xCAFEBABE) return false; + in.skipBytes(4); // Version + + int poolCount = in.readUnsignedShort(); + for (int i = 1; i < poolCount; i++) { + int tag = in.readUnsignedByte(); + if (tag == 1) { // UTF8 String + String utf8 = in.readUTF(); + if (utf8.toLowerCase().contains(queryLower)) { + return true; // 命中!需要反编译 + } + } else { + // 跳过其他 Tag + switch (tag) { + case 7: case 8: case 16: case 19: case 20: in.skipBytes(2); break; + case 3: case 4: case 9: case 10: case 11: case 12: case 17: case 18: in.skipBytes(4); break; + case 5: case 6: in.skipBytes(8); i++; break; + case 15: in.skipBytes(3); break; + default: return false; // 解析错误,保守起见返回 false (或者 true 强制检查) + } + } + } + } catch (Exception e) { + return false; } return false; } - // 打开图片文件 - private void openImageEntry(String entryPath, JarFile jar) { + // --- 工具类 --- + static class SymbolDef { + String name; String className; String filePath; String type; + public SymbolDef(String name, String className, String type, String filePath) { + this.name = name; this.className = className; this.type = type; this.filePath = filePath; + } + } + static class SymbolIndex { + private final Map> map = new ConcurrentHashMap<>(); + public void add(SymbolDef def) { map.computeIfAbsent(def.name, k -> Collections.synchronizedList(new ArrayList<>())).add(def); } + public void clear() { map.clear(); } + public List find(String name) { return map.getOrDefault(name, Collections.emptyList()); } + } + // JarAnalyzer 类保持不变,请保留之前的实现 + + // 辅助方法:读取资源 + private static byte[] readResourceAsBytes(JarEntry entry) throws IOException { + try (InputStream is = currentJarFile.getInputStream(entry); + ByteArrayOutputStream os = new ByteArrayOutputStream()) { + byte[] b = new byte[8192]; int len; + while ((len = is.read(b)) != -1) os.write(b, 0, len); + return os.toByteArray(); + } + } + private static String readResourceAsText(JarEntry entry) throws IOException { + return new String(readResourceAsBytes(entry), StandardCharsets.UTF_8); + } + private static String readResourceAsBase64(JarEntry entry) throws IOException { + return Base64.getEncoder().encodeToString(readResourceAsBytes(entry)); + } + private static boolean containsBytes(byte[] source, byte[] match) { + if (match.length == 0) return true; + for (int i = 0; i <= source.length - match.length; i++) { + boolean found = true; + for (int j = 0; j < match.length; j++) { if (source[i + j] != match[j]) { found = false; break; } } + if (found) return true; + } + return false; + } + + // 反编译方法 + private static String decompileClass(String path) { try { - JarEntry je = jar.getJarEntry(entryPath); - if (je == null) return; - - try (InputStream is = jar.getInputStream(je)) { - byte[] imageData = IOUtils.toByteArray(is); - binaryContentCache.put(entryPath, imageData); - - BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(imageData)); - if (originalImage == null) { - showError("图片加载失败", "不支持的图片格式: " + entryPath); - return; + StringBuilder output = new StringBuilder(); + OutputSinkFactory mySink = new OutputSinkFactory() { + public List getSupportedSinks(SinkType sinkType, Collection available) { + return Arrays.asList(SinkClass.STRING, SinkClass.DECOMPILED, SinkClass.DECOMPILED_MULTIVER); } - - // 创建包含图片查看器和工具栏的面板 - JPanel mainPanel = new JPanel(new BorderLayout()); - ImageViewerPanel imageViewer = new ImageViewerPanel(originalImage); - mainPanel.add(new JScrollPane(imageViewer), BorderLayout.CENTER); - - // 添加底部工具栏 - JToolBar toolBar = createImageToolBar(imageViewer, originalImage); - mainPanel.add(toolBar, BorderLayout.SOUTH); - - // 添加标签页 - openTabs.addTab(entryPath, mainPanel); - int idx = openTabs.getTabCount() - 1; // 修复:使用最后添加的标签索引 - openTabs.setTabComponentAt(idx, makeTabComponent(openTabs, entryPath)); - entryToComponent.put(entryPath, mainPanel); - openTabs.setSelectedIndex(idx); - } - } catch (Exception e) { - e.printStackTrace(); - showError("图片加载失败", e.getMessage()); - } - } - - private JToolBar createImageToolBar(ImageViewerPanel viewer, BufferedImage image) { - JToolBar toolBar = new JToolBar(); - toolBar.setFloatable(false); - - // 缩放工具 - JButton zoomInBtn = new JButton("放大"); - zoomInBtn.addActionListener(e -> viewer.zoom(1.2)); - - JButton zoomOutBtn = new JButton("缩小"); - zoomOutBtn.addActionListener(e -> viewer.zoom(0.8)); - - JButton fitBtn = new JButton("适应窗口"); - fitBtn.addActionListener(e -> viewer.fitToWindow()); - - JButton actualSizeBtn = new JButton("实际大小"); - actualSizeBtn.addActionListener(e -> viewer.resetZoom()); - - // 图片属性 - JButton infoBtn = new JButton("属性"); - infoBtn.addActionListener(e -> showImageInfo(image)); - - toolBar.add(zoomInBtn); - toolBar.add(zoomOutBtn); - toolBar.add(fitBtn); - toolBar.add(actualSizeBtn); - toolBar.addSeparator(); - toolBar.add(infoBtn); - - return toolBar; - } - - private void showImageInfo(BufferedImage image) { - String info = String.format( - "宽度: %d 像素\n高度: %d 像素\n颜色类型: %s", - image.getWidth(), - image.getHeight(), - getColorTypeName(image.getType()) - ); - - JOptionPane.showMessageDialog(this, info, "图片属性", JOptionPane.INFORMATION_MESSAGE); - } - - private String getColorTypeName(int type) { - switch (type) { - case BufferedImage.TYPE_INT_RGB: return "RGB"; - case BufferedImage.TYPE_INT_ARGB: return "ARGB"; - case BufferedImage.TYPE_INT_ARGB_PRE: return "ARGB_PRE"; - case BufferedImage.TYPE_INT_BGR: return "BGR"; - case BufferedImage.TYPE_3BYTE_BGR: return "3BYTE_BGR"; - case BufferedImage.TYPE_4BYTE_ABGR: return "4BYTE_ABGR"; - case BufferedImage.TYPE_BYTE_GRAY: return "灰度"; - default: return "未知 (" + type + ")"; - } - } - - // 图片查看器面板 - private static class ImageViewerPanel extends JPanel { - private BufferedImage originalImage; - private BufferedImage scaledImage; - private double scale = 1.0; - private Point dragStart; - private Dimension initialSize; - - public ImageViewerPanel(BufferedImage image) { - this.originalImage = image; - this.scaledImage = image; - this.initialSize = new Dimension(image.getWidth(), image.getHeight()); - setLayout(new BorderLayout()); - setPreferredSize(initialSize); - centerImage(); - - // 添加鼠标事件监听器 - MouseAdapter adapter = new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - dragStart = e.getPoint(); - } - - @Override - public void mouseDragged(MouseEvent e) { - if (dragStart != null) { - JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, ImageViewerPanel.this); - if (viewport != null) { - Point vp = viewport.getViewPosition(); - int dx = dragStart.x - e.getX(); - int dy = dragStart.y - e.getY(); - - vp.translate(dx, dy); - vp.x = Math.max(0, Math.min(vp.x, getWidth() - viewport.getWidth())); - vp.y = Math.max(0, Math.min(vp.y, getHeight() - viewport.getHeight())); - - viewport.setViewPosition(vp); - dragStart = e.getPoint(); - } - } - } - - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - double scaleFactor = e.getWheelRotation() < 0 ? 1.1 : 0.9; - zoom(scaleFactor); + public Sink getSink(SinkType sinkType, SinkClass sinkClass) { + return content -> { if (sinkType == SinkType.JAVA) output.append(content); }; } }; - - addMouseListener(adapter); - addMouseMotionListener(adapter); - addMouseWheelListener(adapter); - } - - public void zoom(double factor) { - scale *= factor; - scale = Math.max(0.1, Math.min(scale, 10.0)); // 限制缩放范围 - updateScaledImage(); - } - - public void resetZoom() { - scale = 1.0; - updateScaledImage(); - } - - public void fitToWindow() { - JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, this); - if (viewport != null) { - Dimension viewSize = viewport.getSize(); - double widthRatio = (double) viewSize.width / originalImage.getWidth(); - double heightRatio = (double) viewSize.height / originalImage.getHeight(); - scale = Math.min(widthRatio, heightRatio); - updateScaledImage(); - centerImage(); - } - } - - private void updateScaledImage() { - int newWidth = (int) (originalImage.getWidth() * scale); - int newHeight = (int) (originalImage.getHeight() * scale); - - scaledImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = scaledImage.createGraphics(); - g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2d.drawImage(originalImage, 0, 0, newWidth, newHeight, null); - g2d.dispose(); - - setPreferredSize(new Dimension(newWidth, newHeight)); - revalidate(); - repaint(); - } - - private void centerImage() { - JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, this); - if (viewport != null) { - Dimension viewSize = viewport.getSize(); - Dimension imageSize = getPreferredSize(); - - int x = (viewSize.width - imageSize.width) / 2; - int y = (viewSize.height - imageSize.height) / 2; - - setLocation(x, y); - } - } - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - // 居中绘制图片 - int x = (getWidth() - scaledImage.getWidth()) / 2; - int y = (getHeight() - scaledImage.getHeight()) / 2; - g.drawImage(scaledImage, x, y, this); - } - } - - // 打开二进制文件 - private void openBinaryEntry(String entryPath, JarFile jar) { - try { - JarEntry je = jar.getJarEntry(entryPath); - if (je == null) return; - - try (InputStream is = jar.getInputStream(je)) { - byte[] data = IOUtils.toByteArray(is); - binaryContentCache.put(entryPath, data); - - JTextArea hexViewer = new JTextArea(); - hexViewer.setEditable(false); - hexViewer.setFont(new Font("Monospaced", Font.PLAIN, 12)); - - // 显示十六进制预览 - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < data.length; i += 16) { - // 十六进制部分 - for (int j = 0; j < 16 && i+j < data.length; j++) { - sb.append(String.format("%02X ", data[i+j])); - } - sb.append(" "); - - // ASCII部分 - for (int j = 0; j < 16 && i+j < data.length; j++) { - char c = (char) (data[i+j] & 0xFF); - sb.append(c >= 32 && c < 127 ? c : '.'); - } - sb.append("\n"); - } - - hexViewer.setText(sb.toString()); - - JScrollPane scrollPane = new JScrollPane(hexViewer); - openTabs.addTab(entryPath, scrollPane); - int idx = openTabs.indexOfComponent(scrollPane); - openTabs.setTabComponentAt(idx, makeTabComponent(openTabs, entryPath)); - entryToComponent.put(entryPath, scrollPane); - openTabs.setSelectedIndex(idx); - } - } catch (Exception e) { - e.printStackTrace(); - showError("二进制文件加载失败", e.getMessage()); - } - } - - // 打开ZIP文件 - private void openZipEntry(String entryPath, JarFile jar) { - try { - JarEntry je = jar.getJarEntry(entryPath); - if (je == null) return; - - try (InputStream is = jar.getInputStream(je)) { - byte[] zipData = IOUtils.toByteArray(is); - binaryContentCache.put(entryPath, zipData); - - // 创建临时ZIP文件 - Path tempZip = Files.createTempFile("temp_", ".zip"); - Files.write(tempZip, zipData); - - // 在新窗口中显示ZIP内容 - showZipContents(entryPath, tempZip.toFile()); - } - } catch (Exception e) { - e.printStackTrace(); - showError("ZIP文件加载失败", e.getMessage()); - } - } - - private void showZipContents(String entryPath, File zipFile) { - JDialog zipDialog = new JDialog(this, "ZIP内容: " + entryPath, false); - zipDialog.setSize(800, 600); - zipDialog.setLayout(new BorderLayout()); - - DefaultMutableTreeNode zipRoot = new DefaultMutableTreeNode(zipFile.getName()); - JTree zipTree = new JTree(new DefaultTreeModel(zipRoot)); - - try (ZipFile zf = new ZipFile(zipFile)) { - Enumeration entries = zf.entries(); - while (entries.hasMoreElements()) { - ZipEntry ze = entries.nextElement(); - addEntryToTree(zipRoot, ze.getName()); - } - } catch (IOException e) { - e.printStackTrace(); - } - - zipTree.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - TreePath path = zipTree.getPathForLocation(e.getX(), e.getY()); - if (path == null) return; - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.getChildCount() == 0) { - String filePath = buildEntryPath(path); - extractAndOpenFromZip(zipFile, filePath); - } - } - } - }); - - zipDialog.add(new JScrollPane(zipTree), BorderLayout.CENTER); - zipDialog.setLocationRelativeTo(this); - zipDialog.setVisible(true); - } - - private void extractAndOpenFromZip(File zipFile, String entryPath) { - try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) { - ZipEntry ze; - while ((ze = zis.getNextEntry()) != null) { - if (ze.getName().equals(entryPath)) { - byte[] data = IOUtils.toByteArray(zis); - - if (isImageFile(entryPath)) { - openImageFromBytes(entryPath, data); - } else if (isBinaryFile(entryPath)) { - openBinaryFromBytes(entryPath, data); - } else { - openTextFromBytes(entryPath, data); - } - break; - } - } - } catch (Exception e) { - e.printStackTrace(); - showError("打开ZIP条目失败", e.getMessage()); - } - } - - private void openImageFromBytes(String entryPath, byte[] data) { - try { - BufferedImage image = ImageIO.read(new ByteArrayInputStream(data)); - if (image == null) { - showError("图片加载失败", "不支持的图片格式: " + entryPath); - return; - } - - ImageViewerPanel imageViewer = new ImageViewerPanel(image); - imageViewers.put(entryPath, imageViewer); - - openTabs.addTab(entryPath, new JScrollPane(imageViewer)); - int idx = openTabs.indexOfComponent(imageViewer); - openTabs.setTabComponentAt(idx, makeTabComponent(openTabs, entryPath)); - entryToComponent.put(entryPath, imageViewer); - openTabs.setSelectedIndex(idx); - } catch (Exception e) { - e.printStackTrace(); - showError("图片加载失败", e.getMessage()); - } - } - - private void openBinaryFromBytes(String entryPath, byte[] data) { - JTextArea hexViewer = new JTextArea(); - hexViewer.setEditable(false); - hexViewer.setFont(new Font("Monospaced", Font.PLAIN, 12)); - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < Math.min(1024, data.length); i += 16) { - // 十六进制部分 - for (int j = 0; j < 16 && i+j < data.length; j++) { - sb.append(String.format("%02X ", data[i+j])); - } - sb.append(" "); - - // ASCII部分 - for (int j = 0; j < 16 && i+j < data.length; j++) { - char c = (char) (data[i+j] & 0xFF); - sb.append(c >= 32 && c < 127 ? c : '.'); - } - sb.append("\n"); - } - - if (data.length > 1024) { - sb.append("\n... [只显示前1024字节] ...\n"); - } - - hexViewer.setText(sb.toString()); - - JScrollPane scrollPane = new JScrollPane(hexViewer); - openTabs.addTab(entryPath, scrollPane); - int idx = openTabs.indexOfComponent(scrollPane); - openTabs.setTabComponentAt(idx, makeTabComponent(openTabs, entryPath)); - entryToComponent.put(entryPath, scrollPane); - openTabs.setSelectedIndex(idx); - } - - private void openTextFromBytes(String entryPath, byte[] data) { - String content = new String(data, StandardCharsets.UTF_8); - RSyntaxTextArea editor = createEditorForEntry(entryPath, content); - openEditors.put(entryPath, editor); - - RTextScrollPane scroller = new RTextScrollPane(editor); - scroller.setLineNumbersEnabled(true); - - openTabs.addTab(entryPath, scroller); - int idx = openTabs.indexOfComponent(scroller); - openTabs.setTabComponentAt(idx, makeTabComponent(openTabs, entryPath)); - entryToComponent.put(entryPath, scroller); - openTabs.setSelectedIndex(idx); - } - - private Component makeTabComponent(JTabbedPane tabbedPane, String entryPath) { - JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); - p.setOpaque(false); - String title = entryPath; - JLabel lbl = new JLabel(title); - lbl.setBorder(BorderFactory.createEmptyBorder(2, 6, 2, 6)); - JButton close = new JButton("x"); - close.setMargin(new Insets(0, 4, 0, 4)); - close.addActionListener(e -> { - Component comp = entryToComponent.remove(entryPath); - openEditors.remove(entryPath); - imageViewers.remove(entryPath); - if (comp != null) { - tabbedPane.remove(comp); - } - updateTabTitles(); - }); - p.add(lbl); - p.add(close); - p.putClientProperty("entryPath", entryPath); - return p; - } - - private void updateTabTitles() { - Map baseCount = new HashMap<>(); - for (String entry : entryToComponent.keySet()) { - String base = entry.contains("/") ? entry.substring(entry.lastIndexOf('/') + 1) : entry; - baseCount.put(base, baseCount.getOrDefault(base, 0) + 1); - } - for (Map.Entry e : entryToComponent.entrySet()) { - String entry = e.getKey(); - Component comp = e.getValue(); - int idx = openTabs.indexOfComponent(comp); - if (idx < 0) continue; - String base = entry.contains("/") ? entry.substring(entry.lastIndexOf('/') + 1) : entry; - String title = baseCount.getOrDefault(base, 0) > 1 ? entry : base; - Component tabComp = openTabs.getTabComponentAt(idx); - if (tabComp instanceof JPanel) { - JPanel p = (JPanel) tabComp; - for (Component c : p.getComponents()) { - if (c instanceof JLabel) ((JLabel) c).setText(title); - } - } else { - openTabs.setTitleAt(idx, title); - } - openTabs.setToolTipTextAt(idx, entry); - } - } - - // ---------- 创建只读编辑器并绑定事件 ---------- - private RSyntaxTextArea createEditorForEntry(String entryName, String content) { - RSyntaxTextArea editor = new RSyntaxTextArea(); - editor.setEditable(false); - editor.setAntiAliasingEnabled(true); - editor.setCodeFoldingEnabled(true); - editor.setFont(new Font("Consolas", Font.PLAIN, 14)); - - // 设置语法高亮 - if (entryName.endsWith(".xml") || entryName.endsWith(".mcmeta") || entryName.endsWith(".mf")) { - editor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_XML); - } else if (entryName.endsWith(".java") || entryName.endsWith(".class")) { - editor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA); - } else if (entryName.endsWith(".json")) { - editor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JSON); - } else if (entryName.endsWith(".cfg") || entryName.endsWith(".properties") || entryName.endsWith(".toml")) { - editor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE); - } else if (entryName.endsWith(".html")) { - editor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_HTML); - } else if (entryName.endsWith(".js")) { - editor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); - } else if (entryName.endsWith(".css")) { - editor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_CSS); - } else if (entryName.endsWith(".py")) { - editor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_PYTHON); - } else if (entryName.endsWith(".c") || entryName.endsWith(".cpp")) { - editor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_C); - } else { - editor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_NONE); - } - - editor.setText(content); - editor.setCaretPosition(0); - - editor.setPopupMenu(null); - editor.setComponentPopupMenu(null); - - SyntaxScheme scheme = new SyntaxScheme(true); - configureIDEATheme(scheme, editor); - editor.setSyntaxScheme(scheme); - - JPopupMenu popup = createEditorPopup(entryName); - - editor.setToolTipSupplier((ta, me) -> { - MethodLink ml = findMethodUnderCursor(editor, me.getPoint()); - if (ml != null) return buildMethodTooltipFromLink(ml); - String jd = getJavadocAtPoint(editor, me.getPoint()); - return jd != null ? formatJavadoc(jd) : null; - }); - - editor.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - lastMousePoint = e.getPoint(); - // 中键跳转 - if (SwingUtilities.isMiddleMouseButton(e)) { - MethodLink ml = findMethodUnderCursor(editor, e.getPoint()); - if (ml != null) navigateToMethod(ml); - } - if (SwingUtilities.isRightMouseButton(e)) popup.show(editor, e.getX(), e.getY()); - } - // 删除双击跳转逻辑 - }); - - return editor; - } - - private void configureIDEATheme(SyntaxScheme scheme, RSyntaxTextArea editor) { - // 根据当前主题是深色还是浅色设置不同的背景和前景色 - Color background = isDarkTheme() ? new Color(0x1E1F22) : new Color(0xFFFFFF); - Color foreground = isDarkTheme() ? new Color(0xE0E0E0) : new Color(0x000000); - - Style defaultStyle = scheme.getStyle(Token.NULL); - defaultStyle.foreground = foreground; - defaultStyle.background = background; - - // 深色主题和浅色主题的不同颜色配置 - if (isDarkTheme()) { - setTokenStyle(scheme, Token.RESERVED_WORD, 0xCC7832); - setTokenStyle(scheme, Token.SEPARATOR, 0x4EC9B0); - setTokenStyle(scheme, Token.OPERATOR, 0xFFD700); - setTokenStyle(scheme, Token.IDENTIFIER, 0xE0E0E0); - setTokenStyle(scheme, Token.LITERAL_STRING_DOUBLE_QUOTE, 0x6A8759); - setTokenStyle(scheme, Token.LITERAL_NUMBER_DECIMAL_INT, 0x6897BB); - setTokenStyle(scheme, Token.COMMENT_EOL, 0x6A8759); - setTokenStyle(scheme, Token.COMMENT_MULTILINE, 0x6A8759); - setTokenStyle(scheme, Token.COMMENT_DOCUMENTATION, 0x629755); - setTokenStyle(scheme, Token.ANNOTATION, 0xBBB529); - setTokenStyle(scheme, Token.FUNCTION, 0xFFC66D); - setTokenStyle(scheme, Token.DATA_TYPE, 0xE8BF6A); - - editor.setSelectionColor(new Color(0x214283)); - editor.setCurrentLineHighlightColor(new Color(0x323232)); - } else { - setTokenStyle(scheme, Token.RESERVED_WORD, 0x0000FF); - setTokenStyle(scheme, Token.SEPARATOR, 0x008000); - setTokenStyle(scheme, Token.OPERATOR, 0x000000); - setTokenStyle(scheme, Token.IDENTIFIER, 0x000000); - setTokenStyle(scheme, Token.LITERAL_STRING_DOUBLE_QUOTE, 0x008000); - setTokenStyle(scheme, Token.LITERAL_NUMBER_DECIMAL_INT, 0x0000FF); - setTokenStyle(scheme, Token.COMMENT_EOL, 0x008000); - setTokenStyle(scheme, Token.COMMENT_MULTILINE, 0x008000); - setTokenStyle(scheme, Token.COMMENT_DOCUMENTATION, 0x008000); - setTokenStyle(scheme, Token.ANNOTATION, 0x808000); - setTokenStyle(scheme, Token.FUNCTION, 0x000080); - setTokenStyle(scheme, Token.DATA_TYPE, 0x000080); - - editor.setSelectionColor(new Color(0xADD6FF)); - editor.setCurrentLineHighlightColor(new Color(0xE8E8E8)); - } - - editor.setBackground(background); - editor.setHighlightCurrentLine(true); - } - - private void setTokenStyle(SyntaxScheme scheme, int tokenType, int rgb) { - Style s = scheme.getStyle(tokenType); - s.foreground = new Color(rgb); - } - - // ---------- editor popup:跳转 / 查找引用(带预览) / 重命名 ---------- - private JPopupMenu createEditorPopup(String entryName) { - JPopupMenu popup = new JPopupMenu(); - - JMenuItem gotoMethod = new JMenuItem("跳转到方法定义"); - gotoMethod.addActionListener(e -> { - RSyntaxTextArea ed = openEditors.get(entryName); - if (ed == null) return; - Point p = lastMousePoint != null ? lastMousePoint : ed.getMousePosition(); - if (p == null) return; - MethodLink ml = findMethodUnderCursor(ed, p); - if (ml != null) navigateToMethod(ml); - else { - String id = getIdentifierAtPoint(ed, p); - if (id != null) navigateToMethod(new MethodLink("UnknownClass", id, "")); - else JOptionPane.showMessageDialog(this, "未找到方法/标识符"); - } - }); - popup.add(gotoMethod); - - JMenuItem findRefs = new JMenuItem("查找引用..."); - findRefs.addActionListener(e -> { - RSyntaxTextArea ed = openEditors.get(entryName); - if (ed == null) return; - Point p = lastMousePoint != null ? lastMousePoint : ed.getMousePosition(); - if (p == null) return; - MethodLink ml = findMethodUnderCursor(ed, p); - String id; - String cls = null; - if (ml != null) { id = ml.methodName; cls = ml.className; } - else { id = getIdentifierAtPoint(ed, p); } - if (id == null) { JOptionPane.showMessageDialog(this, "未找到要查找引用的标识符"); return; } - final String targetClass = cls; - final String methodName = id; - runBackground("查找引用: " + methodName + " ...", () -> showReferencesDialogBlockingWithPreview(targetClass, methodName)); - }); - popup.add(findRefs); - - JMenuItem rename = new JMenuItem("重命名..."); - rename.addActionListener(e -> { - RSyntaxTextArea ed = openEditors.get(entryName); - if (ed == null) return; - Point p = lastMousePoint != null ? lastMousePoint : ed.getMousePosition(); - if (p == null) return; - MethodLink ml = findMethodUnderCursor(ed, p); - String symbol = null; - boolean isClassRename; - if (ml != null && !"UnknownClass".equals(ml.className)) symbol = ml.methodName; - else symbol = getIdentifierAtPoint(ed, p); - if (symbol == null) { - symbol = findClassNameOnLine(ed, p); - if (symbol != null) isClassRename = true; - else { - isClassRename = false; - } - } else { - isClassRename = false; - } - if (symbol == null) { JOptionPane.showMessageDialog(this, "未识别要重命名的符号"); return; } - String newName = JOptionPane.showInputDialog(this, "将 `" + symbol + "` 重命名为:", symbol); - if (newName == null || newName.trim().isEmpty()) return; - newName = newName.trim(); - deobfMap.put(symbol, newName); - String finalSymbol = symbol; - String finalNewName = newName; - runBackground("正在全局替换并重建索引...", () -> { - globalReplaceSymbolBlocking(finalSymbol, finalNewName, isClassRename); - buildGlobalIndexWithCache(); - }); - }); - popup.add(rename); - - return popup; - } - - // ---------- 查找引用(阻塞并含预览) ---------- - private void showReferencesDialogBlockingWithPreview(String targetClass, String methodName) { - List results = new ArrayList<>(); - if (currentJarFile == null) return; - - try (JarFile jar = new JarFile(currentJarFile)) { - Enumeration en = jar.entries(); - while (en.hasMoreElements()) { - JarEntry je = en.nextElement(); - if (je.isDirectory()) continue; - String name = je.getName(); - if (!(name.endsWith(".java") || name.endsWith(".class") || isTextFile(name) || name.toLowerCase().endsWith(".xml"))) - continue; - String content = getContentForEntry(name, jar); - if (content == null || content.isEmpty()) continue; - - // 使用 JavaParser 查找 MethodCallExpr,应用更严格匹配逻辑以减少误报 - Pattern p = Pattern.compile("\\b([A-Za-z_][A-Za-z0-9_\\.]*?)\\." + Pattern.quote(methodName) + "\\s*\\("); - Matcher m = p.matcher(content); - while (m.find()) { - String qual = m.group(1); - if (!qual.contains("(") && !qual.contains("->")) { - int pos = m.start(); - results.add(new SearchResult(name, pos, methodName)); - } - } - } - } catch (Exception e) { - e.printStackTrace(); - } - - if (results.isEmpty()) { - SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "未找到引用: " + methodName)); - return; - } - - // 在 EDT 显示带预览的窗口(双击跳转但不关闭窗口) - SwingUtilities.invokeLater(() -> { - JDialog dlg = new JDialog(this, "引用: " + methodName, false); - dlg.setLayout(new BorderLayout()); - DefaultListModel lm = new DefaultListModel<>(); - results.forEach(lm::addElement); - JList list = new JList<>(lm); - list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - JTextArea preview = new JTextArea(); - preview.setEditable(false); - preview.setFont(new Font("Consolas", Font.PLAIN, 12)); - JSplitPane sp = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, new JScrollPane(list), new JScrollPane(preview)); - sp.setDividerLocation(420); - dlg.add(sp, BorderLayout.CENTER); - JPanel bottom = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - JButton openBtn = new JButton("打开选中项"); - JButton closeBtn = new JButton("关闭"); - bottom.add(openBtn); - bottom.add(closeBtn); - dlg.add(bottom, BorderLayout.SOUTH); - - list.addListSelectionListener(ev -> { - SearchResult sel = list.getSelectedValue(); - if (sel != null) { - String snippet = getSnippetForPosition(sel.filePath, sel.position, 8); - preview.setText(snippet); - preview.setCaretPosition(0); - } - }); - - list.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - SearchResult sel = list.getSelectedValue(); - if (sel != null) { - navigateToSearchResult(sel); - // 不关闭对话框(按要求) - } - } - } - }); - - openBtn.addActionListener(ev -> { - SearchResult sel = list.getSelectedValue(); - if (sel != null) navigateToSearchResult(sel); - }); - closeBtn.addActionListener(ev -> dlg.dispose()); - - dlg.setSize(1100, 600); - dlg.setLocationRelativeTo(this); - dlg.setVisible(true); - }); - } - - private void navigateToSearchResult(SearchResult r) { - openEntryInTab(r.filePath); - Component comp = entryToComponent.get(r.filePath); - if (comp == null) return; - int idx = openTabs.indexOfComponent(comp); - if (idx >= 0) openTabs.setSelectedIndex(idx); - RSyntaxTextArea ed = openEditors.get(r.filePath); - if (ed == null) return; - SwingUtilities.invokeLater(() -> { - try { - ed.setCaretPosition(Math.min(r.position, ed.getText().length())); - Rectangle rect = ed.modelToView(Math.max(0, r.position)); - if (rect != null) ed.scrollRectToVisible(rect); - ed.requestFocusInWindow(); - } catch (BadLocationException e) { e.printStackTrace(); } - }); - } - - // ---------- 全局替换(阻塞) ---------- - private void globalReplaceSymbolBlocking(String oldName, String newName, boolean isClassRename) { - if (currentJarFile == null) return; - // 构建一次性 Pattern,避免多次遍历映射导致的 O(n*m) - Pattern p = Pattern.compile("\\b" + Pattern.quote(oldName) + "\\b"); - try (JarFile jar = new JarFile(currentJarFile)) { - Enumeration en = jar.entries(); - while (en.hasMoreElements()) { - JarEntry je = en.nextElement(); - if (je.isDirectory()) continue; - String name = je.getName(); - if (!(name.endsWith(".java") || name.endsWith(".class") || isTextFile(name) || name.toLowerCase().endsWith(".xml"))) - continue; - String content = getContentForEntry(name, jar); - if (content == null) content = ""; - Matcher m = p.matcher(content); - if (m.find()) { - String replaced = m.replaceAll(Matcher.quoteReplacement(newName)); - overrideContents.put(name, replaced); - fullCodeCache.put(name, replaced); - } - } - } catch (Exception e) { e.printStackTrace(); } - } - - // ---------- editor helpers ---------- - private MethodLink findMethodUnderCursor(RSyntaxTextArea editor, Point p) { - try { - int offset = editor.viewToModel(p); - int line = editor.getLineOfOffset(offset); - Token token = editor.getTokenListForLine(line); - while (token != null && token.isPaintable()) { - if (offset >= token.getOffset() && offset <= token.getEndOffset()) { - if (token.getType() == Token.IDENTIFIER) { - return resolveMethodCall(editor, token, offset); - } - break; - } - token = token.getNextToken(); - } - } catch (BadLocationException e) { /* ignore */ } - return null; - } - - private MethodLink resolveMethodCall(RSyntaxTextArea editor, Token token, int offset) { - try { - String code = editor.getText(); - ParseResult pr = javaParser.parse(code); - if (pr.isSuccessful() && pr.getResult().isPresent()) { - CompilationUnit cu = pr.getResult().get(); - List calls = cu.findAll(MethodCallExpr.class); - for (MethodCallExpr mce : calls) { - if (mce.getRange().isPresent()) { - Range r = mce.getRange().get(); - int beginOffset = lineColToOffset(code, r.begin.line, r.begin.column); - int endOffset = lineColToOffset(code, r.end.line, r.end.column); - if (offset >= beginOffset && offset <= endOffset) { - try { - String className = mce.resolve().getClassName(); - String methodName = mce.getNameAsString(); - String signature = mce.resolve().getSignature(); - return new MethodLink(className, methodName, signature); - } catch (Throwable ex) { - return new MethodLink("UnknownClass", mce.getNameAsString(), ""); - } - } - } - } - } - } catch (Throwable ignored) { } - return new MethodLink("UnknownClass", token.getLexeme(), ""); - } - - private String getJavadocAtPoint(RSyntaxTextArea editor, Point p) { - try { - int offset = editor.viewToModel(p); - int line = editor.getLineOfOffset(offset); - Token token = editor.getTokenListForLine(line); - while (token != null && token.isPaintable()) { - if (token.getType() == Token.COMMENT_DOCUMENTATION) { - if (offset >= token.getOffset() && offset <= token.getEndOffset()) return token.getLexeme(); - } - token = token.getNextToken(); - } - } catch (BadLocationException ignored) {} - return null; - } - - private String getIdentifierAtPoint(RSyntaxTextArea editor, Point p) { - try { - int offset = editor.viewToModel(p); - int line = editor.getLineOfOffset(offset); - Token token = editor.getTokenListForLine(line); - while (token != null && token.isPaintable()) { - if (offset >= token.getOffset() && offset <= token.getEndOffset()) { - if (token.getType() == Token.IDENTIFIER) return token.getLexeme(); - break; - } - token = token.getNextToken(); - } - } catch (BadLocationException ignored) {} - return null; - } - - private String findClassNameOnLine(RSyntaxTextArea editor, Point p) { - try { - int offset = editor.viewToModel(p); - int line = editor.getLineOfOffset(offset); - int start = editor.getLineStartOffset(line); - int end = editor.getLineEndOffset(line); - String lineText = editor.getText(start, Math.max(0, Math.min(editor.getText().length(), end) - start)); - Pattern pc = Pattern.compile("\\b(class|interface|enum)\\s+(\\w+)"); - Matcher m = pc.matcher(lineText); - if (m.find()) return m.group(2); - } catch (Exception ignored) {} - return null; - } - - private String buildMethodTooltipFromLink(MethodLink link) { - String key = link.className + "#" + link.methodName + (link.signature != null ? link.signature : ""); - MethodInfo mi = methodIndex.get(key); - if (mi != null) { - return "" + mi.className + "." + mi.memberName + "
" + (mi.javadoc != null ? mi.javadoc : ""); - } - return "" + link.className + "." + link.methodName + ""; - } - - private String formatJavadoc(String raw) { - if (raw == null) return null; - String formatted = raw.replace("@param", "@param") - .replace("@return", "@return") - .replace("\n", "
"); - return "" + formatted + ""; - } - - // ---------- 读取 / 反编译(优先 fullCodeCache/overrideContents) ---------- - private String getContentForEntry(String entryName, JarFile jar) { - if (overrideContents.containsKey(entryName)) return overrideContents.get(entryName); - if (fullCodeCache.containsKey(entryName)) return fullCodeCache.get(entryName); - try { - JarEntry je = jar.getJarEntry(entryName); - if (je == null) return ""; - if (entryName.endsWith(".java") || isTextFile(entryName) || entryName.toLowerCase().endsWith(".xml") || - entryName.endsWith(".json") || entryName.endsWith(".mcmeta") || entryName.endsWith(".cfg") || - entryName.endsWith(".mf") || entryName.endsWith(".toml") || entryName.endsWith(".properties")) { - try (InputStream is = jar.getInputStream(je)) { - String content = new String(IOUtils.toByteArray(is), StandardCharsets.UTF_8); - fullCodeCache.put(entryName, content); - return content; - } - } else if (entryName.endsWith(".class")) { - String out = decompileClass(je); - if (out == null) out = ""; - if (!deobfMap.isEmpty()) { - for (Map.Entry map : deobfMap.entrySet()) { - out = out.replaceAll("\\b" + Pattern.quote(map.getKey()) + "\\b", - Matcher.quoteReplacement(map.getValue())); - } - } - fullCodeCache.put(entryName, out); - return out; - } else { - return ""; - } - } catch (Exception e) { - e.printStackTrace(); - } - return ""; - } - - private String decompileClass(JarEntry entry) { - if (overrideContents.containsKey(entry.getName())) return overrideContents.get(entry.getName()); - if (fullCodeCache.containsKey(entry.getName())) return fullCodeCache.get(entry.getName()); - - try (JarFile jar = new JarFile(currentJarFile)) { - String out = "// Decompile failed\n"; - String className = entry.getName(); - - switch (currentDecompiler) { - case "CFR": - out = decompileWithCFR(jar, className); - break; - case "Fernflower": - out = decompileWithCFR(jar, className); - break; - case "Procyon": - out = decompileWithProcyon(jar, className); - break; - default: - out = decompileWithCFR(jar, className); - } - - fullCodeCache.put(entry.getName(), out); - return out; - } catch (Exception e) { - e.printStackTrace(); - return "// Decompile failed\n"; - } - } - - private String decompileWithCFR(JarFile jar, String className) { - try { - CFROutputSinkFactory sinkFactory = new CFROutputSinkFactory(); CfrDriver driver = new CfrDriver.Builder() - .withClassFileSource(new JarClassFileSource(jar)) - .withOutputSink(sinkFactory) + .withClassFileSource(new JarClassFileSource(currentJarFile)) + .withOutputSink(mySink) + .withOptions(Collections.singletonMap("showversion", "false")) .build(); - driver.analyse(Collections.singletonList(className)); - return sinkFactory.getOutput(); - } catch (Exception e) { - e.printStackTrace(); - return "// CFR decompile failed: " + e.getMessage(); - } + driver.analyse(Collections.singletonList(path)); + return output.toString(); + } catch (Exception e) { return "// Decompilation Failed: " + e; } } - private String decompileWithFernflower(JarFile jar, String className) { - try { - // Fernflower 反编译器实现 - //ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - //PrintStream printStream = new PrintStream(outputStream); -// - //Fernflower fernflower = new Fernflower( - // new JarFileSource(jar), - // new PrintStreamDecompiler(printStream), - // new FernflowerPreferences(), - // new SimpleLogger() - //); -// - //fernflower.getStructContext().addSpace(jar, true); - //fernflower.decompileContext(); - //fernflower.clearContext(); + // 必须保留之前的 JarAnalyzer 实现和 JarClassFileSource + static class JarAnalyzer { + public static void analyze(JarFile jar, List classes, SymbolIndex index) { + index.clear(); + System.out.println("Analyzing " + classes.size() + " classes..."); - return decompileWithCFR(jar,className); - } catch (Exception e) { - e.printStackTrace(); - return "// Fernflower decompile failed: " + e.getMessage(); - } - } + classes.parallelStream().forEach(entry -> { + try (DataInputStream in = new DataInputStream(new BufferedInputStream(jar.getInputStream(entry)))) { + // 1. 获取全名: com/example/JavaAgent$ModClassTransformer.class + String rawName = entry.getName(); + if (!rawName.endsWith(".class")) return; - private String decompileWithProcyon(JarFile jar, String className) { - try { - StringWriter stringWriter = new StringWriter(); - PlainTextOutput output = new PlainTextOutput(stringWriter); - DecompilerSettings settings = new DecompilerSettings(); + String className = rawName.substring(0, rawName.length() - 6); - settings.setTypeLoader(new ITypeLoader() { - @Override - public boolean tryLoadType(String internalName, Buffer buffer) { - try { - String path = internalName.replace('.', '/') + ".class"; - JarEntry entry = jar.getJarEntry(path); - if (entry == null) { - return false; - } - try (InputStream input = jar.getInputStream(entry)) { - byte[] data = input.readAllBytes(); - if (data.length < 4 || - data[0] != (byte)0xCA || - data[1] != (byte)0xFE || - data[2] != (byte)0xBA || - data[3] != (byte)0xBE) { - System.err.println("[ERROR] Invalid class file format"); - return false; - } - buffer.reset(data.length + 1); - for (byte b : data) { - buffer.writeByte(b & 0xFF); - } - buffer.position(0); - return true; - } - } catch (Exception e) { - System.err.println("[ERROR] Failed to load class: " + e.getMessage()); - return false; + // 2. 计算目标文件路径 (Target File Path) + // 如果是内部类,指向顶层父类文件,这样 CFR 反编译时才能看到完整代码 + String targetFilePath; + if (className.contains("$")) { + targetFilePath = className.substring(0, className.indexOf('$')) + ".class"; + } else { + targetFilePath = rawName; } + + // 3. 提取简单类名 (用于索引 Key) + String simpleName; + if (className.contains("$")) { + // 内部类: 取最后一个 $ 之后 (JavaAgent$ModClassTransformer -> ModClassTransformer) + simpleName = className.substring(className.lastIndexOf('$') + 1); + } else { + // 普通类: 取最后一个 / 之后 + simpleName = className.contains("/") ? className.substring(className.lastIndexOf('/') + 1) : className; + } + + // 4. 注册类名索引 + // 排除匿名内部类 (纯数字) + if (!simpleName.isEmpty() && !simpleName.matches("^\\d+$")) { + // System.out.println("Indexing Class: " + simpleName + " -> " + targetFilePath); // Debug + index.add(new SymbolDef(simpleName, className, "Class", targetFilePath)); + } + + // 5. 解析类成员 (方法/字段) + parseClassMembers(in, index, className, targetFilePath); + + } catch (Exception e) { + // e.printStackTrace(); // 如果某个类解析失败,打印堆栈但不中断整个流程 } }); - - String fullClassName = className.replace(".class", "").replace('/', '.'); - - try { - Decompiler.decompile(fullClassName, output, settings); - } catch (Exception e) { - System.err.println("[ERROR] Decompilation failed: " + e.getMessage()); - throw e; - } - - String result = stringWriter.toString(); - if (result.trim().isEmpty()) { - return "// Decompilation produced empty output (possible error)"; - } - return result; - } catch (Exception e) { - System.err.println("[CRITICAL] Decompilation failed:"); - e.printStackTrace(); - return "// Procyon decompile failed: " + - e.getClass().getSimpleName() + " - " + - (e.getMessage() != null ? e.getMessage() : "Check class file integrity"); - } - } - - // ---------- 全jar索引 + 缓存(构建并把所有可文本内容放入 fullCodeCache) ---------- - private void buildGlobalIndexWithCache() { - - try { - // 如果用户已设置 indexFileLocation 且存在,先尝试加载 - if (indexFileLocation != null && indexFileLocation.exists()) { - Map> loaded = loadIndexFromDisk(indexFileLocation); - if (!loaded.isEmpty()) { - globalIndex.putAll(loaded); - // methodIndex 仍需按需重建(这里不重建 methodIndex,保留快速全局搜索) - return; // 提前返回(注意:若你需要 fullCodeCache,也可以在磁盘保存 fullCodeCache 的实现) - } - } - } catch (Exception ex) { - // 忽略索引加载错误,继续正常构建 - ex.printStackTrace(); + System.out.println("Indexing complete. Total symbols: " + index.map.size()); } - methodIndex.clear(); - globalIndex.clear(); - if (currentJarFile == null) return; - try (JarFile jar = new JarFile(currentJarFile)) { - Enumeration en = jar.entries(); - List entries = new ArrayList<>(); - while (en.hasMoreElements()) { - JarEntry je = en.nextElement(); - if (!je.isDirectory()) entries.add(je.getName()); - } - Collections.sort(entries); - for (String name : entries) { - if (!(name.endsWith(".java") || name.endsWith(".class"))) continue; - String content = getContentForEntry(name, jar); - if (content == null) content = ""; - ParseResult pr = javaParser.parse(content); - if (pr.isSuccessful() && pr.getResult().isPresent()) { - CompilationUnit cu = pr.getResult().get(); - String className = name.replace("/", ".").replace(".java", "").replace(".class", ""); - String finalContent = content; - cu.findAll(MethodDeclaration.class).forEach(md -> { - MethodInfo mi = new MethodInfo(); - mi.className = className; - mi.memberName = md.getNameAsString(); - mi.parameters = md.getParameters().toString(); - mi.javadoc = md.getJavadoc().map(d -> d.getDescription().toText()).orElse(null); - mi.filePath = name; - mi.position = md.getBegin().map(p -> lineColToOffset(finalContent, p.line, p.column)).orElse(0); - mi.isField = false; - mi.isStatic = md.isStatic(); - methodIndex.put(mi.getSignature(), mi); - globalIndex.computeIfAbsent(mi.memberName, k -> new ArrayList<>()).add(new SearchResult(name, mi.position, mi.memberName)); - }); - String finalContent1 = content; - cu.findAll(FieldDeclaration.class).forEach(fd -> { - fd.getVariables().forEach(v -> { - MethodInfo mi = new MethodInfo(); - mi.className = className; - mi.memberName = v.getNameAsString(); - mi.parameters = ""; - mi.javadoc = fd.getJavadoc().map(d -> d.getDescription().toText()).orElse(null); - mi.filePath = name; - mi.position = v.getBegin().map(p -> lineColToOffset(finalContent1, p.line, p.column)).orElse(0); - mi.isField = true; - mi.isStatic = fd.isStatic(); - methodIndex.put(mi.getSignature(), mi); - globalIndex.computeIfAbsent(mi.memberName, k -> new ArrayList<>()).add(new SearchResult(name, mi.position, mi.memberName)); - }); - }); - } - } - applyMappingToIndex(); + private static void parseClassMembers(DataInputStream in, SymbolIndex index, String className, String targetFilePath) throws IOException { + // Magic Number + if (in.readInt() != 0xCAFEBABE) return; + in.readUnsignedShort(); // Minor + in.readUnsignedShort(); // Major - if (indexFileLocation != null) { - saveIndexToDisk(indexFileLocation); - } - } catch (Exception e) { - e.printStackTrace(); - } - } + int constantPoolCount = in.readUnsignedShort(); + String[] pool = new String[constantPoolCount]; - private void buildMethodIndex(String entryPath) { - try (JarFile jar = new JarFile(currentJarFile)) { - String content = getContentForEntry(entryPath, jar); - if (content == null) content = ""; - ParseResult pr = javaParser.parse(content); - if (pr.isSuccessful() && pr.getResult().isPresent()) { - CompilationUnit cu = pr.getResult().get(); - String className = entryPath.replace("/", ".").replace(".java", "").replace(".class", ""); - String finalContent = content; - cu.findAll(MethodDeclaration.class).forEach(md -> { - MethodInfo mi = new MethodInfo(); - mi.className = className; - mi.memberName = md.getNameAsString(); - mi.parameters = md.getParameters().toString(); - mi.javadoc = md.getJavadoc().map(d -> d.getDescription().toText()).orElse(null); - mi.filePath = entryPath; - mi.position = md.getBegin().map(p -> lineColToOffset(finalContent, p.line, p.column)).orElse(0); - mi.isField = false; - mi.isStatic = md.isStatic(); - methodIndex.put(mi.getSignature(), mi); - globalIndex.computeIfAbsent(mi.memberName, k -> new ArrayList<>()).add(new SearchResult(entryPath, mi.position, mi.memberName)); - }); - String finalContent1 = content; - cu.findAll(FieldDeclaration.class).forEach(fd -> { - fd.getVariables().forEach(v -> { - MethodInfo mi = new MethodInfo(); - mi.className = className; - mi.memberName = v.getNameAsString(); - mi.parameters = ""; - mi.javadoc = fd.getJavadoc().map(d -> d.getDescription().toText()).orElse(null); - mi.filePath = entryPath; - mi.position = v.getBegin().map(p -> lineColToOffset(finalContent1, p.line, p.column)).orElse(0); - mi.isField = true; - mi.isStatic = fd.isStatic(); - methodIndex.put(mi.getSignature(), mi); - globalIndex.computeIfAbsent(mi.memberName, k -> new ArrayList<>()).add(new SearchResult(entryPath, mi.position, mi.memberName)); - }); - }); - } - } catch (Exception e) { e.printStackTrace(); } - } - - // ---------- 混淆表加载(阻塞版本,供后台线程调用) ---------- - private void loadObfuscationMapBlocking(File mapFile) { - try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(mapFile), StandardCharsets.UTF_8))) { - Pattern mdPattern = Pattern.compile("^MD:\\s+(\\S+)\\s+(\\([^\\)]*\\)\\S*)\\s+(\\S+)\\s+(\\([^\\)]*\\)\\S*)$"); - Pattern fdPattern = Pattern.compile("^FD:\\s+(\\S+)\\s+(\\S+)$"); - String line; - while ((line = br.readLine()) != null) { - line = line.trim(); - if (line.isEmpty()) continue; - Matcher m = mdPattern.matcher(line); - if (m.find()) { - String obf = extractSimpleName(m.group(1)); - String deobf = extractSimpleName(m.group(3)); - if (!obf.equals(deobf)) deobfMap.put(obf, deobf); - continue; - } - m = fdPattern.matcher(line); - if (m.find()) { - String obf = extractSimpleName(m.group(1)); - String deobf = extractSimpleName(m.group(2)); - if (!obf.equals(deobf)) deobfMap.put(obf, deobf); + // 解析常量池 + for (int i = 1; i < constantPoolCount; i++) { + int tag = in.readUnsignedByte(); + switch (tag) { + case 1: // UTF8 + pool[i] = in.readUTF(); + break; + case 7: // Class + case 8: // String + case 16: // MethodType (关键修正: u2) + case 19: // Module + case 20: // Package + in.skipBytes(2); + break; + case 3: // Integer + case 4: // Float + case 9: // FieldRef + case 10: // MethodRef + case 11: // InterfaceMethodRef + case 12: // NameAndType + case 17: // Dynamic + case 18: // InvokeDynamic + in.skipBytes(4); + break; + case 5: // Long + case 6: // Double + in.skipBytes(8); + i++; + break; + case 15: // MethodHandle + in.skipBytes(3); + break; + default: + // 遇到未知 Tag,为防止错位,停止解析该类的成员,但之前的类名索引已生效 + return; } } - try (JarFile jar = new JarFile(currentJarFile)) { - Enumeration en = jar.entries(); - while (en.hasMoreElements()) { - JarEntry je = en.nextElement(); - if (je.isDirectory()) continue; - String name = je.getName(); - if (name.endsWith(".class")) { - // 触发反编译并应用混淆映射 - getContentForEntry(name, jar); + in.readUnsignedShort(); // Access + in.readUnsignedShort(); // This + in.readUnsignedShort(); // Super + int interfaceCount = in.readUnsignedShort(); + in.skipBytes(interfaceCount * 2); + + // 解析字段 + int fieldCount = in.readUnsignedShort(); + for (int i = 0; i < fieldCount; i++) { + in.readUnsignedShort(); // Access + int nameIndex = in.readUnsignedShort(); + in.readUnsignedShort(); // Desc + int attrCount = in.readUnsignedShort(); + for (int j = 0; j < attrCount; j++) { in.readUnsignedShort(); int len = in.readInt(); in.skipBytes(len); } + + if (nameIndex > 0 && nameIndex < pool.length && pool[nameIndex] != null) { + index.add(new SymbolDef(pool[nameIndex], className, "Field", targetFilePath)); + } + } + + // 解析方法 + int methodCount = in.readUnsignedShort(); + for (int i = 0; i < methodCount; i++) { + in.readUnsignedShort(); // Access + int nameIndex = in.readUnsignedShort(); + in.readUnsignedShort(); // Desc + int attrCount = in.readUnsignedShort(); + for (int j = 0; j < attrCount; j++) { in.readUnsignedShort(); int len = in.readInt(); in.skipBytes(len); } + + if (nameIndex > 0 && nameIndex < pool.length && pool[nameIndex] != null) { + String mName = pool[nameIndex]; + if (!mName.equals("") && !mName.equals("")) { + index.add(new SymbolDef(mName, className, "Method", targetFilePath)); } } } - - // 性能优化:使用单个 Pattern 对每个条目替换(一次扫描) - applyMappingToAllEntriesBlocking(); - // 重新构建索引(使用缓存) - buildGlobalIndexWithCache(); - } catch (Exception e) { - e.printStackTrace(); } } - private String extractSimpleName(String full) { - if (full == null) return full; - int lastSlash = full.lastIndexOf('/'); - if (lastSlash >= 0 && lastSlash + 1 < full.length()) return full.substring(lastSlash + 1); - int lastDot = full.lastIndexOf('.'); - if (lastDot >= 0 && lastDot + 1 < full.length()) return full.substring(lastDot + 1); - return full; - } - - /** - * 高效地把映射应用到所有文本条目(阻塞)。使用单一 Pattern 扫描每个文件并替换匹配的键。 - */ - private void applyMappingToAllEntriesBlocking() { - if (currentJarFile == null || deobfMap.isEmpty()) return; - // 构造 alternation pattern,注意 escape - List keys = new ArrayList<>(deobfMap.keySet()); - if (keys.isEmpty()) return; - StringBuilder alt = new StringBuilder(); - for (int i = 0; i < keys.size(); i++) { - if (i > 0) alt.append("|"); - alt.append(Pattern.quote(keys.get(i))); + public static class JarClassFileSource implements org.benf.cfr.reader.api.ClassFileSource { + private final JarFile jarFile; + public JarClassFileSource(JarFile f) { this.jarFile = f; } + public void informAnalysisRelativePathDetail(String s, String s1) {} + public Collection addJar(String s) { return Collections.emptyList(); } + public String getPossiblyRenamedPath(String s) { return s; } + public org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair getClassFileContent(String path) throws IOException { + JarEntry entry = jarFile.getJarEntry(path); if(entry == null) throw new IOException(path); + return org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair.make(readResourceAsBytes(entry), path); } - Pattern p = Pattern.compile("\\b(" + alt.toString() + ")\\b"); - - try (JarFile jar = new JarFile(currentJarFile)) { - Enumeration en = jar.entries(); - while (en.hasMoreElements()) { - JarEntry je = en.nextElement(); - if (je.isDirectory()) continue; - String name = je.getName(); - if (!(name.endsWith(".java") || name.endsWith(".class") || isTextFile(name) || name.toLowerCase().endsWith(".xml"))) - continue; - String content = getContentForEntry(name, jar); - if (content == null) content = ""; - Matcher m = p.matcher(content); - boolean found = m.find(); - if (!found) continue; - StringBuffer sb = new StringBuffer(); - do { - String key = m.group(1); - String replacement = deobfMap.getOrDefault(key, key); - m.appendReplacement(sb, Matcher.quoteReplacement(replacement)); - } while (m.find()); - m.appendTail(sb); - String replaced = sb.toString(); - overrideContents.put(name, replaced); - fullCodeCache.put(name, replaced); - } - } catch (Exception e) { e.printStackTrace(); } - } - - private void applyMappingToIndex() { - if (deobfMap.isEmpty()) return; - Map newIndex = new ConcurrentHashMap<>(); - for (MethodInfo mi : methodIndex.values()) { - String newMember = mi.memberName; - if (deobfMap.containsKey(mi.memberName)) newMember = deobfMap.get(mi.memberName); - MethodInfo ni = new MethodInfo(); - ni.className = mi.className; - ni.memberName = newMember; - ni.parameters = mi.parameters; - ni.javadoc = mi.javadoc; - ni.filePath = mi.filePath; - ni.position = mi.position; - ni.isField = mi.isField; - ni.isStatic = mi.isStatic; - newIndex.put(ni.getSignature(), ni); - } - methodIndex.clear(); - methodIndex.putAll(newIndex); - - Map> newGlobal = new ConcurrentHashMap<>(); - for (Map.Entry> e : globalIndex.entrySet()) { - String key = e.getKey(); - String updated = key; - for (Map.Entry m : deobfMap.entrySet()) updated = updated.replace(m.getKey(), m.getValue()); - newGlobal.put(updated, e.getValue()); - } - globalIndex.clear(); - globalIndex.putAll(newGlobal); - } - - // ---------- 2) 替换 applyMappingToOpenEditors 方法(将耗时替换放到后台线程) ---------- - private void applyMappingToOpenEditors() { - if (deobfMap.isEmpty() || openEditors.isEmpty()) return; - - // 对每个打开的编辑器启动一个 SwingWorker,doInBackground 执行重替换,done 在 EDT 更新文本 - for (Map.Entry e : openEditors.entrySet()) { - final RSyntaxTextArea ed = e.getValue(); - final String originalText = ed.getText(); - - SwingWorker sw = new SwingWorker<>() { - @Override - protected String doInBackground() { - String newTxt = originalText; - // deobfMap 通常不大,逐个替换 - for (Map.Entry map : deobfMap.entrySet()) { - try { - // 使用边界匹配并转义关键字,避免误替换 - String pattern = "\\b" + Pattern.quote(map.getKey()) + "\\b"; - newTxt = newTxt.replaceAll(pattern, Matcher.quoteReplacement(map.getValue())); - } catch (Exception ex) { - // 单个 regex 失败也不要阻塞其它替换 - ex.printStackTrace(); - } - } - return newTxt; - } - - @Override - protected void done() { - try { - String finalNewTxt = get(); - if (!finalNewTxt.equals(originalText)) { - // 在 EDT 更新文本 - SwingUtilities.invokeLater(() -> { - // 保持光标位置(尽量) - int caret = ed.getCaretPosition(); - ed.setText(finalNewTxt); - if (caret <= ed.getText().length()) ed.setCaretPosition(caret); - }); - } - } catch (Exception ex) { - ex.printStackTrace(); - } - } - }; - sw.execute(); - } - } - - - // ---------- 搜索(当前/全局) ---------- - private JDialog currentSearchDialog = null; - - private void showLocalSearchDialog() { - RSyntaxTextArea ed = getCurrentEditor(); - if (ed == null) return; - if (currentSearchDialog != null && currentSearchDialog.isVisible()) { - currentSearchDialog.toFront(); - return; - } - - currentSearchDialog = new JDialog(this, "查找(当前文件)", false); - currentSearchDialog.setLayout(new BorderLayout()); - JPanel top = new JPanel(); - JTextField tf = new JTextField(30); - JButton next = new JButton("下一个"); - JButton prev = new JButton("上一个"); - top.add(tf); top.add(prev); top.add(next); - currentSearchDialog.add(top, BorderLayout.NORTH); - - tf.addKeyListener(new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ENTER) { - searchInEditor(ed, tf.getText(), true); // 默认搜索下一个 - } - } - }); - - next.addActionListener(e -> searchInEditor(ed, tf.getText(), true)); - prev.addActionListener(e -> searchInEditor(ed, tf.getText(), false)); - - currentSearchDialog.addWindowListener(new WindowAdapter() { - @Override - public void windowClosing(WindowEvent e) { - currentSearchDialog = null; - } - }); - - currentSearchDialog.pack(); - currentSearchDialog.setLocationRelativeTo(this); - currentSearchDialog.setVisible(true); - - tf.requestFocusInWindow(); - } - - private void searchInEditor(RSyntaxTextArea ed, String pattern, boolean forward) { - if (pattern == null || pattern.isEmpty()) return; - String text = ed.getText(); - int caret = ed.getCaretPosition(); - if (forward) { - int idx = text.indexOf(pattern, caret); - if (idx != -1) ed.select(idx, idx + pattern.length()); - else { - int idx2 = text.indexOf(pattern); - if (idx2 != -1) ed.select(idx2, idx2 + pattern.length()); - } - } else { - int idx = text.lastIndexOf(pattern, Math.max(0, caret - 1)); - if (idx != -1) ed.select(idx, idx + pattern.length()); - else { - int idx2 = text.lastIndexOf(pattern); - if (idx2 != -1) ed.select(idx2, idx2 + pattern.length()); - } - } - } - - - private void showGlobalSearchDialog() { - if (currentJarFile == null) { JOptionPane.showMessageDialog(this, "请先打开 JAR"); return; } - JDialog dlg = new JDialog(this, "全局搜索", false); - dlg.setLayout(new BorderLayout()); - JTextField tf = new JTextField(30); - JButton btn = new JButton("搜索"); - DefaultListModel lm = new DefaultListModel<>(); - JList jList = new JList<>(lm); - btn.addActionListener(e -> { - lm.clear(); - new SwingWorker() { - @Override - protected Void doInBackground() throws Exception { - try (JarFile jar = new JarFile(currentJarFile)) { - Enumeration en = jar.entries(); - while (en.hasMoreElements()) { - JarEntry je = en.nextElement(); - if (je.isDirectory()) continue; - String name = je.getName(); - if (!(name.endsWith(".java") || name.endsWith(".class") || isTextFile(name) || name.toLowerCase().endsWith(".xml"))) - continue; - String content = getContentForEntry(name, jar); - if (content == null) continue; - String pattern = tf.getText(); - int idx = content.indexOf(pattern); - while (idx >= 0) { - publish(new SearchResult(name, idx, pattern)); - idx = content.indexOf(pattern, idx + 1); - } - } - } - return null; - } - @Override - protected void process(List chunks) { for (SearchResult r : chunks) lm.addElement(r); } - @Override - protected void done() { JOptionPane.showMessageDialog(dlg, "搜索完成,共 " + lm.size() + " 个结果"); } - }.execute(); - }); - dlg.add(tf, BorderLayout.NORTH); - dlg.add(new JScrollPane(jList), BorderLayout.CENTER); - dlg.add(btn, BorderLayout.SOUTH); - jList.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - SearchResult r = jList.getSelectedValue(); - if (r != null) { - //dlg.dispose(); - navigateToSearchResult(r); - } - } - } - }); - dlg.setSize(700, 500); - dlg.setLocationRelativeTo(this); - dlg.setVisible(true); - } - - // ---------- 导出(实现 A) ---------- - private void exportOverrideJarBlocking(File outJar) { - if (currentJarFile == null) return; - try (JarFile inJar = new JarFile(currentJarFile); - FileOutputStream fos = new FileOutputStream(outJar); - JarOutputStream jos = new JarOutputStream(fos)) { - - Enumeration en = inJar.entries(); - Set written = new HashSet<>(); - while (en.hasMoreElements()) { - JarEntry je = en.nextElement(); - String name = je.getName(); - if (written.contains(name)) continue; - JarEntry outEntry = new JarEntry(name); - jos.putNextEntry(outEntry); - if (overrideContents.containsKey(name)) { - byte[] data = overrideContents.get(name).getBytes(StandardCharsets.UTF_8); - jos.write(data); - } else { - try (InputStream is = inJar.getInputStream(je)) { - IOUtils.copy(is, jos); - } catch (IOException ignored) {} - } - jos.closeEntry(); - written.add(name); - } - for (Map.Entry e : overrideContents.entrySet()) { - String name = e.getKey(); - if (written.contains(name)) continue; - JarEntry newEntry = new JarEntry(name); - jos.putNextEntry(newEntry); - byte[] data = e.getValue().getBytes(StandardCharsets.UTF_8); - jos.write(data); - jos.closeEntry(); - written.add(name); - } - } catch (Exception e) { - e.printStackTrace(); - SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "导出失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE)); - return; - } - SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(this, "导出成功: " + outJar.getAbsolutePath())); - } - - // ---------- 辅助 ---------- - private boolean isTextFile(String name) { - String n = name.toLowerCase(); - return n.endsWith(".txt") || n.endsWith(".xml") || n.endsWith(".mf") || - n.endsWith(".properties") || n.endsWith(".json") || n.endsWith(".mcmeta") || - n.endsWith(".cfg") || n.endsWith(".toml") || - n.endsWith(".html") || n.endsWith(".js") || n.endsWith(".css") || - n.endsWith(".py") || n.endsWith(".c") || n.endsWith(".cpp"); - } - - private boolean isImageFile(String name) { - String n = name.toLowerCase(); - return n.endsWith(".png") || n.endsWith(".jpg") || n.endsWith(".jpeg") || - n.endsWith(".gif") || n.endsWith(".bmp"); - } - - private boolean isBinaryFile(String name) { - String n = name.toLowerCase(); - return n.endsWith(".exe") || n.endsWith(".dll") || n.endsWith(".so") || - n.endsWith(".bin") || n.endsWith(".dat"); - } - - private int lineColToOffset(String content, int line, int column) { - if (line <= 0) return 0; - String[] lines = content.split("\n", -1); - int offset = 0; - int l = Math.min(line - 1, lines.length - 1); - for (int i = 0; i < l; i++) offset += lines[i].length() + 1; - offset += Math.max(0, column - 1); - return Math.min(offset, content.length()); - } - - private void navigateToMethod(MethodLink link) { - String sourcePath = link.className.replace('.', '/') + ".java"; - String classPath = link.className.replace('.', '/') + ".class"; - if (entryExistsInJar(sourcePath)) { - openEntryInTab(sourcePath); - RSyntaxTextArea ed = openEditors.get(sourcePath); - if (ed != null) SwingUtilities.invokeLater(() -> { - Pattern p = Pattern.compile("\\b" + Pattern.quote(link.methodName) + "\\s*\\("); - Matcher m = p.matcher(ed.getText()); - if (m.find()) ed.setCaretPosition(m.start()); - }); - return; - } - if (entryExistsInJar(classPath)) { - openEntryInTab(classPath); - RSyntaxTextArea ed = openEditors.get(classPath); - if (ed != null) SwingUtilities.invokeLater(() -> { - Pattern p = Pattern.compile(Pattern.quote(link.methodName)); - Matcher m = p.matcher(ed.getText()); - if (m.find()) ed.setCaretPosition(m.start()); - }); - return; - } - MethodInfo mi = methodIndex.get(link.getFullSignature()); - if (mi != null) { - openEntryInTab(mi.filePath); - RSyntaxTextArea ed = openEditors.get(mi.filePath); - if (ed != null) SwingUtilities.invokeLater(() -> ed.setCaretPosition(Math.min(mi.position, ed.getText().length()))); - return; - } - List list = globalIndex.get(link.methodName); - if (list != null && !list.isEmpty()) navigateToSearchResult(list.get(0)); - else JOptionPane.showMessageDialog(this, "未找到对应的定义或源码"); - } - - private boolean entryExistsInJar(String path) { - try (JarFile jar = new JarFile(currentJarFile)) { - return jar.getEntry(path) != null; - } catch (Exception e) { return false; } - } - - // ---------- snippet(单一定义) ---------- - private String getSnippetForPosition(String entryPath, int pos, int contextLines) { - String content = fullCodeCache.getOrDefault(entryPath, ""); - if ((content == null || content.isEmpty()) && currentJarFile != null) { - try (JarFile jar = new JarFile(currentJarFile)) { content = getContentForEntry(entryPath, jar); } - catch (Exception ignored) {} - } - if (content == null || content.isEmpty()) return ""; - String[] lines = content.split("\n", -1); - int running = 0; - int lineIndex = 0; - for (; lineIndex < lines.length; lineIndex++) { - int lineLen = lines[lineIndex].length() + 1; - if (running + lineLen > pos) break; - running += lineLen; - } - int startLine = Math.max(0, lineIndex - contextLines); - int endLine = Math.min(lines.length - 1, lineIndex + contextLines); - StringBuilder sb = new StringBuilder(); - for (int i = startLine; i <= endLine; i++) { - if (i == lineIndex) sb.append(">> "); - sb.append(String.format("%4d: %s\n", i + 1, lines[i])); - } - return sb.toString(); - } - - // ---------- 帮助方法:构建 entryPath ---------- - private String buildEntryPath(TreePath path) { - StringBuilder sb = new StringBuilder(); - Object[] nodes = path.getPath(); - for (int i = 1; i < nodes.length; i++) { // skip root - DefaultMutableTreeNode node = (DefaultMutableTreeNode) nodes[i]; - sb.append(node.getUserObject()); - if (i != nodes.length - 1) sb.append("/"); - } - return sb.toString(); - } - - // ---------- types ---------- - private static class SearchResult { - final String filePath; - final int position; - final String matchText; - SearchResult(String f, int p, String t) { filePath = f; position = p; matchText = t; } - public String toString() { return filePath + " : " + matchText + " (pos=" + position + ")"; } - } - - private static class MethodInfo { - String className; - String memberName; - String parameters; - String javadoc; - String filePath; - int position; - boolean isField; - boolean isStatic; - String getSignature() { return className + "#" + memberName + (parameters != null ? parameters : ""); } - } - - private static class MethodLink { - final String className; - final String methodName; - final String signature; - MethodLink(String c, String m, String s) { className = c; methodName = m; signature = s; } - String getFullSignature() { return className + "#" + methodName + signature; } - } - - // ---------- 显示错误 ---------- - private void showError(String title, String message) { - JOptionPane.showMessageDialog(this, message, title, JOptionPane.ERROR_MESSAGE); - } - - // ---------- main ---------- - public static void main(String[] args) { - SwingUtilities.invokeLater(() -> { - try { UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarculaLaf()); } catch (Exception ignored) {} - ModernJarViewer v = new ModernJarViewer(null); - v.setVisible(true); - }); } } \ No newline at end of file diff --git a/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java b/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java index 0d69b64..546980f 100644 --- a/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java +++ b/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java @@ -3,6 +3,7 @@ package com.axis.innovators.box.register; import com.axis.innovators.box.AxisInnovatorsBox; import com.axis.innovators.box.tools.StateManager; +import com.axis.innovators.box.util.AdvancedJFileChooser; import com.axis.innovators.box.window.LoadIcon; import com.axis.innovators.box.window.MainWindow; import com.axis.innovators.box.window.WindowsJDialog; @@ -574,7 +575,7 @@ public class RegistrationSettingsItem extends WindowsJDialog { JButton selectBgBtn = new JButton("选择图片"); selectBgBtn.setPreferredSize(new Dimension(100, 28)); selectBgBtn.addActionListener(e -> { - JFileChooser fileChooser = new JFileChooser(); + AdvancedJFileChooser fileChooser = new AdvancedJFileChooser(); fileChooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter( "图片文件 (*.jpg, *.jpeg, *.png, *.gif, *.bmp)", "jpg", "jpeg", "png", "gif", "bmp")); diff --git a/src/main/java/com/axis/innovators/box/register/RegistrationTool.java b/src/main/java/com/axis/innovators/box/register/RegistrationTool.java index e910bbe..6c7abff 100644 --- a/src/main/java/com/axis/innovators/box/register/RegistrationTool.java +++ b/src/main/java/com/axis/innovators/box/register/RegistrationTool.java @@ -2,6 +2,9 @@ package com.axis.innovators.box.register; import com.axis.innovators.box.AxisInnovatorsBox; import com.axis.innovators.box.browser.MainApplication; +import com.axis.innovators.box.browser.util.TerminalManager; +import com.axis.innovators.box.util.build.BuildInformation; +import com.axis.innovators.box.util.build.BuildSystem; import com.axis.innovators.box.window.FridaWindow; import com.axis.innovators.box.window.JarApiProfilingWindow; import com.axis.innovators.box.window.MainWindow; @@ -33,129 +36,172 @@ public class RegistrationTool { public RegistrationTool(AxisInnovatorsBox main) { this.main = main; int id = 0; - MainWindow.ToolCategory debugCategory = new MainWindow.ToolCategory("调试工具", - "debug/debug.png", - "用于调试指定Windows工具的一个分类"); + // 判断系统是否支持 + if (BuildInformation.isMatchSystem(BuildSystem.WINDOWS) + || BuildInformation.isMatchSystem(BuildSystem.UNKNOWN)) { + MainWindow.ToolCategory debugCategory = new MainWindow.ToolCategory("调试工具", + "debug/debug.png", + "用于调试指定Windows工具的一个分类"); - debugCategory.addTool(new MainWindow.ToolItem("Frida注入工具", "debug/frida/frida_main.png", - "使用frida注入目标进程的脚本程序 " + - "\n作者:tzdwindows 7", ++id, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); - FridaWindow fridaWindow = new FridaWindow(owner); - main.popupWindow(fridaWindow); - } - })); - - MainWindow.ToolCategory programmingToolsCategory = new MainWindow.ToolCategory("编程工具", - "programming/programming.png", - "编程工具"); - programmingToolsCategory.addTool(new MainWindow.ToolItem("JarApi查看器", "programming/JarApiViewer/JarApi_Viewer.png", - "查看Jar内的方法以及其注解" + - "\n作者:tzdwindows 7", ++id, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); - JarApiProfilingWindow jarApiProfilingWindow = new JarApiProfilingWindow(owner); - main.popupWindow(jarApiProfilingWindow); - } - })); - - programmingToolsCategory.addTool(new MainWindow.ToolItem("C语言编辑器", "programming/LanguageEditor/file-editing.png", - "C语言编译器,智能化的idea" + - "\n作者:tzdwindows 7", ++id, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - // Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); - MainApplication.popupCCodeEditorWindow(); - } - })); - - programmingToolsCategory.addTool(new MainWindow.ToolItem("多语言在线执行(当遇到无限循环时会抛出错误)", "programming/LanguageEditor/file-editing.png", - "多语言在线执行,当遇到无限循环时会抛出错误" + - "\n作者:tzdwindows 7", ++id, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - // Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); - MainApplication.popupCodeEditorWindow(); - } - })); - - programmingToolsCategory.addTool(new MainWindow.ToolItem("数据库管理工具", "programming/programming_dark.png", - "用于管理数据库" + - "\n作者:tzdwindows 7", ++id, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - // Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); - MainApplication.popupDataBaseWindow(); - } - })); - - MainWindow.ToolCategory aICategory = new MainWindow.ToolCategory("AI工具", - "ai/ai.png", - "人工智能/大语言模型"); - - aICategory.addTool(new MainWindow.ToolItem("本地AI执行工具", "ai/local/local_main.png", - "在本机对开源大语言模型进行推理" + - "\n作者:tzdwindows 7", ++id, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - - try { - LM.loadLibrary(LM.CUDA); - } catch (Exception ex) { - logger.error("无法加载AI推理库", ex); - JOptionPane.showMessageDialog(null, "无法加载AI推理库", - "无法加载AI推理库", JOptionPane.ERROR_MESSAGE); + debugCategory.addTool(new MainWindow.ToolItem("Frida注入工具", "debug/frida/frida_main.png", + "使用frida注入目标进程的脚本程序 " + + "\n作者:tzdwindows 7", ++id, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); + FridaWindow fridaWindow = new FridaWindow(owner); + main.popupWindow(fridaWindow); } + })); - Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); - // 这是被抛弃的界面,在后面的版本可能会删除 - //LocalWindow dialog = new LocalWindow(owner); - //main.popupWindow(dialog); - MainApplication.popupAIWindow((JFrame)owner); + MainWindow.ToolCategory programmingToolsCategory = new MainWindow.ToolCategory("编程工具", + "programming/programming.png", + "编程工具"); + programmingToolsCategory.addTool(new MainWindow.ToolItem("JarApi查看器", "programming/JarApiViewer/JarApi_Viewer.png", + "查看Jar内的方法以及其注解" + + "\n作者:tzdwindows 7", ++id, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); + JarApiProfilingWindow jarApiProfilingWindow = new JarApiProfilingWindow(owner); + main.popupWindow(jarApiProfilingWindow); + } + })); + + programmingToolsCategory.addTool(new MainWindow.ToolItem("C语言编辑器", "programming/LanguageEditor/file-editing.png", + "C语言编译器,智能化的idea" + + "\n作者:tzdwindows 7", ++id, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + // Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); + MainApplication.popupCCodeEditorWindow(); + } + })); + + programmingToolsCategory.addTool(new MainWindow.ToolItem("多语言在线执行(当遇到无限循环时会抛出错误)", "programming/LanguageEditor/file-editing.png", + "多语言在线执行,当遇到无限循环时会抛出错误" + + "\n作者:tzdwindows 7", ++id, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + // Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); + MainApplication.popupCodeEditorWindow(); + } + })); + + programmingToolsCategory.addTool(new MainWindow.ToolItem("数据库管理工具", "programming/programming_dark.png", + "用于管理数据库" + + "\n作者:tzdwindows 7", ++id, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + // Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); + MainApplication.popupDataBaseWindow(); + } + })); + + programmingToolsCategory.addTool(new MainWindow.ToolItem("Linux终端工具", "programming/linux.png", + "用于启动一个Linux终端" + + "\n作者:tzdwindows 7", ++id, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); + TerminalManager.popupRealLinuxWindow(); + } + })); + + programmingToolsCategory.addTool(new MainWindow.ToolItem("MySql控制台", "programming/mysql.png", + "用于启动一个MySql控制台" + + "\n作者:tzdwindows 7", ++id, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); + MainApplication.popupSimulatingSQLWindow((JFrame) owner); + } + })); + + MainWindow.ToolCategory aICategory = new MainWindow.ToolCategory("AI工具", + "ai/ai.png", + "人工智能/大语言模型"); + + aICategory.addTool(new MainWindow.ToolItem("本地AI执行工具", "ai/local/local_main.png", + "在本机对开源大语言模型进行推理" + + "\n作者:tzdwindows 7", ++id, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + + try { + LM.loadLibrary(LM.CUDA); + } catch (Exception ex) { + logger.error("无法加载AI推理库", ex); + JOptionPane.showMessageDialog(null, "无法加载AI推理库", + "无法加载AI推理库", JOptionPane.ERROR_MESSAGE); + } + + Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); + // 这是被抛弃的界面,在后面的版本可能会删除 + //LocalWindow dialog = new LocalWindow(owner); + //main.popupWindow(dialog); + MainApplication.popupAIWindow((JFrame) owner); + } + })); + + MainWindow.ToolCategory hahahah = new MainWindow.ToolCategory( + "good工具", + "haha/ok.png", + "good " + ); + hahahah.addTool(new MainWindow.ToolItem("123", "ai/local/local_main.png", + "456789" + + "\n作者:Vinfya", ++id, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + // 在这里写 + // 这个就是弹窗Ok + JOptionPane.showMessageDialog(null, "你好..."); + + } + })); + + + MainWindow.ToolCategory systemCategory = new MainWindow.ToolCategory("系统工具", + "windows/windows.png", + "系统工具"); + systemCategory.addTool(new MainWindow.ToolItem("任务栏主题设置", "windows/windowsOptimization/windowsOptimization.png", + "可以设置Windows任务栏的颜色等各种信息", ++id, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); + TaskbarAppearanceWindow taskbarAppearanceWindow = new TaskbarAppearanceWindow(owner); + main.popupWindow(taskbarAppearanceWindow); + } + })); + + addSystemToolCategory(debugCategory, "debugTools"); + addSystemToolCategory(aICategory, "fridaTools"); + addSystemToolCategory(programmingToolsCategory, "programmingTools"); + addSystemToolCategory(systemCategory, "systemTools"); + addSystemToolCategory(hahahah, "mc"); + } + } + + /** + * 注册ToolCategory + * @param toolCategory ToolCategory + */ + private boolean addSystemToolCategory(MainWindow.ToolCategory toolCategory, + String registeredName) { + registeredName = "system:" + registeredName; + if (!main.isWindow()) { + if (registeredNameList.contains(registeredName)) { + throw new RegistrationError(registeredName + " duplicate registered names"); } - })); - - MainWindow.ToolCategory hahahah = new MainWindow.ToolCategory( - "good工具", - "haha/ok.png", - "good " - ); - hahahah.addTool(new MainWindow.ToolItem("123", "ai/local/local_main.png", - "456789" + - "\n作者:Vinfya", ++id, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - // 在这里写 - // 这个就是弹窗Ok - JOptionPane.showMessageDialog(null, "你好..."); - - } - })); - - - - MainWindow.ToolCategory systemCategory = new MainWindow.ToolCategory("系统工具", - "windows/windows.png", - "系统工具"); - systemCategory.addTool(new MainWindow.ToolItem("任务栏主题设置", "windows/windowsOptimization/windowsOptimization.png", - "可以设置Windows任务栏的颜色等各种信息",++id, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); - TaskbarAppearanceWindow taskbarAppearanceWindow = new TaskbarAppearanceWindow(owner); - main.popupWindow(taskbarAppearanceWindow); - } - })); - - addToolCategory(debugCategory, "system:debugTools"); - addToolCategory(aICategory,"system:fridaTools"); - addToolCategory(programmingToolsCategory, "system:programmingTools"); - addToolCategory(systemCategory, "system:systemTools"); - addToolCategory(hahahah, "system:mc"); - + uuidList.add(toolCategory.getId()); + registeredNameList.add(registeredName); + toolCategories.add(toolCategory); + return true; + } else { + logger.warn("Wrong time to add tools"); + return false; + } } /** diff --git a/src/main/java/com/axis/innovators/box/tools/LibraryLoad.java b/src/main/java/com/axis/innovators/box/tools/LibraryLoad.java index ce3aab8..e4975c8 100644 --- a/src/main/java/com/axis/innovators/box/tools/LibraryLoad.java +++ b/src/main/java/com/axis/innovators/box/tools/LibraryLoad.java @@ -1,5 +1,8 @@ package com.axis.innovators.box.tools; +import com.axis.innovators.box.events.BrowserCreationCallback; +import com.axis.innovators.box.events.SubscribeEvent; + /** * 在程序链接库文件中加载指定链接库 * @author tzdwindows 7 diff --git a/src/main/java/com/axis/innovators/box/util/AdvancedJFileChooser.java b/src/main/java/com/axis/innovators/box/util/AdvancedJFileChooser.java index aba55c3..855e93a 100644 --- a/src/main/java/com/axis/innovators/box/util/AdvancedJFileChooser.java +++ b/src/main/java/com/axis/innovators/box/util/AdvancedJFileChooser.java @@ -1,67 +1,132 @@ package com.axis.innovators.box.util; +import com.axis.innovators.box.util.build.BuildInformation; +import com.axis.innovators.box.util.build.BuildSystem; import com.axis.innovators.box.window.JarApiProfilingWindow; +import jnafilechooser.api.JnaFileChooser; import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; import java.awt.*; import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; import java.util.prefs.Preferences; /** + * 修改版:回归继承 JFileChooser,以解决 boolean/int 类型不匹配问题。 + * 通过内部代理的方式调用 Windows 原生组件。 + * * @author tzdwindows 7 */ public class AdvancedJFileChooser extends JFileChooser { + private static final String PREF_KEY = "LAST_DIRECTORY"; - private static final File FALLBACK_DIR = new File(System.getProperty("user.home")); public AdvancedJFileChooser() { - super(loadValidDirectory()); - setFileSelectionMode(FILES_AND_DIRECTORIES); + super(); + // 初始化目录 + File lastDir = loadValidDirectory(); + if (lastDir != null) { + setCurrentDirectory(lastDir); + } + } + + /** + * 重写核心方法 showDialog。 + * 拦截调用,启动 Windows 原生选择器,并将结果转换回 int 返回。 + */ + @Override + public int showDialog(Component parent, String approveButtonText) { + // 1. 仅在 Windows 下启用原生界面 + if (BuildInformation.isMatchSystem(BuildSystem.WINDOWS)) { + + JnaFileChooser fc = new JnaFileChooser(); + + // --- 关键:将 JFileChooser 的设置同步给 Native Chooser --- + + // 2. 同步过滤器 (解决你遇到的过滤器不生效问题) + javax.swing.filechooser.FileFilter swingFilter = getFileFilter(); + if (swingFilter instanceof FileNameExtensionFilter) { + FileNameExtensionFilter ext = (FileNameExtensionFilter) swingFilter; + fc.addFilter(ext.getDescription(), ext.getExtensions()); + } + + // 3. 同步标题 + String title = getDialogTitle(); + fc.setTitle((title != null && !title.isEmpty()) ? title : approveButtonText); + + // 4. 同步当前目录 + if (getCurrentDirectory() != null) { + fc.setCurrentDirectory(getCurrentDirectory().getAbsolutePath()); + } + + // 5. 设置模式 (文件/目录) + if (getFileSelectionMode() == DIRECTORIES_ONLY) fc.setMode(JnaFileChooser.Mode.Directories); + else fc.setMode(JnaFileChooser.Mode.Files); // 默认为文件 + + // 6. 显示原生窗口 + Window window = getWindowForComponent(parent); + boolean result; + if (getDialogType() == SAVE_DIALOG) { + result = fc.showSaveDialog(window); + } else { + result = fc.showOpenDialog(window); + } + + // 7. 处理结果并转换类型 (boolean -> int) + if (result) { + File selected = fc.getSelectedFile(); + setSelectedFile(selected); // 回填文件,供 getSelectedFile() 调用 + persistDirectory(selected); + return APPROVE_OPTION; // 返回 0 + } else { + return CANCEL_OPTION; // 返回 1 + } + } + return super.showDialog(parent, approveButtonText); + } + + // --- 兼容性重写 --- + + @Override + public int showOpenDialog(Component parent) { + setDialogType(OPEN_DIALOG); + return showDialog(parent, "Open"); } @Override - public int showDialog(Component parent, String approveButtonText) { - int result = super.showDialog(parent, approveButtonText); - if (result == APPROVE_OPTION) { - persistDirectory(getSelectedPath()); - } - return result; + public int showSaveDialog(Component parent) { + setDialogType(SAVE_DIALOG); + return showDialog(parent, "Save"); } - private String getSelectedPath() { - File selected = getSelectedFile(); - return (selected != null && selected.isDirectory()) ? - selected.getAbsolutePath() : - getCurrentDirectory().getAbsolutePath(); + // --- 辅助工具 --- + + private Window getWindowForComponent(Component parent) { + if (parent == null) return null; + if (parent instanceof Window) return (Window) parent; + return SwingUtilities.getWindowAncestor(parent); } private static File loadValidDirectory() { try { Preferences prefs = Preferences.userNodeForPackage(JarApiProfilingWindow.class); - String path = prefs.get(PREF_KEY, FALLBACK_DIR.getAbsolutePath()); - - File dir = new File(path).getCanonicalFile(); - return dir.isDirectory() && Files.isReadable(dir.toPath()) ? - dir : - FALLBACK_DIR; - - } catch (IOException | InvalidPathException | SecurityException e) { - return FALLBACK_DIR; - } + String path = prefs.get(PREF_KEY, null); + if (path != null) { + File dir = new File(path); + if (dir.exists() && dir.isDirectory()) return dir; + } + } catch (Exception e) {} + return null; } - private void persistDirectory(String path) { + private void persistDirectory(File file) { + if (file == null) return; try { - File dir = new File(path).getCanonicalFile(); - if (dir.isDirectory() && Files.isWritable(dir.toPath())) { + File dir = file.isDirectory() ? file : file.getParentFile(); + if (dir != null && dir.exists()) { Preferences prefs = Preferences.userNodeForPackage(JarApiProfilingWindow.class); prefs.put(PREF_KEY, dir.getAbsolutePath()); } - } catch (IOException | InvalidPathException | SecurityException e) { - System.err.println("无法保存目录: " + e.getMessage()); - } + } catch (Exception e) {} } } \ No newline at end of file diff --git a/src/main/java/com/axis/innovators/box/util/FileAssociationManager.java b/src/main/java/com/axis/innovators/box/util/FileAssociationManager.java index f567701..5ec23af 100644 --- a/src/main/java/com/axis/innovators/box/util/FileAssociationManager.java +++ b/src/main/java/com/axis/innovators/box/util/FileAssociationManager.java @@ -1,5 +1,7 @@ package com.axis.innovators.box.util; +import com.axis.innovators.box.util.build.BuildSystem; + import java.io.IOException; /** diff --git a/src/main/java/com/axis/innovators/box/util/Tray.java b/src/main/java/com/axis/innovators/box/util/Tray.java index 10504e8..d0557eb 100644 --- a/src/main/java/com/axis/innovators/box/util/Tray.java +++ b/src/main/java/com/axis/innovators/box/util/Tray.java @@ -4,6 +4,7 @@ import com.axis.innovators.box.AxisInnovatorsBox; import com.axis.innovators.box.browser.MainApplication; import com.axis.innovators.box.decompilation.gui.ModernJarViewer; import com.axis.innovators.box.tools.RegisterTray; +import com.axis.innovators.box.util.build.BuildSystem; import javax.swing.*; import java.util.ArrayList; @@ -18,7 +19,7 @@ public class Tray { private static final Random random = new Random(); private static final String trayName = "轴创工具箱 v1.1"; private static final String trayDescription = "轴创工具箱"; - private static final String trayIcon =System.getProperty("user.dir") + "/logo.ico"; + private static final String trayIcon = System.getProperty("user.dir") + "/logo.ico"; private static List menuItems = null; private static final List idx = new ArrayList<>(); private static final List trayLabelsList = new ArrayList<>(); @@ -35,8 +36,7 @@ public class Tray { }); load(new TrayLabels("启动 Jar 查看器", () -> SwingUtilities.invokeLater(() -> { - ModernJarViewer viewer = new ModernJarViewer(null); - viewer.setVisible(true); + ModernJarViewer.popupSimulatingWindow(null,null); }))); load(new TrayLabels("启动 HTML 查看器", new Runnable() { diff --git a/src/main/java/com/axis/innovators/box/util/build/BuildInformation.java b/src/main/java/com/axis/innovators/box/util/build/BuildInformation.java new file mode 100644 index 0000000..f0c289f --- /dev/null +++ b/src/main/java/com/axis/innovators/box/util/build/BuildInformation.java @@ -0,0 +1,129 @@ +package com.axis.innovators.box.util.build; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.InputStream; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Properties; + +/** + * 静态的构建信息 + */ +public class BuildInformation { + private static final Logger logger = LogManager.getLogger(BuildInformation.class); + private static String VERSION = ""; + private static final String BUILD_TIME = ""; + private static final BuildSystem BUILD_SYSTEM; + private static final LocalDateTime BUILD_TIMESTAMP; + + static { + Properties props = new Properties(); + String version = "Unknown"; + String timestampStr = ""; + BuildSystem buildSystem = BuildSystem.UNKNOWN; + try (InputStream input = BuildInformation.class.getResourceAsStream("/build/build.properties")) { + if (input != null) { + props.load(input); + version = props.getProperty("version", "Unknown"); + timestampStr = props.getProperty("buildTimestamp", ""); + String systemStr = props.getProperty("buildSystem", "UNKNOWN"); + + try { + buildSystem = BuildSystem.valueOf(systemStr.toUpperCase()); + } catch (IllegalArgumentException ignored) { + } + } else { + logger.info("Notice: build.properties not found, using default values."); + } + } catch (Exception e) { + logger.info("Error loading build properties: {}", e.getMessage()); + } + + VERSION = version; + BUILD_SYSTEM = buildSystem; + + LocalDateTime parsedTime = null; + if (timestampStr != null && !timestampStr.isBlank()) { + try { + // 优先处理 ISO 格式 (2024-05-20T10:15:30) + parsedTime = LocalDateTime.parse(timestampStr); + } catch (DateTimeParseException e) { + try { + // 备用格式 + parsedTime = LocalDateTime.parse(timestampStr, + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } catch (DateTimeParseException e2) { + logger.info("Could not parse timestamp: {}", timestampStr); + } + } + } + BUILD_TIMESTAMP = parsedTime; + } + + /** + * 判断指定系统字符串是否匹配当前的构建系统。 + * 支持缩写:win -> WINDOWS, li -> LINUX, mac -> MACOS + * + * @param system 输入的系统标识字符串(如 "win", "WINDOWS", "li" 等) + * @return 如果匹配当前的 BUILD_SYSTEM 则返回 true + */ + public static boolean isMatchSystem(String system) { + if (system == null) { + throw new NullPointerException("system cannot be null"); + } + + String lowerInput = system.toLowerCase().trim(); + BuildSystem target; + if (lowerInput.contains("win")) { + target = BuildSystem.WINDOWS; + } else if (lowerInput.contains("li")) { + target = BuildSystem.LINUX; + } else if (lowerInput.contains("mac")) { + target = BuildSystem.MACOS; + } else { + try { + target = BuildSystem.valueOf(lowerInput.toUpperCase()); + } catch (IllegalArgumentException e) { + target = BuildSystem.UNKNOWN; + } + } + return target == BUILD_SYSTEM; + } + + /** + * 判断指定系统是否为匹配的目标系统 + * + * 该方法用于检查传入的系统对象是否与预定义的系统常量相匹配。 + * 通常用于在系统中识别特定的系统配置或环境,如构建系统、测试系统等。 + * + * 示例: + * isMatchSystem(currentSystem) // 如果 currentSystem 是 BUILD_SYSTEM,则返回 true + * + * @param system 需要判断的系统对象,不能为 null + * @return 匹配结果:当传入的系统与预定义的 BUILD_SYSTEM 相同时返回 true;否则返回 false + * @throws NullPointerException 如果传入的系统对象为 null + * + * @see #BUILD_SYSTEM 用于比对的预定义系统常量 + */ + public static boolean isMatchSystem(BuildSystem system) { + if (system == null) { + throw new NullPointerException("system cannot be null"); + } + // 兼容未定义的构建系统 + if (BUILD_SYSTEM == BuildSystem.UNKNOWN){ + return true; + } + return system == BUILD_SYSTEM; + } + + public static String getVersion() { + return VERSION; + } + + public static String getBuildTime() { + return BUILD_TIME; + } +} diff --git a/src/main/java/com/axis/innovators/box/util/build/BuildSystem.java b/src/main/java/com/axis/innovators/box/util/build/BuildSystem.java new file mode 100644 index 0000000..09e3eee --- /dev/null +++ b/src/main/java/com/axis/innovators/box/util/build/BuildSystem.java @@ -0,0 +1,8 @@ +package com.axis.innovators.box.util.build; + +public enum BuildSystem { + UNKNOWN, + WINDOWS, + LINUX, + MACOS +} diff --git a/src/main/java/com/axis/innovators/box/util/build/build.properties b/src/main/java/com/axis/innovators/box/util/build/build.properties new file mode 100644 index 0000000..7e14cca --- /dev/null +++ b/src/main/java/com/axis/innovators/box/util/build/build.properties @@ -0,0 +1,4 @@ +# build.properties +Version=1.0.0 +Build_Timestamp=2023-10-01T12:00:00Z +Build_System=LINUX \ No newline at end of file diff --git a/src/main/java/com/axis/innovators/box/window/LoadIcon.java b/src/main/java/com/axis/innovators/box/window/LoadIcon.java index 041a49a..e536228 100644 --- a/src/main/java/com/axis/innovators/box/window/LoadIcon.java +++ b/src/main/java/com/axis/innovators/box/window/LoadIcon.java @@ -27,6 +27,73 @@ public class LoadIcon { return loadIcon(LoadIcon.class, filename, size); } + /** + * 加载指定宽高的图片(适用于背景图) + * @param filename 图片名 + * @param width 宽度 + * @param height 高度 + * @return ImageIcon对象 + */ + public static ImageIcon loadIcon(String filename, int width, int height) { + return loadIcon(LoadIcon.class, filename, width, height); + } + + /** + * 加载指定宽高的图片(核心构造体) + * @param clazz resources包所在的jar + * @param filename 图片名 + * @param width 目标宽度 + * @param height 目标高度 + * @return ImageIcon对象 + */ + public static ImageIcon loadIcon(Class clazz, String filename, int width, int height) { + try { + if (filename == null || filename.isEmpty()) { + return createPlaceholderIcon(width, height); + } + + Image image; + // 1. 处理绝对路径 + if (new File(filename).isAbsolute()) { + image = new ImageIcon(filename).getImage(); + } else { + // 2. 处理资源路径 + String fullPath = ICON_PATH + filename; + URL imgUrl = clazz.getResource(fullPath); + if (imgUrl == null) { + // 尝试不带 /icons/ 路径直接加载 (兼容 loadIcon0 逻辑) + imgUrl = clazz.getResource(filename); + } + + if (imgUrl == null) { + logger.warn("Resource not found: {}", filename); + return createPlaceholderIcon(width, height); + } + image = new ImageIcon(imgUrl).getImage(); + } + + // 3. 执行高质量缩放 + return new ImageIcon(image.getScaledInstance(width, height, Image.SCALE_SMOOTH)); + + } catch (Exception e) { + logger.error("Failed to load image: {}", filename, e); + return createPlaceholderIcon(width, height); + } + } + + /** + * 创建指定大小的占位图 + */ + private static ImageIcon createPlaceholderIcon(int width, int height) { + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = img.createGraphics(); + // 使用更符合现代深色主题的占位颜色 + g2d.setColor(new Color(30, 30, 30)); + g2d.fillRect(0, 0, width, height); + g2d.dispose(); + return new ImageIcon(img); + } + /** * 加载图片 * @param clazz resources包所在的jar diff --git a/src/main/java/com/axis/innovators/box/window/ProgressBarManager.java b/src/main/java/com/axis/innovators/box/window/ProgressBarManager.java index 24241fb..d0a1d9d 100644 --- a/src/main/java/com/axis/innovators/box/window/ProgressBarManager.java +++ b/src/main/java/com/axis/innovators/box/window/ProgressBarManager.java @@ -1,462 +1,163 @@ package com.axis.innovators.box.window; +import com.axis.innovators.box.AxisInnovatorsBox; +import org.jetbrains.annotations.NotNull; + import javax.swing.*; import java.awt.*; import java.awt.event.*; -import java.awt.geom.*; -import java.util.HashMap; -import java.util.Map; +import java.awt.geom.RoundRectangle2D; /** - * 启动窗口的任务系统(已改造为现代流畅动画视觉效果) - * 注意:保留了原有对外方法和签名(updateMainProgress/updateSubProgress/setTotalTasks/close) - * 作者: tzdwindows 7(UI 改造版) + * 现代简约风格启动窗口 - 已添加 Logo 支持 */ -public class ProgressBarManager extends WindowsJDialog { - private JFrame loadingFrame; - private SmoothProgressBar mainProgressBar; - private SmoothProgressBar subProgressBar; - private JLabel statusLabel; - private JLabel timeLabel; - private long startTime; - - private int totalTasks; - private int completedTasks; - private Map subTasks = new HashMap<>(); - - // 动画计时器(60FPS) - private Timer animationTimer; - - // 视觉参数 - private Color accentColor = new Color(0x00C2FF); // 科技感青蓝 - private Font uiFont; +public class ProgressBarManager { + private final JFrame frame; + private final MinimalProgressBar mainBar; + private final JLabel statusLabel; + private final int totalTasks; + private final int arc = 20; // 圆角半径 public ProgressBarManager(String title, int totalTasks) { this.totalTasks = Math.max(1, totalTasks); - this.completedTasks = 0; - this.startTime = System.currentTimeMillis(); - // 尝试设置现代中文友好字体(Windows 常见) - try { - uiFont = new Font("Microsoft YaHei UI", Font.PLAIN, 13); - // 若系统无该字体则 fallback - if (!uiFont.getFamily().toLowerCase().contains("microsoft") && - !uiFont.getFamily().toLowerCase().contains("yahei")) { - uiFont = new Font("Segoe UI", Font.PLAIN, 13); - } - } catch (Throwable t) { - uiFont = new Font(Font.SANS_SERIF, Font.PLAIN, 13); - } + frame = new JFrame(); + frame.setUndecorated(true); + frame.setBackground(new Color(0, 0, 0, 0)); // 允许圆角透明 + frame.setSize(600, 360); + frame.setLocationRelativeTo(null); - loadingFrame = new JFrame(title); - loadingFrame.setUndecorated(true); - loadingFrame.setBackground(new Color(0, 0, 0, 0)); // 允许圆角透明背景 - loadingFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - loadingFrame.setSize(520, 300); - loadingFrame.setLocationRelativeTo(null); - loadingFrame.setIconImage(LoadIcon.loadIcon("logo.png", 64).getImage()); + JPanel contentPane = getjPanel(); + frame.setContentPane(contentPane); + int logoSize = 100; + JLabel logoLabel = new JLabel(LoadIcon.loadIcon("logo.png", logoSize)); + logoLabel.setBounds((600 - logoSize) / 2, 85, logoSize, logoSize); + contentPane.add(logoLabel); + statusLabel = new JLabel("Starting system..."); + statusLabel.setForeground(new Color(255, 255, 255, 160)); + statusLabel.setFont(new Font("Microsoft YaHei UI", Font.PLAIN, 11)); + statusLabel.setBounds(45, 285, 400, 20); + contentPane.add(statusLabel); + mainBar = new MinimalProgressBar(); + mainBar.setBounds(45, 310, 510, 4); + contentPane.add(mainBar); + WindowDragger.makeDraggable(frame, contentPane); + frame.setVisible(true); + } - // 主容器(带动画背景和圆角卡片) - AnimatedBackgroundPanel root = new AnimatedBackgroundPanel(); - root.setLayout(new GridBagLayout()); - root.setBorder(BorderFactory.createEmptyBorder(18, 18, 18, 18)); + private @NotNull JPanel getjPanel() { + JPanel contentPane = new JPanel(null) { + private final Image bg = LoadIcon.loadIcon("startup_background.png", 600, 360).getImage(); - // 卡片面板 - JPanel card = new JPanel() { @Override protected void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g.create(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // 卡片阴影(简单外发光) - int arc = 18; + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); int w = getWidth(); int h = getHeight(); - - // 背景渐变 - GradientPaint gp = new GradientPaint(0, 0, new Color(20, 22, 25, 230), - 0, h, new Color(14, 16, 19, 230)); - g2.setPaint(gp); - - // 圆角矩形 - RoundRectangle2D rr = new RoundRectangle2D.Float(6, 6, w - 12, h - 12, arc, arc); - g2.fill(rr); - - // 细微边框 - g2.setStroke(new BasicStroke(1f)); - g2.setColor(new Color(255, 255, 255, 10)); - g2.draw(rr); - + RoundRectangle2D body = new RoundRectangle2D.Float(0, 0, w, h, arc, arc); + g2.setClip(body); + g2.drawImage(bg, 0, 0, w, h, this); + g2.setClip(null); + g2.setColor(new Color(255, 255, 255, 35)); + g2.setStroke(new BasicStroke(1.2f)); + g2.draw(body); + g2.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + g2.setColor(new Color(255, 255, 255, 100)); + g2.drawString("v" + AxisInnovatorsBox.getVersion(), 35, h - 25); + String vendor = "Axis Innovators Box"; + int vWidth = g2.getFontMetrics().stringWidth(vendor); + g2.drawString(vendor, w - vWidth - 35, h - 25); g2.dispose(); - super.paintComponent(g); } }; - card.setOpaque(false); - card.setLayout(new BorderLayout(12, 12)); - card.setPreferredSize(new Dimension(480, 240)); - card.setBorder(BorderFactory.createEmptyBorder(14, 14, 14, 14)); - - // 顶部 logo + 标题 - JPanel top = new JPanel(new BorderLayout()); - top.setOpaque(false); - JLabel logoLabel = new JLabel(LoadIcon.loadIcon("logo.png", 48)); - logoLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 12)); - JLabel titleLabel = new JLabel(title); - titleLabel.setFont(uiFont.deriveFont(Font.BOLD, 18f)); - titleLabel.setForeground(Color.WHITE); - - top.add(logoLabel, BorderLayout.WEST); - top.add(titleLabel, BorderLayout.CENTER); - - // 中间进度区 - JPanel center = new JPanel(new GridBagLayout()); - center.setOpaque(false); - GridBagConstraints c = new GridBagConstraints(); - c.gridx = 0; - c.gridy = 0; - c.weightx = 1; - c.fill = GridBagConstraints.HORIZONTAL; - - mainProgressBar = new SmoothProgressBar(0); - mainProgressBar.setPreferredSize(new Dimension(420, 26)); - mainProgressBar.setAccentColor(accentColor); - - subProgressBar = new SmoothProgressBar(0); - subProgressBar.setPreferredSize(new Dimension(420, 18)); - subProgressBar.setAccentColor(new Color(0x6EE7FF)); - subProgressBar.setShowStripe(true); - - center.add(mainProgressBar, c); - c.gridy++; - c.insets = new Insets(8, 0, 0, 0); - center.add(subProgressBar, c); - - // 底部文本 - JPanel bottom = new JPanel(new BorderLayout()); - bottom.setOpaque(false); - statusLabel = new JLabel("Initializing...", SwingConstants.LEFT); - statusLabel.setFont(uiFont.deriveFont(Font.PLAIN, 12f)); - statusLabel.setForeground(new Color(220, 230, 240)); - - timeLabel = new JLabel("Elapsed: 0s", SwingConstants.RIGHT); - timeLabel.setFont(uiFont.deriveFont(Font.PLAIN, 12f)); - timeLabel.setForeground(new Color(180, 200, 215)); - - bottom.add(statusLabel, BorderLayout.WEST); - bottom.add(timeLabel, BorderLayout.EAST); - bottom.setBorder(BorderFactory.createEmptyBorder(8, 2, 2, 2)); - - card.add(top, BorderLayout.NORTH); - card.add(center, BorderLayout.CENTER); - card.add(bottom, BorderLayout.SOUTH); - - root.add(card); - loadingFrame.setContentPane(root); - - // 拖动窗口支持(在无边框下) - WindowDragger.makeDraggable(loadingFrame, card); - - // 启动动画定时器 - animationTimer = new Timer(1000 / 60, e -> { - boolean repaintNeeded = false; - if (mainProgressBar.animateStep()) repaintNeeded = true; - if (subProgressBar.animateStep()) repaintNeeded = true; - root.advanceAnimation(); - updateTimeLabel(); - if (repaintNeeded) { - root.repaint(); - } else { - // 仍需刷新背景动画 - root.repaint(); - } - }); - animationTimer.start(); - - loadingFrame.setVisible(true); - - // 防止用户误操作关闭(保持原行为) - loadingFrame.addWindowListener(new WindowAdapter() { - @Override - public void windowClosing(WindowEvent e) { - // DO NOTHING - } - }); + contentPane.setOpaque(false); + return contentPane; } - /** - * 更新主任务进度(对外接口保持不变) - * @param completedTasks 已完成的主任务数量 - */ - public void updateMainProgress(int completedTasks) { - this.completedTasks = completedTasks; - double progress = (completedTasks / (double) Math.max(1, totalTasks)) * 100.0; - if (progress < 0) progress = 0; - if (progress > 100) progress = 100; - mainProgressBar.setTarget((int) Math.round(progress)); - statusLabel.setText("主任务: " + completedTasks + " / " + totalTasks + " (" + (int) progress + "%)"); + // 更新进度逻辑保持不变 + public void updateMainProgress(int completed) { + double percent = (completed / (double) totalTasks) * 100; + mainBar.setProgress((int) percent); + statusLabel.setText(String.format("Loading components... %d%%", (int) percent)); } - /** - * 更新子任务进度(对外接口保持不变) - * @param subTaskName 子任务名称 - * @param subTaskCompleted 已完成的子任务数量 - * @param subTaskTotal 子任务总数 - */ - public void updateSubProgress(String subTaskName, int subTaskCompleted, int subTaskTotal) { - if (subTaskTotal <= 0) subTaskTotal = 1; - subTasks.put(subTaskName, subTaskCompleted); - double progress = (subTaskCompleted / (double) subTaskTotal) * 100.0; - if (progress < 0) progress = 0; - if (progress > 100) progress = 100; - subProgressBar.setTarget((int) Math.round(progress)); - subProgressBar.setLabel(subTaskName); + public void updateSubProgress(String name, int completed, int total) { + statusLabel.setText(name + "..."); } - /** - * 更新总任务数 - */ - public void setTotalTasks(int totalTasks) { - this.totalTasks = Math.max(1, totalTasks); - } + public void close() { frame.dispose(); } - /** - * 关闭加载窗口 - */ - public void close() { - if (animationTimer != null && animationTimer.isRunning()) { - animationTimer.stop(); - } - loadingFrame.dispose(); - } + private static class MinimalProgressBar extends JComponent { + private int progress = 0; + private double smoothProgress = 0; + private final Timer timer; - /** - * 更新时间标签 - */ - private void updateTimeLabel() { - long elapsedTime = (System.currentTimeMillis() - startTime) / 1000; - long hours = elapsedTime / 3600; - long mins = (elapsedTime % 3600) / 60; - long secs = elapsedTime % 60; - if (hours > 0) { - timeLabel.setText(String.format("Elapsed: %dh %02dm %02ds", hours, mins, secs)); - } else if (mins > 0) { - timeLabel.setText(String.format("Elapsed: %dm %02ds", mins, secs)); - } else { - timeLabel.setText(String.format("Elapsed: %ds", secs)); - } - } - - // -------------------------- - // 内部类:平滑进度条(支持插值动画、条纹、标签) - // -------------------------- - private static class SmoothProgressBar extends JComponent { - private int target = 0; - private double displayed = 0.0; - private int height = 20; - private Color base = new Color(255, 255, 255, 18); - private Color fill = new Color(0x00C2FF); - private String label = ""; - private boolean showStripe = false; - private Color stripeColor = new Color(255, 255, 255, 30); - private double stripeOffset = 0.0; - - public SmoothProgressBar(int initial) { - this.target = Math.max(0, Math.min(100, initial)); - this.displayed = this.target; - setOpaque(false); - setPreferredSize(new Dimension(200, height)); + public MinimalProgressBar() { + timer = new Timer(16, e -> { + if (Math.abs(progress - smoothProgress) > 0.1) { + smoothProgress += (progress - smoothProgress) * 0.12; + repaint(); + } + }); + timer.start(); } - public void setAccentColor(Color c) { - this.fill = c; - } - - public void setLabel(String label) { - this.label = label; - } - - public void setShowStripe(boolean v) { - this.showStripe = v; - } - - public void setTarget(int t) { - t = Math.max(0, Math.min(100, t)); - this.target = t; - } - - /** - * 每帧推进插值,返回是否需要重绘 - */ - public boolean animateStep() { - // 平滑插值(阻尼) - double diff = target - displayed; - if (Math.abs(diff) < 0.02) { - displayed = target; - } else { - displayed += diff * 0.18; // 阻尼因子(调整流畅度) - } - - // 条纹动画 - if (showStripe) { - stripeOffset += 1.8; - if (stripeOffset > 60) stripeOffset = 0; - } - - // 是否需要重绘 - return Math.abs(diff) > 0.001 || showStripe; - } + public void setProgress(int p) { this.progress = p; } @Override protected void paintComponent(Graphics g) { - int w = getWidth(); - int h = getHeight(); Graphics2D g2 = (Graphics2D) g.create(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - // 背景轨道 - RoundRectangle2D bg = new RoundRectangle2D.Float(0, 0, w, h, h, h); - g2.setColor(base); - g2.fill(bg); + int w = getWidth(); + int h = getHeight(); + int currentW = (int) (w * (smoothProgress / 100.0)); - // 阴影(内阴影模拟) - g2.setColor(new Color(0, 0, 0, 30)); - g2.setStroke(new BasicStroke(1f)); - g2.draw(bg); + // 1. 背景槽:增加一点深度感 + g2.setColor(new Color(255, 255, 255, 15)); + g2.fill(new RoundRectangle2D.Float(0, 0, w, h, h, h)); - // 填充(渐变) - int fillW = (int) Math.round((displayed / 100.0) * w); - if (fillW > 0) { - GradientPaint gp = new GradientPaint(0, 0, fill.brighter(), w, 0, fill.darker()); - RoundRectangle2D fg = new RoundRectangle2D.Float(0, 0, fillW, h, h, h); - g2.setPaint(gp); - g2.fill(fg); - - // 发光边缘 - g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.25f)); - g2.setColor(fill); - g2.fill(new RoundRectangle2D.Float(0, -h / 3f, fillW, h + h / 3f, h, h)); + if (currentW > 0) { + // 2. 绘制外发光 (关键:解决“空”的感觉) + // 在进度条下方绘制一层模糊的青色,增加视觉占位 + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.4f)); + Color glowColor = new Color(0x00C2FF); + for (int i = 1; i <= 3; i++) { + g2.setColor(new Color(glowColor.getRed(), glowColor.getGreen(), glowColor.getBlue(), 40 / i)); + g2.fill(new RoundRectangle2D.Float(0, 0, currentW, h + i, h, h)); + } g2.setComposite(AlphaComposite.SrcOver); - } - // 条纹效果 - if (showStripe && fillW > 6) { - Shape clip = g2.getClip(); - g2.setClip(new RoundRectangle2D.Float(0, 0, fillW, h, h, h)); - int stripeW = 18; - for (int x = -stripeW * 2; x < w + stripeW * 2; x += stripeW) { - int sx = (int) (x + stripeOffset); - Polygon p = new Polygon(); - p.addPoint(sx, 0); - p.addPoint(sx + stripeW, 0); - p.addPoint(sx + stripeW - 8, h); - p.addPoint(sx - 8, h); - g2.setColor(stripeColor); - g2.fill(p); - } - g2.setClip(clip); - } + // 3. 核心进度条 + // 使用横向渐变,让光效有流动感 + GradientPaint gradient = new GradientPaint(0, 0, new Color(0x00C2FF), currentW, 0, new Color(0x0088FF)); + g2.setPaint(gradient); + g2.fill(new RoundRectangle2D.Float(0, 0, currentW, h, h, h)); - // 文本显示(居中) - String text; - if (label != null && !label.isEmpty()) { - text = label + " " + Math.round(displayed) + "%"; - } else { - text = Math.round(displayed) + "%"; + // 4. 头部高亮点 (让线条看起来在发光) + g2.setColor(Color.WHITE); + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f)); + g2.fill(new RoundRectangle2D.Float(currentW - 5, 0, 5, h, h, h)); } - g2.setFont(new Font(Font.SANS_SERIF, Font.BOLD, Math.max(11, h - 6))); - FontMetrics fm = g2.getFontMetrics(); - int tx = (w - fm.stringWidth(text)) / 2; - int ty = (h + fm.getAscent() - fm.getDescent()) / 2; - g2.setColor(new Color(255, 255, 255, 210)); - g2.drawString(text, tx, ty); g2.dispose(); } } - // -------------------------- - // 内部类:带动画效果的背景面板(流动扫描线 + 颗粒/渐变) - // -------------------------- - private class AnimatedBackgroundPanel extends JPanel { - private double offset = 0; - private double particlePhase = 0; - - public AnimatedBackgroundPanel() { - setOpaque(false); - } - - public void advanceAnimation() { - offset += 0.9; - if (offset > 2000) offset = 0; - particlePhase += 0.02; - } - - @Override - protected void paintComponent(Graphics g) { - int w = getWidth(); - int h = getHeight(); - Graphics2D g2 = (Graphics2D) g.create(); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // 背景渐变(深色) - Paint p = new GradientPaint(0, 0, new Color(8, 10, 12), w, h, new Color(18, 20, 24)); - g2.setPaint(p); - g2.fillRect(0, 0, w, h); - - // 斜向扫描线(细微) - g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.06f)); - g2.setColor(Color.WHITE); - for (int i = -200; i < w + h; i += 40) { - int x1 = i + (int) offset; - int y1 = 0; - int x2 = i - h + (int) offset; - int y2 = h; - g2.setStroke(new BasicStroke(2f)); - g2.drawLine(x1, y1, x2, y2); - } - g2.setComposite(AlphaComposite.SrcOver); - - // 轻微颗粒(科技光斑) - g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.08f)); - for (int i = 0; i < 10; i++) { - float px = (float) ((Math.sin(particlePhase + i) + 1) / 2.0 * w); - float py = (float) ((Math.cos(particlePhase * 0.7 + i * 0.3) + 1) / 2.0 * h); - int size = 6 + (i % 3) * 4; - g2.fillOval((int) px, (int) py, size, size); - } - g2.setComposite(AlphaComposite.SrcOver); - - g2.dispose(); - super.paintComponent(g); - } - } - - // -------------------------- - // 工具:使无边框窗口可拖动 - // -------------------------- private static class WindowDragger { - public static void makeDraggable(Window wnd, Component dragRegion) { - final Point[] mouseDown = {null}; - dragRegion.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - mouseDown[0] = e.getPoint(); - } - - @Override - public void mouseReleased(MouseEvent e) { - mouseDown[0] = null; - } + public static void makeDraggable(JFrame f, JPanel p) { + Point move = new Point(); + p.addMouseListener(new MouseAdapter() { + public void mousePressed(MouseEvent e) { move.setLocation(e.getPoint()); } }); - dragRegion.addMouseMotionListener(new MouseMotionAdapter() { - @Override + p.addMouseMotionListener(new MouseMotionAdapter() { public void mouseDragged(MouseEvent e) { - if (mouseDown[0] != null) { - Point curr = e.getLocationOnScreen(); - wnd.setLocation(curr.x - mouseDown[0].x, curr.y - mouseDown[0].y); - } + Point curr = e.getLocationOnScreen(); + f.setLocation(curr.x - move.x, curr.y - move.y); } }); } } -} +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/Main.java b/src/main/java/com/chuangzhou/vivid2D/Main.java deleted file mode 100644 index ff667bd..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/Main.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.chuangzhou.vivid2D; - -import com.chuangzhou.vivid2D.window.MainWindow; -import com.formdev.flatlaf.themes.FlatMacDarkLaf; - -import javax.swing.*; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; - -public class Main { - public static void main(String[] args) { - FlatMacDarkLaf.setup(); - System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); - System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); - SwingUtilities.invokeLater(() -> { - MainWindow mainWin = new MainWindow(); - mainWin.setVisible(true); - }); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/Vivid2D.java b/src/main/java/com/chuangzhou/vivid2D/Vivid2D.java deleted file mode 100644 index ad1d80a..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/Vivid2D.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.chuangzhou.vivid2D; - -import com.chuangzhou.vivid2D.window.MainWindow; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public class Vivid2D { - private static final Logger logger = LogManager.getLogger(Vivid2D.class); - private static final String VERSIONS = "0.0.1"; - private static final String[] AUTHOR = new String[]{ - "tzdwindows 7" - }; - - private MainWindow mainWindow; -} diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/ModelManagement.java b/src/main/java/com/chuangzhou/vivid2D/ai/ModelManagement.java deleted file mode 100644 index 7e90b3b..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/ModelManagement.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.chuangzhou.vivid2D.ai; - -import com.chuangzhou.vivid2D.ai.anime_face_segmentation.AnimeModelWrapper; -import com.chuangzhou.vivid2D.ai.anime_segmentation.Anime2VividModelWrapper; -import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetVividModelWrapper; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 模型管理器 - 负责模型的注册、分类和检索 - */ -public class ModelManagement { - private final Map> models = new ConcurrentHashMap<>(); - private final Map> modelsByCategory = new ConcurrentHashMap<>(); - private final List modelDisplayNames = new ArrayList<>(); - private final Map displayNameToRegistrationName = new ConcurrentHashMap<>(); - - private ModelManagement() { - initializeDefaultCategories(); - registerDefaultModels(); - } - - /** - * 初始化默认分类 - */ - private void initializeDefaultCategories() { - modelsByCategory.put("Image Segmentation", new ArrayList<>()); - modelsByCategory.put("Image Processing", new ArrayList<>()); - modelsByCategory.put("Image Generation", new ArrayList<>()); - modelsByCategory.put("Image Inpainting", new ArrayList<>()); - modelsByCategory.put("Image Completion", new ArrayList<>()); - modelsByCategory.put("Face Analysis", new ArrayList<>()); - } - - /** - * 注册默认模型 - */ - private void registerDefaultModels() { - registerModel("segmentation:anime_face", "Anime Face Segmentation", - AnimeModelWrapper.class, "Image Segmentation"); - registerModel("segmentation:anime", "Anime Image Segmentation", - Anime2VividModelWrapper.class, "Image Segmentation"); - registerModel("segmentation:face_parsing", "Face Parsing", - BiSeNetVividModelWrapper.class, "Image Segmentation"); - } - - /** - * 注册模型 - * @param modelRegistrationName 注册名称,格式必须为 "category:model_name" - * @param modelDisplayName 模型显示名称 - * @param modelClass 模型类 - * @param category 模型类别 - */ - public void registerModel(String modelRegistrationName, String modelDisplayName, - Class modelClass, String category) { - if (!isValidRegistrationName(modelRegistrationName)) { - throw new IllegalArgumentException( - "Invalid registration name format. Expected 'category:model_name', got: " + modelRegistrationName); - } - if (models.containsKey(modelRegistrationName)) { - throw new IllegalArgumentException( - "Model registration name already exists: " + modelRegistrationName); - } - if (displayNameToRegistrationName.containsKey(modelDisplayName)) { - throw new IllegalArgumentException( - "Model display name already exists: " + modelDisplayName); - } - if (!modelsByCategory.containsKey(category)) { - modelsByCategory.put(category, new ArrayList<>()); - } - models.put(modelRegistrationName, modelClass); - displayNameToRegistrationName.put(modelDisplayName, modelRegistrationName); - modelDisplayNames.add(modelDisplayName); - modelsByCategory.get(category).add(modelRegistrationName); - } - - /** - * 验证注册名称格式 - */ - private boolean isValidRegistrationName(String name) { - return name != null && name.matches("^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+$"); - } - - /** - * 通过显示名称获取模型类 - */ - public Class getModel(String modelDisplayName) { - String registrationName = displayNameToRegistrationName.get(modelDisplayName); - return registrationName != null ? models.get(registrationName) : null; - } - - /** - * 通过索引获取模型类 - */ - public Class getModel(int modelIndex) { - if (modelIndex >= 0 && modelIndex < modelDisplayNames.size()) { - String displayName = modelDisplayNames.get(modelIndex); - return getModel(displayName); - } - return null; - } - - /** - * 通过注册名称获取模型类 - */ - public Class getModelByRegistrationName(String registrationName) { - return models.get(registrationName); - } - - /** - * 通过类名获取模型类 - */ - public Class getModelByClassName(String className) { - for (Class modelClass : models.values()) { - if (modelClass.getName().equals(className)) { - return modelClass; - } - } - return null; - } - - /** - * 获取所有模型的显示名称 - */ - public List getAllModelDisplayNames() { - return Collections.unmodifiableList(modelDisplayNames); - } - - /** - * 获取所有模型的注册名称 - */ - public Set getAllModelRegistrationNames() { - return Collections.unmodifiableSet(models.keySet()); - } - - /** - * 按类别获取模型注册名称 - */ - public List getModelsByCategory(String category) { - return Collections.unmodifiableList( - modelsByCategory.getOrDefault(category, new ArrayList<>()) - ); - } - - /** - * 获取所有可用的类别 - */ - public Set getAllCategories() { - return Collections.unmodifiableSet(modelsByCategory.keySet()); - } - - /** - * 获取模型数量 - */ - public int getModelCount() { - return modelDisplayNames.size(); - } - - /** - * 获取模型显示名称对应的注册名称 - */ - public String getRegistrationName(String modelDisplayName) { - return displayNameToRegistrationName.get(modelDisplayName); - } - - /** - * 获取模型注册名称对应的显示名称 - */ - public String getDisplayName(String registrationName) { - for (Map.Entry entry : displayNameToRegistrationName.entrySet()) { - if (entry.getValue().equals(registrationName)) { - return entry.getKey(); - } - } - return null; - } - - /** - * 检查模型是否存在 - */ - public boolean containsModel(String modelDisplayName) { - return displayNameToRegistrationName.containsKey(modelDisplayName); - } - - /** - * 检查注册名称是否存在 - */ - public boolean containsRegistrationName(String registrationName) { - return models.containsKey(registrationName); - } - - /** - * 移除模型 - */ - public boolean removeModel(String modelDisplayName) { - String registrationName = displayNameToRegistrationName.get(modelDisplayName); - if (registrationName != null) { - // 从所有存储中移除 - models.remove(registrationName); - displayNameToRegistrationName.remove(modelDisplayName); - modelDisplayNames.remove(modelDisplayName); - - // 从类别中移除 - for (List categoryModels : modelsByCategory.values()) { - categoryModels.remove(registrationName); - } - - return true; - } - return false; - } - - private static final class InstanceHolder { - private static final ModelManagement instance = new ModelManagement(); - } - - public static ModelManagement getInstance() { - return InstanceHolder.instance; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/SegmentationResult.java b/src/main/java/com/chuangzhou/vivid2D/ai/SegmentationResult.java deleted file mode 100644 index 3644106..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/SegmentationResult.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.chuangzhou.vivid2D.ai; - -import java.awt.image.BufferedImage; -import java.util.Map; - -public class SegmentationResult { - // 分割掩码图(每个像素的颜色为对应类别颜色) - private final BufferedImage maskImage; - - // 类别索引 -> 类别名称 - private final Map labels; - - // 类别名称 -> ARGB 颜色 - private final Map palette; - - public SegmentationResult(BufferedImage maskImage, Map labels, Map palette) { - this.maskImage = maskImage; - this.labels = labels; - this.palette = palette; - } - - public BufferedImage getMaskImage() { - return maskImage; - } - - public Map getLabels() { - return labels; - } - - public Map getPalette() { - return palette; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/Segmenter.java b/src/main/java/com/chuangzhou/vivid2D/ai/Segmenter.java deleted file mode 100644 index db5c1d7..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/Segmenter.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.chuangzhou.vivid2D.ai; - -import ai.djl.MalformedModelException; -import ai.djl.inference.Predictor; -import ai.djl.modality.cv.Image; -import ai.djl.modality.cv.ImageFactory; -import ai.djl.ndarray.NDArray; -import ai.djl.ndarray.NDList; -import ai.djl.ndarray.NDManager; -import ai.djl.ndarray.types.DataType; -import ai.djl.repository.zoo.Criteria; -import ai.djl.repository.zoo.ModelNotFoundException; -import ai.djl.repository.zoo.ZooModel; -import ai.djl.translate.Batchifier; -import ai.djl.translate.TranslateException; -import ai.djl.translate.Translator; -import ai.djl.translate.TranslatorContext; -import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetLabelPalette; -import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetSegmentationResult; -import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetSegmenter; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.util.*; - -public abstract class Segmenter implements AutoCloseable { - // 内部类,用于从Translator安全地传出数据 - public static class SegmentationData { - public final int[] indices; - public final long[] shape; - - public SegmentationData(int[] indices, long[] shape) { - this.indices = indices; - this.shape = shape; - } - } - - private String engine = "PyTorch"; - protected final ZooModel modelWrapper; - protected final Predictor predictor; - protected final List labels; - protected final Map palette; - - public Segmenter(Path modelDir, List labels) throws IOException, MalformedModelException, ModelNotFoundException { - this.labels = new ArrayList<>(labels); - this.palette = BiSeNetLabelPalette.defaultPalette(); - - Translator translator = new Translator() { - @Override - public NDList processInput(TranslatorContext ctx, Image input) { - return Segmenter.this.processInput(ctx, input); - } - - @Override - public Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list) { - return Segmenter.this.processOutput(ctx, list); - } - - @Override - public Batchifier getBatchifier() { - return Segmenter.this.getBatchifier(); - } - }; - - Criteria criteria = Criteria.builder() - .setTypes(Image.class, Segmenter.SegmentationData.class) - .optModelPath(modelDir) - .optEngine(engine) - .optTranslator(translator) - .build(); - - this.modelWrapper = criteria.loadModel(); - this.predictor = modelWrapper.newPredictor(); - } - - /** - * 处理模型输入 - * @param ctx translator 上下文 - * @param input 图片 - * @return 模型输入 - */ - public abstract NDList processInput(TranslatorContext ctx, Image input); - - /** - * 处理模型输出 - * @param ctx translator 上下文 - * @param list 模型输出 - * @return 模型输出 - */ - public abstract Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list); - - /** - * 获取批量处理方式 - * @return 批量处理方式 - */ - public Batchifier getBatchifier(){ - return null; - } - - public SegmentationResult segment(File imgFile) throws TranslateException, IOException { - Image img = ImageFactory.getInstance().fromFile(imgFile.toPath()); - - // predict 方法现在直接返回安全的 Java 对象 - Segmenter.SegmentationData data = predictor.predict(img); - - long[] shp = data.shape; - int[] indices = data.indices; - - int height, width; - if (shp.length == 2) { - height = (int) shp[0]; - width = (int) shp[1]; - } else { - throw new RuntimeException("Unexpected classMap shape from SegmentationData: " + Arrays.toString(shp)); - } - - // 后续处理完全基于 Java 对象,不再有 Native resource 问题 - BufferedImage mask = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Map labelsMap = new HashMap<>(); - for (int i = 0; i < labels.size(); i++) { - labelsMap.put(i, labels.get(i)); - } - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int idx = indices[y * width + x]; - String label = labelsMap.getOrDefault(idx, "unknown"); - int argb = palette.getOrDefault(label, 0xFF00FF00); - mask.setRGB(x, y, argb); - } - } - - return new SegmentationResult(mask, labelsMap, palette); - } - - public void setEngine(String engine) { - this.engine = engine; - } - - @Override - public void close() { - try { - predictor.close(); - } catch (Exception ignore) { - } - try { - modelWrapper.close(); - } catch (Exception ignore) { - } - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/VividModelWrapper.java b/src/main/java/com/chuangzhou/vivid2D/ai/VividModelWrapper.java deleted file mode 100644 index 140b540..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/VividModelWrapper.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.chuangzhou.vivid2D.ai; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.List; - -public abstract class VividModelWrapper implements AutoCloseable{ - protected final s segmenter; - protected final List labels; // index -> name - protected final Map palette; // name -> ARGB - - protected VividModelWrapper(s segmenter, List labels, Map palette) { - this.segmenter = segmenter; - this.labels = labels; - this.palette = palette; - } - - public List getLabels() { - return Collections.unmodifiableList(labels); - } - - public Map getPalette() { - return Collections.unmodifiableMap(palette); - } - - /** - * 直接返回分割结果(SegmentationResult) - */ - public SegmentationResult segment(File inputImage) throws Exception { - return segmenter.segment(inputImage); - } - - /** - * 把指定 targets(标签名集合)从输入图片中分割并保存到 outDir。 - * 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。 - *

- * 返回值:Map,ResultFiles 包含 maskFile、overlayFile(两个 PNG) - */ - public abstract Map segmentAndSave(File inputImage, Set targets, Path outDir) throws Exception; - - protected static String safeFileName(String s) { - return s.replaceAll("[^a-zA-Z0-9_\\-\\.]", "_"); - } - - protected static Set parseTargetsSet(Set in) { - if (in == null || in.isEmpty()) return Collections.emptySet(); - // 若包含单个 "all" - if (in.size() == 1) { - String only = in.iterator().next(); - if ("all".equalsIgnoreCase(only.trim())) { - return Set.of("all"); - } - } - // 直接返回 trim 后的小写不变集合(保持用户传入的名字) - Set out = new LinkedHashSet<>(); - for (String s : in) { - if (s != null) out.add(s.trim()); - } - return out; - } - - /** - * 关闭底层资源 - */ - @Override - public void close() { - try { - segmenter.close(); - } catch (Exception ignore) {} - } - - /* ================= helper: 从 modelDir 读取 synset.txt ================= */ - - protected static Optional> loadLabelsFromSynset(Path modelDir) { - Path syn = modelDir.resolve("synset.txt"); - if (Files.exists(syn)) { - try { - List lines = Files.readAllLines(syn); - List cleaned = new ArrayList<>(); - for (String l : lines) { - String s = l.trim(); - if (!s.isEmpty()) cleaned.add(s); - } - if (!cleaned.isEmpty()) return Optional.of(cleaned); - } catch (IOException ignore) {} - } - return Optional.empty(); - } - - /** - * 存放结果文件路径 - */ - public static class ResultFiles { - private final File maskFile; - private final File overlayFile; - - public ResultFiles(File maskFile, File overlayFile) { - this.maskFile = maskFile; - this.overlayFile = overlayFile; - } - - public File getMaskFile() { - return maskFile; - } - - public File getOverlayFile() { - return overlayFile; - } - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeLabelPalette.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeLabelPalette.java deleted file mode 100644 index efc3265..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeLabelPalette.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.chuangzhou.vivid2D.ai.anime_face_segmentation; - -import java.util.*; - -/** - * Anime-Face-Segmentation UNet 模型的标签和颜色调色板。 - * 基于 Anime-Face-Segmentation 项目的 util.py 中的颜色定义。 - * 标签索引必须与模型输出索引一致(0-6)。 - */ -public class AnimeLabelPalette { - - /** - * Anime-Face-Segmentation UNet 模型的标准标签(7个类别,索引 0-6) - */ - public static List defaultLabels() { - return Arrays.asList( - "background", // 0 - 青色 (0,255,255) - "hair", // 1 - 蓝色 (255,0,0) - "eye", // 2 - 红色 (0,0,255) - "mouth", // 3 - 白色 (255,255,255) - "face", // 4 - 绿色 (0,255,0) - "skin", // 5 - 黄色 (255,255,0) - "clothes" // 6 - 紫色 (255,0,255) - ); - } - - /** - * 返回对应的调色板:类别名 -> ARGB 颜色值。 - * 颜色值基于 util.py 中的 PALETTE 数组的 RGB 值转换为 ARGB 格式 (0xFFRRGGBB)。 - */ - public static Map defaultPalette() { - Map map = new HashMap<>(); - // 索引 0: background -> (0,255,255) 青色 - map.put("background", 0xFF00FFFF); - // 索引 1: hair -> (255,0,0) 蓝色 - map.put("hair", 0xFFFF0000); - // 索引 2: eye -> (0,0,255) 红色 - map.put("eye", 0xFF0000FF); - // 索引 3: mouth -> (255,255,255) 白色 - map.put("mouth", 0xFFFFFFFF); - // 索引 4: face -> (0,255,0) 绿色 - map.put("face", 0xFF00FF00); - // 索引 5: skin -> (255,255,0) 黄色 - map.put("skin", 0xFFFFFF00); - // 索引 6: clothes -> (255,0,255) 紫色 - map.put("clothes", 0xFFFF00FF); - - return map; - } - - /** - * 获取类别索引到名称的映射 - */ - public static Map getIndexToLabelMap() { - List labels = defaultLabels(); - Map map = new HashMap<>(); - for (int i = 0; i < labels.size(); i++) { - map.put(i, labels.get(i)); - } - return map; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeModelWrapper.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeModelWrapper.java deleted file mode 100644 index f8a4a3f..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeModelWrapper.java +++ /dev/null @@ -1,322 +0,0 @@ -package com.chuangzhou.vivid2D.ai.anime_face_segmentation; - -import com.chuangzhou.vivid2D.ai.VividModelWrapper; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Method; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.List; - -/** - * AnimeModelWrapper - 专门为 Anime-Face-Segmentation 模型封装的 Wrapper - */ -public class AnimeModelWrapper extends VividModelWrapper { - - private AnimeModelWrapper(AnimeSegmenter segmenter, List labels, Map palette) { - super(segmenter, labels, palette); - } - - /** - * 加载模型 - */ - public static AnimeModelWrapper load(Path modelDir) throws Exception { - List labels = loadLabelsFromSynset(modelDir).orElseGet(AnimeLabelPalette::defaultLabels); - AnimeSegmenter segmenter = new AnimeSegmenter(modelDir, labels); - Map palette = AnimeLabelPalette.defaultPalette(); - return new AnimeModelWrapper(segmenter, labels, palette); - } - - /** - * 直接返回分割结果(在丢给底层 segmenter 前会做通用预处理:RGB 转换 + 等比 letterbox 缩放到模型输入尺寸) - */ - public AnimeSegmentationResult segment(File inputImage) throws Exception { - File pre = null; - try { - pre = preprocessAndSave(inputImage); - // 将预处理后的临时文件丢给底层 segmenter - return segmenter.segment(pre); - } finally { - if (pre != null && pre.exists()) { - try { Files.deleteIfExists(pre.toPath()); } catch (Exception ignore) {} - } - } - } - - /** - * 分割并保存结果 - */ - public Map segmentAndSave(File inputImage, Set targets, Path outDir) throws Exception { - if (!Files.exists(outDir)) { - Files.createDirectories(outDir); - } - - AnimeSegmentationResult res = segment(inputImage); - BufferedImage original = ImageIO.read(inputImage); - BufferedImage maskImage = res.getMaskImage(); - - int maskW = maskImage.getWidth(); - int maskH = maskImage.getHeight(); - - // 解析 targets - Set realTargets = parseTargetsSet(targets); - Map saved = new LinkedHashMap<>(); - - for (String target : realTargets) { - if (!palette.containsKey(target)) { - // 尝试忽略大小写匹配 - String finalTarget = target; - Optional matched = palette.keySet().stream() - .filter(k -> k.equalsIgnoreCase(finalTarget)) - .findFirst(); - if (matched.isPresent()) target = matched.get(); - else { - System.err.println("Warning: unknown label '" + target + "' - skip."); - continue; - } - } - - int targetColor = palette.get(target); - - // 1) 生成透明背景的二值掩码(只保留 target 像素) - BufferedImage partMask = new BufferedImage(maskW, maskH, BufferedImage.TYPE_INT_ARGB); - for (int y = 0; y < maskH; y++) { - for (int x = 0; x < maskW; x++) { - int c = maskImage.getRGB(x, y); - if (c == targetColor) { - partMask.setRGB(x, y, targetColor | 0xFF000000); // 保证不透明 - } else { - partMask.setRGB(x, y, 0x00000000); - } - } - } - - // 2) 将 mask 缩放到与原图一致(如果需要),并生成 overlay(半透明) - BufferedImage maskResized = partMask; - if (original.getWidth() != maskW || original.getHeight() != maskH) { - maskResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g = maskResized.createGraphics(); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g.drawImage(partMask, 0, 0, original.getWidth(), original.getHeight(), null); - g.dispose(); - } - - BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = overlay.createGraphics(); - g2.drawImage(original, 0, 0, null); - // 半透明颜色(alpha = 0x88) - int rgbOnly = (targetColor & 0x00FFFFFF); - int translucent = (0x88 << 24) | rgbOnly; - BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB); - for (int y = 0; y < colorOverlay.getHeight(); y++) { - for (int x = 0; x < colorOverlay.getWidth(); x++) { - int mc = maskResized.getRGB(x, y); - if ((mc & 0x00FFFFFF) == (targetColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) { - colorOverlay.setRGB(x, y, translucent); - } else { - colorOverlay.setRGB(x, y, 0x00000000); - } - } - } - g2.drawImage(colorOverlay, 0, 0, null); - g2.dispose(); - - // 保存 - String safe = safeFileName(target); - File maskOut = outDir.resolve(safe + "_mask.png").toFile(); - File overlayOut = outDir.resolve(safe + "_overlay.png").toFile(); - - ImageIO.write(maskResized, "png", maskOut); - ImageIO.write(overlay, "png", overlayOut); - - saved.put(target, new ResultFiles(maskOut, overlayOut)); - } - - return saved; - } - - /** - * 专门提取眼睛的方法(在丢给底层 segmenter 前做预处理) - */ - public ResultFiles extractEyes(File inputImage, Path outDir) throws Exception { - if (!Files.exists(outDir)) { - Files.createDirectories(outDir); - } - - File pre = null; - BufferedImage eyes; - try { - pre = preprocessAndSave(inputImage); - eyes = segmenter.extractEyes(pre); - } finally { - if (pre != null && pre.exists()) { - try { Files.deleteIfExists(pre.toPath()); } catch (Exception ignore) {} - } - } - - File eyesMask = outDir.resolve("eyes_mask.png").toFile(); - ImageIO.write(eyes, "png", eyesMask); - - // 创建眼睛的 overlay(原有逻辑,保持不变) - BufferedImage original = ImageIO.read(inputImage); - BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = overlay.createGraphics(); - g2.drawImage(original, 0, 0, null); - - // 缩放眼睛掩码到原图尺寸 - BufferedImage eyesResized = eyes; - if (original.getWidth() != eyes.getWidth() || original.getHeight() != eyes.getHeight()) { - eyesResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g = eyesResized.createGraphics(); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g.drawImage(eyes, 0, 0, original.getWidth(), original.getHeight(), null); - g.dispose(); - } - - int eyeColor = palette.getOrDefault("eye", 0xFF00FF); // 若没有 eye,给个显眼默认色 - int rgbOnly = (eyeColor & 0x00FFFFFF); - int translucent = (0x88 << 24) | rgbOnly; - - BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB); - for (int y = 0; y < colorOverlay.getHeight(); y++) { - for (int x = 0; x < colorOverlay.getWidth(); x++) { - int mc = eyesResized.getRGB(x, y); - if ((mc & 0x00FFFFFF) == (eyeColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) { - colorOverlay.setRGB(x, y, translucent); - } else { - colorOverlay.setRGB(x, y, 0x00000000); - } - } - } - g2.drawImage(colorOverlay, 0, 0, null); - g2.dispose(); - - File eyesOverlay = outDir.resolve("eyes_overlay.png").toFile(); - ImageIO.write(overlay, "png", eyesOverlay); - - return new ResultFiles(eyesMask, eyesOverlay); - } - - /** - * 关闭底层资源 - */ - @Override - public void close() { - try { - segmenter.close(); - } catch (Exception ignore) {} - } - - // ========== 新增:预处理并保存到临时文件 ========== - private File preprocessAndSave(File inputImage) throws IOException { - BufferedImage img = ImageIO.read(inputImage); - if (img == null) throw new IOException("无法读取图片: " + inputImage); - - // 转成标准 RGB(去掉 alpha / 保证三通道) - BufferedImage rgb = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB); - Graphics2D g = rgb.createGraphics(); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g.drawImage(img, 0, 0, null); - g.dispose(); - - // 获取模型输入尺寸(尝试反射读取,找不到则使用默认 512x512) - int[] size = getModelInputSize(); - int targetW = size[0], targetH = size[1]; - - // 等比缩放并居中填充(letterbox),背景用白色 - double scale = Math.min((double) targetW / rgb.getWidth(), (double) targetH / rgb.getHeight()); - int newW = Math.max(1, (int) Math.round(rgb.getWidth() * scale)); - int newH = Math.max(1, (int) Math.round(rgb.getHeight() * scale)); - - BufferedImage resized = new BufferedImage(targetW, targetH, BufferedImage.TYPE_INT_RGB); - Graphics2D g2 = resized.createGraphics(); - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2.setColor(Color.WHITE); - g2.fillRect(0, 0, targetW, targetH); - int x = (targetW - newW) / 2; - int y = (targetH - newH) / 2; - g2.drawImage(rgb, x, y, newW, newH, null); - g2.dispose(); - - // 保存为临时 PNG 文件(确保无压缩失真) - File tmp = Files.createTempFile("anime_pre_", ".png").toFile(); - ImageIO.write(resized, "png", tmp); - return tmp; - } - - // ========== 新增:尝试通过反射从 segmenter 上读取模型输入尺寸 ========== - private int[] getModelInputSize() { - // 默认值 - int defaultSize = 512; - int w = defaultSize, h = defaultSize; - - try { - Class cls = segmenter.getClass(); - - // 尝试方法 getInputWidth/getInputHeight - try { - Method mw = cls.getMethod("getInputWidth"); - Method mh = cls.getMethod("getInputHeight"); - Object ow = mw.invoke(segmenter); - Object oh = mh.invoke(segmenter); - if (ow instanceof Number && oh instanceof Number) { - int iw = ((Number) ow).intValue(); - int ih = ((Number) oh).intValue(); - if (iw > 0 && ih > 0) { - return new int[]{iw, ih}; - } - } - } catch (NoSuchMethodException ignored) {} - - // 尝试方法 getInputSize 返回 int[] 或 Dimension - try { - Method ms = cls.getMethod("getInputSize"); - Object os = ms.invoke(segmenter); - if (os instanceof int[] && ((int[]) os).length >= 2) { - int iw = ((int[]) os)[0]; - int ih = ((int[]) os)[1]; - if (iw > 0 && ih > 0) return new int[]{iw, ih}; - } else if (os != null) { - // 处理 java.awt.Dimension - try { - Method gw = os.getClass().getMethod("getWidth"); - Method gh = os.getClass().getMethod("getHeight"); - Object ow2 = gw.invoke(os); - Object oh2 = gh.invoke(os); - if (ow2 instanceof Number && oh2 instanceof Number) { - int iw = ((Number) ow2).intValue(); - int ih = ((Number) oh2).intValue(); - if (iw > 0 && ih > 0) return new int[]{iw, ih}; - } - } catch (Exception ignored2) {} - } - } catch (NoSuchMethodException ignored) {} - - // 尝试字段 inputWidth/inputHeight - try { - try { - java.lang.reflect.Field fw = cls.getDeclaredField("inputWidth"); - java.lang.reflect.Field fh = cls.getDeclaredField("inputHeight"); - fw.setAccessible(true); fh.setAccessible(true); - Object ow = fw.get(segmenter); - Object oh = fh.get(segmenter); - if (ow instanceof Number && oh instanceof Number) { - int iw = ((Number) ow).intValue(); - int ih = ((Number) oh).intValue(); - if (iw > 0 && ih > 0) return new int[]{iw, ih}; - } - } catch (NoSuchFieldException ignoredField) {} - } catch (Exception ignored) {} - - } catch (Exception ignored) { - // 任何反射异常都回退到默认值 - } - - return new int[]{w, h}; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeSegmentationResult.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeSegmentationResult.java deleted file mode 100644 index bb18069..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeSegmentationResult.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.chuangzhou.vivid2D.ai.anime_face_segmentation; - -import com.chuangzhou.vivid2D.ai.SegmentationResult; - -import java.awt.image.BufferedImage; -import java.util.Map; - -/** - * 动漫分割结果容器 - */ -public class AnimeSegmentationResult extends SegmentationResult { - // 分割掩码图(每个像素的颜色为对应类别颜色) - private final BufferedImage maskImage; - - // 分割概率图(每个像素的类别概率分布) - private final float[][][] probabilityMap; - - // 类别索引 -> 类别名称 - private final Map labels; - - // 类别名称 -> ARGB 颜色 - private final Map palette; - - public AnimeSegmentationResult(BufferedImage maskImage, float[][][] probabilityMap, - Map labels, Map palette) { - super(maskImage, labels, palette); - this.maskImage = maskImage; - this.probabilityMap = probabilityMap; - this.labels = labels; - this.palette = palette; - } - - public BufferedImage getMaskImage() { - return maskImage; - } - - public float[][][] getProbabilityMap() { - return probabilityMap; - } - - public Map getLabels() { - return labels; - } - - public Map getPalette() { - return palette; - } - - /** - * 获取指定类别的概率图 - */ - public float[][] getClassProbability(int classIndex) { - if (probabilityMap == null) return null; - int height = probabilityMap.length; - int width = probabilityMap[0].length; - float[][] result = new float[height][width]; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - result[y][x] = probabilityMap[y][x][classIndex]; - } - } - return result; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeSegmenter.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeSegmenter.java deleted file mode 100644 index ed773a2..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_face_segmentation/AnimeSegmenter.java +++ /dev/null @@ -1,214 +0,0 @@ -package com.chuangzhou.vivid2D.ai.anime_face_segmentation; - -import ai.djl.MalformedModelException; -import ai.djl.inference.Predictor; -import ai.djl.modality.cv.Image; -import ai.djl.modality.cv.ImageFactory; -import ai.djl.ndarray.NDArray; -import ai.djl.ndarray.NDList; -import ai.djl.ndarray.NDManager; -import ai.djl.ndarray.types.DataType; -import ai.djl.ndarray.types.Shape; -import ai.djl.repository.zoo.Criteria; -import ai.djl.repository.zoo.ModelNotFoundException; -import ai.djl.repository.zoo.ZooModel; -import ai.djl.translate.Batchifier; -import ai.djl.translate.TranslateException; -import ai.djl.translate.Translator; -import ai.djl.translate.TranslatorContext; -import com.chuangzhou.vivid2D.ai.Segmenter; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.util.*; - -/** - * AnimeSegmenter: 专门为 Anime-Face-Segmentation UNet 模型设计的分割器 - */ -public class AnimeSegmenter extends Segmenter { - - // 模型默认输入大小(与训练时一致)。若模型不同可以修改为实际值或让 caller 通过构造参数传入。 - private static final int MODEL_INPUT_W = 512; - private static final int MODEL_INPUT_H = 512; - - // 内部类,用于从Translator安全地传出数据 - public static class SegmentationData { - final int[] indices; // 类别索引 [H * W] - final float[][][] probMap; // 概率图 [H][W][C] - final long[] shape; // 形状 [H, W] - - public SegmentationData(int[] indices, float[][][] probMap, long[] shape) { - this.indices = indices; - this.probMap = probMap; - this.shape = shape; - } - } - - private final ZooModel modelWrapper; - private final Predictor predictor; - private final Map palette; - - public AnimeSegmenter(Path modelDir, List labels) throws IOException, MalformedModelException, ModelNotFoundException { - super(modelDir, labels); - this.palette = AnimeLabelPalette.defaultPalette(); - - Translator translator = new Translator() { - @Override - public NDList processInput(TranslatorContext ctx, Image input) { - NDManager manager = ctx.getNDManager(); - - // 如果图片已经是模型输入大小则不再 resize(避免重复缩放导致失真) - Image toUse = input; - if (!(input.getWidth() == MODEL_INPUT_W && input.getHeight() == MODEL_INPUT_H)) { - toUse = input.resize(MODEL_INPUT_W, MODEL_INPUT_H, true); - } - - // 转换为NDArray并预处理 - NDArray array = toUse.toNDArray(manager); - // DJL 返回 HWC 格式数组,转换为 CHW,并标准化到 [0,1] - array = array.transpose(2, 0, 1) // HWC -> CHW - .toType(DataType.FLOAT32, false) - .div(255f) // 归一化到[0,1] - .expandDims(0); // 添加batch维度 [1,3,H,W] - - return new NDList(array); - } - - @Override - public SegmentationData processOutput(TranslatorContext ctx, NDList list) { - if (list == null || list.isEmpty()) { - throw new IllegalStateException("Model did not return any output."); - } - - NDArray output = list.get(0); // 期望形状 [1,C,H,W] 或 [1,C,W,H](以训练时一致为准) - - // 确保维度:把 output 视作 [1, C, H, W] - Shape outShape = output.getShape(); - if (outShape.dimension() != 4) { - throw new IllegalStateException("Unexpected output shape: " + outShape); - } - - // 1. 获取类别索引(argmax) -> [H, W] - NDArray squeezed = output.squeeze(0); // [C,H,W] - NDArray classMap = squeezed.argMax(0).toType(DataType.INT32, false); // argMax over channel维度 - - // 2. 获取概率图(softmax 输出或模型已经输出概率),转换为 [H,W,C] - NDArray probabilities = squeezed.transpose(1, 2, 0) // [H,W,C] - .toType(DataType.FLOAT32, false); - - // 3. 转换为Java数组 - long[] shape = classMap.getShape().getShape(); // [H, W] - int[] indices = classMap.toIntArray(); - long[] probShape = probabilities.getShape().getShape(); // [H, W, C] - int height = (int) probShape[0]; - int width = (int) probShape[1]; - int classes = (int) probShape[2]; - float[] flatProbMap = probabilities.toFloatArray(); - float[][][] probMap = new float[height][width][classes]; - for (int i = 0; i < height; i++) { - for (int j = 0; j < width; j++) { - for (int k = 0; k < classes; k++) { - int index = i * width * classes + j * classes + k; - probMap[i][j][k] = flatProbMap[index]; - } - } - } - - return new SegmentationData(indices, probMap, shape); - } - - @Override - public Batchifier getBatchifier() { - return null; - } - }; - - Criteria criteria = Criteria.builder() - .setTypes(Image.class, SegmentationData.class) - .optModelPath(modelDir) - .optEngine("PyTorch") - .optTranslator(translator) - .build(); - - this.modelWrapper = criteria.loadModel(); - this.predictor = modelWrapper.newPredictor(); - } - - @Override - public NDList processInput(TranslatorContext ctx, Image input) { - return null; - } - - @Override - public Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list) { - return null; - } - - public AnimeSegmentationResult segment(File imgFile) throws TranslateException, IOException { - Image img = ImageFactory.getInstance().fromFile(imgFile.toPath()); - - // 预测并获取分割数据 - SegmentationData data = predictor.predict(img); - - long[] shp = data.shape; - int[] indices = data.indices; - float[][][] probMap = data.probMap; - - int height = (int) shp[0]; - int width = (int) shp[1]; - - // 创建掩码图像 - BufferedImage mask = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Map labelsMap = AnimeLabelPalette.getIndexToLabelMap(); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int idx = indices[y * width + x]; - String label = labelsMap.getOrDefault(idx, "unknown"); - int argb = palette.getOrDefault(label, 0xFF00FF00); // 默认绿色 - mask.setRGB(x, y, argb); - } - } - - return new AnimeSegmentationResult(mask, probMap, labelsMap, palette); - } - - /** - * 专门针对眼睛的分割方法 - */ - public BufferedImage extractEyes(File imgFile) throws TranslateException, IOException { - AnimeSegmentationResult result = segment(imgFile); - BufferedImage mask = result.getMaskImage(); - BufferedImage eyeMask = new BufferedImage(mask.getWidth(), mask.getHeight(), BufferedImage.TYPE_INT_ARGB); - - int eyeColor = palette.get("eye"); - - for (int y = 0; y < mask.getHeight(); y++) { - for (int x = 0; x < mask.getWidth(); x++) { - int rgb = mask.getRGB(x, y); - if (rgb == eyeColor) { - eyeMask.setRGB(x, y, eyeColor); - } else { - eyeMask.setRGB(x, y, 0x00000000); // 透明 - } - } - } - - return eyeMask; - } - - @Override - public void close() { - try { - predictor.close(); - } catch (Exception ignore) { - } - try { - modelWrapper.close(); - } catch (Exception ignore) { - } - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2LabelPalette.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2LabelPalette.java deleted file mode 100644 index da9ed07..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2LabelPalette.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.chuangzhou.vivid2D.ai.anime_segmentation; - -import java.util.*; - -/** - * 动漫分割模型的标签和颜色调色板。 - * 这是一个二分类模型:背景和前景(动漫人物) - */ -public class Anime2LabelPalette { - - /** - * 动漫分割模型的标准标签(2个类别) - */ - public static List defaultLabels() { - return Arrays.asList( - "background", // 0 - "foreground" // 1 - ); - } - - /** - * 返回动漫分割模型的调色板 - */ - public static Map defaultPalette() { - Map map = new HashMap<>(); - // 索引 0: background - 黑色 - map.put("background", 0xFF000000); - // 索引 1: foreground - 白色 - map.put("foreground", 0xFFFFFFFF); - - return map; - } - - /** - * 专门为动漫分割模型设计的调色板(可视化更友好) - */ - public static Map animeSegmentationPalette() { - Map map = new HashMap<>(); - // 背景 - 透明 - map.put("background", 0x00000000); - // 前景 - 红色(用于可视化) - map.put("foreground", 0xFFFF0000); - - return map; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2SegmentationResult.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2SegmentationResult.java deleted file mode 100644 index 6b36eba..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2SegmentationResult.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.chuangzhou.vivid2D.ai.anime_segmentation; - -import com.chuangzhou.vivid2D.ai.SegmentationResult; - -import java.awt.image.BufferedImage; -import java.util.Map; - -/** - * 动漫分割结果容器 - */ -public class Anime2SegmentationResult extends SegmentationResult { - public Anime2SegmentationResult(BufferedImage maskImage, Map labels, Map palette) { - super(maskImage, labels, palette); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2Segmenter.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2Segmenter.java deleted file mode 100644 index 10a969b..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2Segmenter.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.chuangzhou.vivid2D.ai.anime_segmentation; - -import ai.djl.MalformedModelException; -import ai.djl.modality.cv.Image; -import ai.djl.modality.cv.ImageFactory; -import ai.djl.ndarray.NDArray; -import ai.djl.ndarray.NDList; -import ai.djl.ndarray.NDManager; -import ai.djl.ndarray.types.DataType; -import ai.djl.repository.zoo.ModelNotFoundException; -import ai.djl.translate.TranslateException; -import ai.djl.translate.TranslatorContext; -import com.chuangzhou.vivid2D.ai.SegmentationResult; -import com.chuangzhou.vivid2D.ai.Segmenter; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.util.*; - -/** - * Anime2Segmenter: 专门用于动漫分割模型 - * 处理 anime-segmentation 模型的二值分割输出 - */ -public class Anime2Segmenter extends Segmenter { - public Anime2Segmenter(Path modelDir, List labels) throws IOException, MalformedModelException, ModelNotFoundException { - super(modelDir, labels); - } - - @Override - public NDList processInput(TranslatorContext ctx, Image input) { - NDManager manager = ctx.getNDManager(); - - // 调整输入图像尺寸到模型期望的大小 (1024x1024) - Image resized = input.resize(1024, 1024, true); - NDArray array = resized.toNDArray(manager); - - // 转换为 CHW 格式并归一化 - array = array.transpose(2, 0, 1).toType(DataType.FLOAT32, false); - array = array.div(255f); - array = array.expandDims(0); // 添加batch维度 - - return new NDList(array); - } - - @Override - public SegmentationData processOutput(TranslatorContext ctx, NDList list) { - if (list == null || list.isEmpty()) { - throw new IllegalStateException("Model did not return any output."); - } - NDArray out = list.get(0); - // 动漫分割模型输出形状: [1, 1, H, W] - 单通道概率图 - // 应用sigmoid并二值化 - NDArray probabilities = out.div(out.neg().exp().add(1)); - NDArray binaryMask = probabilities.gt(0.5).toType(DataType.INT32, false); - if (binaryMask.getShape().dimension() == 4) { - binaryMask = binaryMask.squeeze(0).squeeze(0); - } - long[] finalShape = binaryMask.getShape().getShape(); - int[] indices = binaryMask.toIntArray(); - return new SegmentationData(indices, finalShape); - } - - @Override - public SegmentationResult segment(File imgFile) throws TranslateException, IOException { - Image img = ImageFactory.getInstance().fromFile(imgFile.toPath()); - Segmenter.SegmentationData data = predictor.predict(img); - long[] shp = data.shape; - int[] indices = data.indices; - int height, width; - if (shp.length == 2) { - height = (int) shp[0]; - width = (int) shp[1]; - } else { - throw new RuntimeException("Unexpected classMap shape from SegmentationData: " + Arrays.toString(shp)); - } - BufferedImage mask = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Map labelsMap = new HashMap<>(); - for (int i = 0; i < labels.size(); i++) { - labelsMap.put(i, labels.get(i)); - } - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int idx = indices[y * width + x]; - String label = labelsMap.getOrDefault(idx, "unknown"); - int argb = palette.getOrDefault(label, 0xFFFF0000); - mask.setRGB(x, y, argb); - } - } - return new SegmentationResult(mask, labelsMap, palette); - } - - @Override - public void close() { - try { - predictor.close(); - } catch (Exception ignore) { - } - try { - modelWrapper.close(); - } catch (Exception ignore) { - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2VividModelWrapper.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2VividModelWrapper.java deleted file mode 100644 index b76a9be..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2VividModelWrapper.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.chuangzhou.vivid2D.ai.anime_segmentation; - -import com.chuangzhou.vivid2D.ai.SegmentationResult; -import com.chuangzhou.vivid2D.ai.VividModelWrapper; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.List; - -/** - * Anime2VividModelWrapper - 对之前 Anime2Segmenter 的封装,提供更便捷的API - *

- * 用法示例: - * Anime2VividModelWrapper wrapper = Anime2VividModelWrapper.load(Paths.get("/path/to/modelDir")); - * Map out = wrapper.segmentAndSave( - * new File("input.jpg"), - * Set.of("foreground"), // 动漫分割主要关注前景 - * Paths.get("outDir") - * ); - * // out contains 每个目标标签对应的 mask+overlay 文件路径 - * wrapper.close(); - */ -public class Anime2VividModelWrapper extends VividModelWrapper { - - private Anime2VividModelWrapper(Anime2Segmenter segmenter, List labels, Map palette) { - super(segmenter, labels, palette); - - } - - /** - * 读取 modelDir/synset.txt(每行一个标签),若不存在则使用 Anime2LabelPalette.defaultLabels() - * 并创建 Anime2Segmenter 实例。 - */ - public static Anime2VividModelWrapper load(Path modelDir) throws Exception { - List labels = loadLabelsFromSynset(modelDir).orElseGet(Anime2LabelPalette::defaultLabels); - Anime2Segmenter s = new Anime2Segmenter(modelDir, labels); - Map palette = Anime2LabelPalette.animeSegmentationPalette(); - return new Anime2VividModelWrapper(s, labels, palette); - } - - public List getLabels() { - return Collections.unmodifiableList(labels); - } - - public Map getPalette() { - return Collections.unmodifiableMap(palette); - } - - /** - * 直接返回分割结果(Anime2SegmentationResult) - */ - public SegmentationResult segment(File inputImage) throws Exception { - return segmenter.segment(inputImage); - } - - /** - * 把指定 targets(标签名集合)从输入图片中分割并保存到 outDir。 - * 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。 - *

- * 返回值:Map,ResultFiles 包含 maskFile、overlayFile(两个 PNG) - */ - public Map segmentAndSave(File inputImage, Set targets, Path outDir) throws Exception { - if (!Files.exists(outDir)) { - Files.createDirectories(outDir); - } - - SegmentationResult res = segment(inputImage); - BufferedImage original = ImageIO.read(inputImage); - BufferedImage maskImage = res.getMaskImage(); - - int maskW = maskImage.getWidth(); - int maskH = maskImage.getHeight(); - - // 解析 targets - Set realTargets = parseTargetsSet(targets); - Map saved = new LinkedHashMap<>(); - - for (String target : realTargets) { - if (!palette.containsKey(target)) { - String finalTarget = target; - Optional matched = palette.keySet().stream() - .filter(k -> k.equalsIgnoreCase(finalTarget)) - .findFirst(); - if (matched.isPresent()) target = matched.get(); - else { - System.err.println("Warning: unknown label '" + target + "' - skip."); - continue; - } - } - - int targetColor = palette.get(target); - - // 1) 生成透明背景的二值掩码(只保留 target 像素) - BufferedImage partMask = new BufferedImage(maskW, maskH, BufferedImage.TYPE_INT_ARGB); - for (int y = 0; y < maskH; y++) { - for (int x = 0; x < maskW; x++) { - int c = maskImage.getRGB(x, y); - if (c == targetColor) { - partMask.setRGB(x, y, targetColor | 0xFF000000); // 保证不透明 - } else { - partMask.setRGB(x, y, 0x00000000); - } - } - } - - // 2) 将 mask 缩放到与原图一致(如果需要),并生成 overlay(半透明) - BufferedImage maskResized = partMask; - if (original.getWidth() != maskW || original.getHeight() != maskH) { - maskResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g = maskResized.createGraphics(); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g.drawImage(partMask, 0, 0, original.getWidth(), original.getHeight(), null); - g.dispose(); - } - - BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = overlay.createGraphics(); - g2.drawImage(original, 0, 0, null); - // 半透明颜色(alpha = 0x88) - int rgbOnly = (targetColor & 0x00FFFFFF); - int translucent = (0x88 << 24) | rgbOnly; - BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB); - for (int y = 0; y < colorOverlay.getHeight(); y++) { - for (int x = 0; x < colorOverlay.getWidth(); x++) { - int mc = maskResized.getRGB(x, y); - if ((mc & 0x00FFFFFF) == (targetColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) { - colorOverlay.setRGB(x, y, translucent); - } else { - colorOverlay.setRGB(x, y, 0x00000000); - } - } - } - g2.drawImage(colorOverlay, 0, 0, null); - g2.dispose(); - - // 保存 - String safe = safeFileName(target); - File maskOut = outDir.resolve(safe + "_mask.png").toFile(); - File overlayOut = outDir.resolve(safe + "_overlay.png").toFile(); - - ImageIO.write(maskResized, "png", maskOut); - ImageIO.write(overlay, "png", overlayOut); - - saved.put(target, new ResultFiles(maskOut, overlayOut)); - } - - return saved; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetLabelPalette.java b/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetLabelPalette.java deleted file mode 100644 index c4c25f5..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetLabelPalette.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.chuangzhou.vivid2D.ai.face_parsing; - -import java.util.*; - -/** - * BiSeNet 人脸解析模型的标准标签和颜色调色板。 - * 颜色值基于 zllrunning/face-parsing.PyTorch 仓库的 test.py 文件。 - * 标签索引必须与模型输出索引一致(0-18)。 - */ -public class BiSeNetLabelPalette { - - /** - * BiSeNet 人脸解析模型的标准标签(19个类别,索引 0-18) - */ - public static List defaultLabels() { - return Arrays.asList( - "background", // 0 - "skin", // 1 - "nose", // 2 - "eye_left", // 3 - "eye_right", // 4 - "eyebrow_left", // 5 - "eyebrow_right",// 6 - "ear_left", // 7 - "ear_right", // 8 - "mouth", // 9 - "lip_upper", // 10 - "lip_lower", // 11 - "hair", // 12 - "hat", // 13 - "earring", // 14 - "necklace", // 15 - "clothes", // 16 - "facial_hair",// 17 - "neck" // 18 - ); - } - - /** - * 返回一个对应的调色板:类别名 -> ARGB 颜色值。 - * 颜色值基于 test.py 中 part_colors 数组的 RGB 值转换为 ARGB 格式 (0xFFRRGGBB)。 - */ - public static Map defaultPalette() { - Map map = new HashMap<>(); - // 索引 0: background - map.put("background", 0xFF000000); // 黑色 - - // 索引 1-18: 对应 part_colors 数组的前 18 个颜色 - // 注意:这里假设 part_colors[i-1] 对应 索引 i 的标签。 - // 索引 1: skin -> [255, 0, 0] - map.put("skin", 0xFFFF0000); - // 索引 2: nose -> [255, 85, 0] - map.put("nose", 0xFFFF5500); - // 索引 3: eye_left -> [255, 170, 0] - map.put("eye_left", 0xFFFFAA00); - // 索引 4: eye_right -> [255, 0, 85] - map.put("eye_right", 0xFFFF0055); - // 索引 5: eyebrow_left -> [255, 0, 170] - map.put("eyebrow_left",0xFFFF00AA); - // 索引 6: eyebrow_right -> [0, 255, 0] - map.put("eyebrow_right",0xFF00FF00); - // 索引 7: ear_left -> [85, 255, 0] - map.put("ear_left", 0xFF55FF00); - // 索引 8: ear_right -> [170, 255, 0] - map.put("ear_right", 0xFFAAFF00); - // 索引 9: mouth -> [0, 255, 85] - map.put("mouth", 0xFF00FF55); - // 索引 10: lip_upper -> [0, 255, 170] - map.put("lip_upper", 0xFF00FFAA); - // 索引 11: lip_lower -> [0, 0, 255] - map.put("lip_lower", 0xFF0000FF); - // 索引 12: hair -> [85, 0, 255] - map.put("hair", 0xFF5500FF); - // 索引 13: hat -> [170, 0, 255] - map.put("hat", 0xFFAA00FF); - // 索引 14: earring -> [0, 85, 255] - map.put("earring", 0xFF0055FF); - // 索引 15: necklace -> [0, 170, 255] - map.put("necklace", 0xFF00AAFF); - // 索引 16: clothes -> [255, 255, 0] - map.put("clothes", 0xFFFFFF00); - // 索引 17: facial_hair -> [255, 85, 85] - map.put("facial_hair", 0xFFFF5555); - // 索引 18: neck -> [255, 170, 170] - map.put("neck", 0xFFFFAAAA); - - return map; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetSegmentationResult.java b/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetSegmentationResult.java deleted file mode 100644 index 5e634ac..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetSegmentationResult.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.chuangzhou.vivid2D.ai.face_parsing; - -import com.chuangzhou.vivid2D.ai.SegmentationResult; - -import java.awt.image.BufferedImage; -import java.util.Map; - -/** - * 分割结果容器 - */ -public class BiSeNetSegmentationResult extends SegmentationResult { - public BiSeNetSegmentationResult(BufferedImage maskImage, Map labels, Map palette) { - super(maskImage, labels, palette); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetSegmenter.java b/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetSegmenter.java deleted file mode 100644 index 30c71e3..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetSegmenter.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.chuangzhou.vivid2D.ai.face_parsing; - -import ai.djl.MalformedModelException; -import ai.djl.inference.Predictor; -import ai.djl.modality.cv.Image; -import ai.djl.modality.cv.ImageFactory; -import ai.djl.ndarray.NDArray; -import ai.djl.ndarray.NDList; -import ai.djl.ndarray.NDManager; -import ai.djl.ndarray.types.DataType; -import ai.djl.repository.zoo.Criteria; -import ai.djl.repository.zoo.ModelNotFoundException; -import ai.djl.repository.zoo.ZooModel; -import ai.djl.translate.Batchifier; -import ai.djl.translate.TranslateException; -import ai.djl.translate.Translator; -import ai.djl.translate.TranslatorContext; -import com.chuangzhou.vivid2D.ai.SegmentationResult; -import com.chuangzhou.vivid2D.ai.Segmenter; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.util.*; - -/** - * Segmenter: 加载模型并对图片做语义分割 - * - * 说明: - * - Translator.processOutput 在翻译器层就把模型输出处理成 (H, W) 的类别索引 NDArray, - * 并把该 NDArray 拷贝到 persistentManager 中返回,从而避免后续 native 资源被释放的问题。 - * - 这里改为在 Translator 内部把 classMap 转为 Java int[](通过 classMap.toIntArray()), - * 再用 persistentManager.create(int[], shape) 创建新的 NDArray 返回,确保安全。 - */ -public class BiSeNetSegmenter extends Segmenter { - - public BiSeNetSegmenter(Path modelDir, List labels) throws IOException, MalformedModelException, ModelNotFoundException { - super(modelDir, labels); - } - - @Override - public NDList processInput(TranslatorContext ctx, Image input) { - NDManager manager = ctx.getNDManager(); - NDArray array = input.toNDArray(manager); - array = array.transpose(2, 0, 1).toType(DataType.FLOAT32, false); - array = array.div(255f); - array = array.expandDims(0); - return new NDList(array); - } - - @Override - public Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list) { - if (list == null || list.isEmpty()) { - throw new IllegalStateException("Model did not return any output."); - } - - NDArray out = list.get(0); - NDArray classMap; - - // 1. 解析模型输出,得到类别图谱 (classMap) - long[] shape = out.getShape().getShape(); - if (shape.length == 4 && shape[1] > 1) { - classMap = out.argMax(1); - } else if (shape.length == 3) { - classMap = (shape[0] == 1) ? out : out.argMax(0); - } else if (shape.length == 2) { - classMap = out; - } else { - throw new IllegalStateException("Unexpected output shape: " + Arrays.toString(shape)); - } - - if (classMap.getShape().dimension() == 3) { - classMap = classMap.squeeze(0); - } - - // 2. *** 关键步骤 *** - // 在 NDArray 仍然有效的上下文中,将其转换为 Java 原生类型 - - // 首先,确保数据类型是 INT32 - NDArray int32ClassMap = classMap.toType(DataType.INT32, false); - - // 然后,获取形状和 int[] 数组 - long[] finalShape = int32ClassMap.getShape().getShape(); - int[] indices = int32ClassMap.toIntArray(); - - // 3. 将 Java 对象封装并返回 - return new SegmentationData(indices, finalShape); - } - - @Override - public SegmentationResult segment(File imgFile) throws TranslateException, IOException { - return super.segment(imgFile); - } - - @Override - public void close() { - super.close(); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetVividModelWrapper.java b/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetVividModelWrapper.java deleted file mode 100644 index 7f2e393..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/ai/face_parsing/BiSeNetVividModelWrapper.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.chuangzhou.vivid2D.ai.face_parsing; - -import com.chuangzhou.vivid2D.ai.SegmentationResult; -import com.chuangzhou.vivid2D.ai.VividModelWrapper; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.List; - -/** - * VividModelWrapper - 对之前 Segmenter / SegmenterExample 的封装 - * - * 用法示例: - * VividModelWrapper wrapper = VividModelWrapper.load(Paths.get("/path/to/modelDir")); - * Map out = wrapper.segmentAndSave( - * new File("input.jpg"), - * Set.of("eye","face"), // 或 Set.of(all labels...);若想全部传 "all" 可以用 helper parseTargets - * Paths.get("outDir") - * ); - * // out contains 每个目标标签对应的 mask+overlay 文件路径 - * wrapper.close(); - */ -public class BiSeNetVividModelWrapper extends VividModelWrapper { - - private BiSeNetVividModelWrapper(BiSeNetSegmenter segmenter, List labels, Map palette) { - super(segmenter, labels, palette); - } - - /** - * 读取 modelDir/synset.txt(每行一个标签),若不存在则使用 LabelPalette.defaultLabels() - * 并创建 Segmenter 实例。 - */ - public static BiSeNetVividModelWrapper load(Path modelDir) throws Exception { - List labels = loadLabelsFromSynset(modelDir).orElseGet(BiSeNetLabelPalette::defaultLabels); - BiSeNetSegmenter s = new BiSeNetSegmenter(modelDir, labels); - Map palette = BiSeNetLabelPalette.defaultPalette(); - return new BiSeNetVividModelWrapper(s, labels, palette); - } - - public List getLabels() { - return super.getLabels(); - } - - public Map getPalette() { - return super.getPalette(); - } - - /** - * 直接返回分割结果(SegmentationResult) - */ - public SegmentationResult segment(File inputImage) throws Exception { - return segmenter.segment(inputImage); - } - - /** - * 把指定 targets(标签名集合)从输入图片中分割并保存到 outDir。 - * 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。 - *

- * 返回值:Map,ResultFiles 包含 maskFile、overlayFile(两个 PNG) - */ - public Map segmentAndSave(File inputImage, Set targets, Path outDir) throws Exception { - if (!Files.exists(outDir)) { - Files.createDirectories(outDir); - } - SegmentationResult res = segment(inputImage); - BufferedImage original = ImageIO.read(inputImage); - BufferedImage maskImage = res.getMaskImage(); - int maskW = maskImage.getWidth(); - int maskH = maskImage.getHeight(); - Set realTargets = parseTargetsSet(targets); - Map saved = new LinkedHashMap<>(); - for (String target : realTargets) { - if (!palette.containsKey(target)) { - String finalTarget = target; - Optional matched = palette.keySet().stream() - .filter(k -> k.equalsIgnoreCase(finalTarget)) - .findFirst(); - if (matched.isPresent()) target = matched.get(); - else { - System.err.println("Warning: unknown label '" + target + "' - skip."); - continue; - } - } - int targetColor = palette.get(target); - BufferedImage partMask = new BufferedImage(maskW, maskH, BufferedImage.TYPE_INT_ARGB); - for (int y = 0; y < maskH; y++) { - for (int x = 0; x < maskW; x++) { - int c = maskImage.getRGB(x, y); - if (c == targetColor) { - partMask.setRGB(x, y, targetColor | 0xFF000000); // 保证不透明 - } else { - partMask.setRGB(x, y, 0x00000000); - } - } - } - BufferedImage maskResized = partMask; - if (original.getWidth() != maskW || original.getHeight() != maskH) { - maskResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g = maskResized.createGraphics(); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g.drawImage(partMask, 0, 0, original.getWidth(), original.getHeight(), null); - g.dispose(); - } - BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = overlay.createGraphics(); - g2.drawImage(original, 0, 0, null); - int rgbOnly = (targetColor & 0x00FFFFFF); - int translucent = (0x88 << 24) | rgbOnly; - BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB); - for (int y = 0; y < colorOverlay.getHeight(); y++) { - for (int x = 0; x < colorOverlay.getWidth(); x++) { - int mc = maskResized.getRGB(x, y); - if ((mc & 0x00FFFFFF) == (targetColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) { - colorOverlay.setRGB(x, y, translucent); - } else { - colorOverlay.setRGB(x, y, 0x00000000); - } - } - } - g2.drawImage(colorOverlay, 0, 0, null); - g2.dispose(); - String safe = safeFileName(target); - File maskOut = outDir.resolve(safe + "_mask.png").toFile(); - File overlayOut = outDir.resolve(safe + "_overlay.png").toFile(); - ImageIO.write(maskResized, "png", maskOut); - ImageIO.write(overlay, "png", overlayOut); - saved.put(target, new ResultFiles(maskOut, overlayOut)); - } - - return saved; - } - - /** - * 关闭底层资源 - */ - @Override - public void close() { - try { - segmenter.close(); - } catch (Exception ignore) {} - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/block/BlocklyMessageHandler.java b/src/main/java/com/chuangzhou/vivid2D/block/BlocklyMessageHandler.java deleted file mode 100644 index c9aa788..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/block/BlocklyMessageHandler.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.chuangzhou.vivid2D.block; - -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - -/** - * 一个专门用于处理来自Blockly前端的JSON消息的处理器。 - * 它负责解析请求,并将其分派给Vivid2DRendererBridge中的相应方法。 - */ -public class BlocklyMessageHandler { - - private final Vivid2DRendererBridge rendererBridge = new Vivid2DRendererBridge(); - private final Gson gson = new Gson(); - - /** - * 用于Gson解析JSON请求的内部数据结构类。 - * 它必须与JavaScript中构建的request对象结构匹配。 - * { "action": "...", "params": { ... } } - */ - private static class RequestData { - String action; - java.util.Map params; - } - - /** - * 尝试处理传入的请求字符串。 - * - * @param request 从JavaScript的onQuery回调中接收到的原始字符串。 - * @return 如果请求被成功识别并处理,则返回 true;否则返回 false。 - */ - public boolean handle(String request) { - try { - // 1. 尝试将请求字符串解析为我们预定义的RequestData结构 - RequestData data = gson.fromJson(request, RequestData.class); - - // 2. 验证解析结果。如果不是有效的JSON或缺少action字段,则这不是我们能处理的请求。 - if (data == null || data.action == null) { - return false; - } - - // 3. 使用 switch 语句根据 action 的值将请求分派到不同的处理方法 - switch (data.action) { - case "moveObject": - // 从参数Map中提取所需数据 - String objectIdMove = data.params.get("objectId").toString(); - // JSON数字默认被Gson解析为Double,需要转换 - int x = ((Double) data.params.get("x")).intValue(); - int y = ((Double) data.params.get("y")).intValue(); - - // 调用实际的业务逻辑 - rendererBridge.moveObject(objectIdMove, x, y); - - // 表示我们已经成功处理了这个请求 - return true; - - case "changeColor": - // 从参数Map中提取所需数据 - String objectIdColor = data.params.get("objectId").toString(); - String colorHex = data.params.get("colorHex").toString(); - - // 调用实际的业务逻辑 - rendererBridge.changeColor(objectIdColor, colorHex); - - // 表示我们已经成功处理了这个请求 - return true; - - // 在这里可以为未来新的积木添加更多的 case ... - - default: - // 请求是合法的JSON,但action是我们不认识的。记录一下,但不处理。 - System.err.println("BlocklyMessageHandler: 收到一个未知的操作(action): " + data.action); - return false; - } - } catch (JsonSyntaxException | ClassCastException | NullPointerException e) { - // 如果发生以下情况,说明这个请求不是我们想要的格式: - // - JsonSyntaxException: 字符串不是一个有效的JSON。 - // - ClassCastException: JSON中的数据类型与我们预期的不符(例如,x坐标是字符串)。 - // - NullPointerException: 缺少必要的参数(例如,params中没有"x"这个键)。 - // 在这些情况下,我们静默地失败并返回false,让其他处理器有机会处理这个请求。 - return false; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/block/BlocklyPanel.java b/src/main/java/com/chuangzhou/vivid2D/block/BlocklyPanel.java deleted file mode 100644 index df71e16..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/block/BlocklyPanel.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.chuangzhou.vivid2D.block; - -import com.axis.innovators.box.browser.CefAppManager; -import me.friwi.jcefmaven.CefAppBuilder; -import me.friwi.jcefmaven.CefInitializationException; -import me.friwi.jcefmaven.UnsupportedPlatformException; -import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler; -import org.cef.CefApp; -import org.cef.CefClient; -import org.cef.browser.CefBrowser; -import org.cef.browser.CefFrame; -import org.cef.browser.CefMessageRouter; -import org.cef.callback.CefQueryCallback; -import org.cef.handler.CefMessageRouterHandlerAdapter; -import com.google.gson.Gson; - -import javax.swing.*; -import java.awt.*; -import java.io.File; -import java.io.IOException; - -public class BlocklyPanel extends JPanel { - - private final CefClient cefClient; - private final CefBrowser cefBrowser; - private final Component browserUI; - private final Vivid2DRendererBridge rendererBridge = new Vivid2DRendererBridge(); - private final Gson gson = new Gson(); - - public BlocklyPanel() throws IOException, InterruptedException, UnsupportedPlatformException, CefInitializationException { - setLayout(new BorderLayout()); - CefApp cefAppManager = CefAppManager.getInstance(); - this.cefClient = cefAppManager.createClient(); - CefMessageRouter msgRouter = CefMessageRouter.create(); - msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { - @Override - public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, String request, - boolean persistent, CefQueryCallback callback) { - try { - System.out.println("Java端接收到指令: " + request); - RequestData data = gson.fromJson(request, RequestData.class); - handleRendererAction(data); - callback.success("OK"); // 通知JS端调用成功 - } catch (Exception e) { - e.printStackTrace(); - callback.failure(500, e.getMessage()); - } - return true; - } - }, true); - cefClient.addMessageRouter(msgRouter); - String url = new File("C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\src\\main\\resources\\web\\blockly_editor.html").toURI().toString(); - this.cefBrowser = cefClient.createBrowser(url, false, false); - this.browserUI = cefBrowser.getUIComponent(); - add(browserUI, BorderLayout.CENTER); - } - - // 辅助方法,根据JS请求调用不同的Java方法 - private void handleRendererAction(RequestData data) { - if (data == null || data.action == null) return; - switch (data.action) { - case "moveObject": - System.out.println(String.format( - "Java端接收到指令: 移动对象 '%s' 到坐标 (%d, %d)", - data.params.get("objectId").toString(), - ((Double)data.params.get("x")).intValue(), - ((Double)data.params.get("y")).intValue() - )); - rendererBridge.moveObject( - data.params.get("objectId").toString(), - ((Double)data.params.get("x")).intValue(), - ((Double)data.params.get("y")).intValue() - ); - break; - case "changeColor": - System.out.println(String.format( - "Java端接收到指令: 改变对象 '%s' 的颜色为 %s", - data.params.get("objectId").toString(), - data.params.get("colorHex").toString() - )); - rendererBridge.changeColor( - data.params.get("objectId").toString(), - data.params.get("colorHex").toString() - ); - break; - // 在这里添加更多case来处理其他操作 - default: - System.err.println("未知的操作: " + data.action); - } - } - - // 用于Gson解析的内部类 - private static class RequestData { - String action; - java.util.Map params; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/block/MainApplication.java b/src/main/java/com/chuangzhou/vivid2D/block/MainApplication.java deleted file mode 100644 index d22fb93..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/block/MainApplication.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.chuangzhou; - -import com.axis.innovators.box.browser.BrowserWindowJDialog; -import com.chuangzhou.vivid2D.block.BlocklyMessageHandler; -import com.formdev.flatlaf.FlatDarkLaf; -import org.cef.browser.CefBrowser; -import org.cef.browser.CefFrame; -import org.cef.browser.CefMessageRouter; -import org.cef.callback.CefQueryCallback; -import org.cef.handler.CefMessageRouterHandlerAdapter; - -import javax.swing.*; -import java.util.concurrent.atomic.AtomicReference; - -public class MainApplication { - - public static void main(String[] args) { - FlatDarkLaf.setup(); - SwingUtilities.invokeLater(() -> { - try { - AtomicReference windowRef = new AtomicReference<>(); - windowRef.set(new BrowserWindowJDialog.Builder("vivid2d-blockly-editor") - .title("Vivid2D - Blockly Editor") - .size(1280, 720) - .htmlPath("C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\src\\main\\resources\\web\\blockly_editor.html") - .build()); - BrowserWindowJDialog blocklyWindow = windowRef.get(); - if (blocklyWindow == null) { - throw new IllegalStateException("BrowserWindowJDialog未能成功创建。"); - } - CefMessageRouter msgRouter = blocklyWindow.getMsgRouter(); - if (msgRouter != null) { - final BlocklyMessageHandler blocklyHandler = new BlocklyMessageHandler(); - msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { - @Override - public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, - String request, boolean persistent, CefQueryCallback callback) { - // 尝试使用我们的处理器来处理请求 - if (blocklyHandler.handle(request)) { - // 如果 handle 方法返回 true,说明请求已被成功处理 - callback.success("OK from Blockly"); - return true; - } - // 如果我们的处理器不处理这个请求,返回 false,让其他处理器(比如默认的)有机会处理它 - return false; - } - }, true); // 第二个参数 true 表示这是第一个被检查的处理器 - } - } catch (Exception e) { - e.printStackTrace(); - JOptionPane.showMessageDialog( - null, - "无法初始化浏览器,请检查环境配置。\n错误: " + e.getMessage(), - "启动失败", - JOptionPane.ERROR_MESSAGE - ); - } - }); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/block/Vivid2DRendererBridge.java b/src/main/java/com/chuangzhou/vivid2D/block/Vivid2DRendererBridge.java deleted file mode 100644 index f84eeb8..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/block/Vivid2DRendererBridge.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.chuangzhou.vivid2D.block; - -public class Vivid2DRendererBridge { - - /** - * 当Blockly中的 "移动对象" 积木被执行时,此方法将被调用。 - * @param objectId 要移动的对象的ID - * @param x X坐标 - * @param y Y坐标 - */ - public void moveObject(String objectId, int x, int y) { - System.out.println(String.format( - "Java端接收到指令: 移动对象 '%s' 到坐标 (%d, %d)", objectId, x, y - )); - // TODO: 在这里调用您自己的 vivid2D 渲染器代码 - // aether.getRenderer().getObjectById(objectId).setPosition(x, y); - } - - /** - * 当Blockly中的 "改变颜色" 积木被执行时,此方法将被调用。 - * @param objectId 对象ID - * @param colorHex 16进制颜色字符串, e.g., "#FF0000" - */ - public void changeColor(String objectId, String colorHex) { - System.out.println(String.format( - "Java端接收到指令: 改变对象 '%s' 的颜色为 %s", objectId, colorHex - )); - // TODO: 在这里调用您自己的 vivid2D 渲染器代码 - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindow.java b/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindow.java deleted file mode 100644 index cdb3e59..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindow.java +++ /dev/null @@ -1,821 +0,0 @@ -package com.chuangzhou.vivid2D.browser; - -import com.axis.innovators.box.AxisInnovatorsBox; -import com.axis.innovators.box.events.BrowserCreationCallback; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import org.cef.CefApp; -import org.cef.CefClient; -import org.cef.CefSettings; -import org.cef.browser.CefBrowser; -import org.cef.browser.CefFrame; -import org.cef.browser.CefMessageRouter; -import org.cef.callback.CefContextMenuParams; -import org.cef.callback.CefJSDialogCallback; -import org.cef.callback.CefMenuModel; -import org.cef.callback.CefQueryCallback; -import org.cef.handler.*; -import org.cef.misc.BoolRef; -import org.cef.network.CefRequest; - -import javax.swing.*; -import java.awt.*; -import java.awt.datatransfer.Clipboard; -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.UnsupportedFlavorException; -import java.awt.event.*; -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; -import java.util.function.Consumer; - -import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST; - -/** - * @author tzdwindows 7 - */ -public class BrowserWindow extends JFrame { - private final String windowId; - private final String htmlUrl; - private CefApp cefApp; - private CefClient client; - private CefBrowser browser; - private final Component browserComponent; - private final String htmlPath; - private static boolean isInitialized = false; - private WindowOperationHandler operationHandler; - private static Thread cefThread; - private CefMessageRouter msgRouter; - - public static class Builder { - private BrowserCreationCallback browserCreationCallback; - private String windowId; - private String title = "JCEF Window"; - private Dimension size = new Dimension(800, 600); - private WindowOperationHandler operationHandler; - private String htmlPath; - private Image icon; - private boolean resizable = true; // 默认允许调整大小 - private boolean maximizable = true; // 默认允许最大化 - private boolean minimizable = true; // 默认允许最小化 - private String htmlUrl = ""; - private boolean openLinksInExternalBrowser = true; // 默认使用外部浏览器 - - public Builder resizable(boolean resizable) { - this.resizable = resizable; - return this; - } - - public Builder maximizable(boolean maximizable) { - this.maximizable = maximizable; - return this; - } - - /** - * 设置链接打开方式 - * - * @param openInBrowser 是否在当前浏览器窗口中打开链接 - * true - 在当前浏览器窗口中打开链接(本地跳转) - * false - 使用系统默认浏览器打开链接(外部跳转) - * @return Builder实例,支持链式调用 - * - * @apiNote 此方法控制两种不同的链接打开行为: - * 1. 当设置为true时: - * - 所有链接将在当前CEF浏览器窗口内打开 - * - * 2. 当设置为false时(默认值): - * - 所有链接将在系统默认浏览器中打开 - * - 更安全,避免潜在的安全风险 - * - 适用于简单的信息展示场景 - * - * @implNote 内部实现说明: - * - 实际存储的是反向值(openLinksInExternalBrowser) - * - 这样设置是为了保持与历史版本的兼容性 - * - 方法名使用"openInBrowser"更符合用户直觉 - * - * @example 使用示例: - * // 在当前窗口打开链接 - * new Builder().openLinksInBrowser(true).build(); - * - * // 使用系统浏览器打开链接(默认) - * new Builder().openLinksInBrowser(false).build(); - * - * @see #openLinksInExternalBrowser 内部存储字段 - * @see CefLifeSpanHandler#onBeforePopup 弹窗处理实现 - * @see CefRequestHandler#onBeforeBrowse 导航处理实现 - */ - public Builder openLinksInBrowser(boolean openInBrowser) { - this.openLinksInExternalBrowser = !openInBrowser; - return this; - } - - public Builder minimizable(boolean minimizable) { - this.minimizable = minimizable; - return this; - } - - public Builder(String windowId) { - this.windowId = windowId; - } - - /** - * 设置浏览器创建回调 - * @param callback 回调 - */ - public Builder setBrowserCreationCallback(BrowserCreationCallback callback){ - this.browserCreationCallback = callback; - return this; - } - - /** - * 设置浏览器窗口标题 - * @param title 标题 - */ - public Builder title(String title) { - this.title = title; - return this; - } - - /** - * 设置浏览器窗口大小 - * @param width 宽度 - * @param height 高度 - */ - public Builder size(int width, int height) { - this.size = new Dimension(width, height); - return this; - } - - /** - * 设置浏览器触发事件 - * @param handler 事件处理器 - */ - public Builder operationHandler(WindowOperationHandler handler) { - this.operationHandler = handler; - return this; - } - - /** - * 设置浏览器图标 - * @param icon 图标 - */ - public Builder icon(Image icon) { - this.icon = icon; - return this; - } - - /** - * 设置HTML路径 - */ - public BrowserWindow build() { - if (htmlUrl.isEmpty()) { - if (this.htmlPath == null || this.htmlPath.isEmpty()) { - throw new IllegalArgumentException("HTML paths cannot be empty"); - } - File htmlFile = new File(this.htmlPath); - if (!htmlFile.exists()) { - throw new RuntimeException("The HTML file does not exist: " + htmlFile.getAbsolutePath()); - } - } - return new BrowserWindow(this); - } - - /** - * 设置HTML路径 - * @param path HTML路径 - */ - public Builder htmlPath(String path) { - this.htmlPath = path; - return this; - } - - /** - * 使用Url - * @param htmlUrl Url路径 - */ - public Builder htmlUrl(String htmlUrl) { - this.htmlUrl = htmlUrl; - return this; - } - } - - private BrowserWindow(Builder builder) { - this.windowId = builder.windowId; - this.htmlPath = builder.htmlPath; - this.operationHandler = builder.operationHandler; - this.htmlUrl = builder.htmlUrl; - - // 设置图标(如果存在) - if (builder.icon != null) { - setIconImage(builder.icon); - } - - // 初始化浏览器组件 - try { - this.browserComponent = initializeCef(builder); - if (operationHandler != null) { - setupMessageHandlers(operationHandler); - } - } catch (Exception e) { - JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage()); - throw new RuntimeException(e); - } - } - - private Component initializeCef(Builder builder) throws MalformedURLException { - if (!isInitialized) { - isInitialized = true; - try { - this.cefApp = CefAppManager.getInstance(); - //CefAppManager.incrementBrowserCount(); - client = cefApp.createClient(); - client.addDisplayHandler(new CefDisplayHandler (){ - @Override - public void onAddressChange(CefBrowser browser, CefFrame frame, String url) { - - } - - @Override - public void onTitleChange(CefBrowser browser, String title) { - - } - - @Override - public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) { - - } - - @Override - public boolean onTooltip(CefBrowser browser, String text) { - return false; - } - - @Override - public void onStatusMessage(CefBrowser browser, String value) { - - } - - @Override - public boolean onConsoleMessage( - CefBrowser browser, - CefSettings.LogSeverity level, - String message, - String source, - int line - ) { - // 格式化输出到 Java 控制台 - //if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) { - String log = String.format( - "[Browser Console] %s %s (Line %d) -> %s", - getLogLevelSymbol(level), - source, - line, - message - ); - System.out.println(log); - //} - return false; - } - - @Override - public boolean onCursorChange(CefBrowser browser, int cursorType) { - return false; - } - - private String getLogLevelSymbol(CefSettings.LogSeverity level) { - switch (level) { - case LOGSEVERITY_ERROR: return "⛔"; - case LOGSEVERITY_WARNING: return "⚠️"; - case LOGSEVERITY_DEFAULT: return "🐞"; - default: return "ℹ️"; - } - } - }); - - if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) { - client.addKeyboardHandler(new CefKeyboardHandlerAdapter() { - @Override - public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) { - // 检测 F12 - if (event.windows_key_code == 123) { - browser.getDevTools().createImmediately(); - return true; - } - return false; - } - }); - } - - client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() { - @Override - public boolean onBeforePopup(CefBrowser browser, CefFrame frame, - String targetUrl, String targetFrameName) { - // 处理弹出窗口:根据配置决定打开方式 - if (builder.openLinksInExternalBrowser) { - // 使用默认浏览器打开 - try { - Desktop.getDesktop().browse(new URI(targetUrl)); - } catch (Exception e) { - System.out.println("Failed to open external browser: " + e.getMessage()); - } - return true; // 拦截弹窗 - } else { - // 在当前浏览器中打开 - browser.loadURL(targetUrl); - return true; // 拦截弹窗并在当前窗口打开 - } - } - }); - - client.addRequestHandler(new CefRequestHandlerAdapter() { - @Override - public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame, - CefRequest request, boolean userGesture, boolean isRedirect) { - // 处理主窗口导航 - if (userGesture) { - if (builder.openLinksInExternalBrowser) { - // 使用默认浏览器打开 - try { - Desktop.getDesktop().browse(new URI(request.getURL())); - return true; // 取消内置浏览器导航 - } catch (Exception e) { - System.out.println("Failed to open external browser: " + e.getMessage()); - } - } else { - // 允许在当前浏览器中打开 - return false; - } - } - return false; - } - }); - - client.addContextMenuHandler(new CefContextMenuHandlerAdapter() { - @Override - public void onBeforeContextMenu(CefBrowser browser, CefFrame frame, - CefContextMenuParams params, CefMenuModel model) { - model.clear(); - if (!params.getSelectionText().isEmpty() || params.isEditable()) { - model.addItem(MENU_ID_USER_FIRST, "复制"); - } - - if (params.isEditable()) { - model.addItem(MENU_ID_USER_FIRST + 1, "粘贴"); - model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本"); - } - } - - @Override - public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame, - CefContextMenuParams params, int commandId, int eventFlags) { - if (commandId == MENU_ID_USER_FIRST) { - if (params.isEditable()) { - browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0); - } else { - browser.executeJavaScript( - "window.getSelection().toString();", - browser.getURL(), - 0 - ); - } - return true; - } else if (commandId == MENU_ID_USER_FIRST + 1) { - pasteContent(browser, false); - return true; - } else if (commandId == MENU_ID_USER_FIRST + 2) { - pasteContent(browser, true); - return true; - } - - return false; - } - - /** - * 处理粘贴操作 - * @param plainText 是否去除格式(纯文本模式) - */ - private void pasteContent(CefBrowser browser, boolean plainText) { - try { - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) { - String text = (String) clipboard.getData(DataFlavor.stringFlavor); - - if (plainText) { - text = text.replaceAll("<[^>]+>", ""); - } - - String escapedText = text - .replace("\\", "\\\\") - .replace("'", "\\'") - .replace("\n", "\\n") - .replace("\r", "\\r"); - - String script = String.format( - "if (document.activeElement) {\n" + - " document.activeElement.value += '%s';\n" + // 简单追加文本 - " document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件 - "}", - escapedText - ); - browser.executeJavaScript(script, browser.getURL(), 0); - } - } catch (UnsupportedFlavorException | IOException e) { - e.printStackTrace(); - } - } - }); - - client.addJSDialogHandler(new CefJSDialogHandlerAdapter() { - @Override - public boolean onJSDialog(CefBrowser browser, String origin_url, JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) { - if (dialog_type == JSDialogType.JSDIALOGTYPE_ALERT) { - SwingUtilities.invokeLater(() -> { - JOptionPane.showMessageDialog( - BrowserWindow.this, - message_text, - "警告", - JOptionPane.INFORMATION_MESSAGE - ); - }); - callback.Continue(true, ""); - return true; - } - return false; - } - }); - - - // 3. 拦截所有新窗口(关键修复点!) - //client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() { - // @Override - // public boolean onBeforePopup(CefBrowser browser, - // CefFrame frame, String target_url, String target_frame_name) { - // return true; // 返回true表示拦截弹窗 - // } - //}); - - - Thread.currentThread().setName("BrowserRenderThread"); - - // 4. 加载HTML - if (htmlUrl.isEmpty()){ - String fileUrl = new File(htmlPath).toURI().toURL().toString(); - System.out.println("Loading HTML from: " + fileUrl); - - // 5. 创建浏览器组件(直接添加到内容面板) - browser = client.createBrowser(fileUrl, false, false); - } else { - System.out.println("Loading Url from: " + htmlUrl); - browser = client.createBrowser(htmlUrl, false, false); - } - - Component browserComponent = browser.getUIComponent(); - if (builder.browserCreationCallback != null) { - boolean handled = builder.browserCreationCallback.onLayoutCustomization( - this, // 当前窗口 - getContentPane(), // 内容面板 - browserComponent, // 浏览器组件 - builder // 构建器对象 - ); - - // 如果回调返回true,跳过默认布局 - if (handled) { - // 设置窗口基本属性 - setTitle(builder.title); - setSize(builder.size); - setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); - - // 添加资源释放监听器 - addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent e) { - browser.close(true); - client.dispose(); - } - }); - - setVisible(true); - return browserComponent; // 直接返回,跳过默认布局 - } - } - - CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig(); - config.jsQueryFunction = "javaQuery";// 定义方法 - config.jsCancelFunction = "javaQueryCancel";// 定义取消方法 - - updateTheme(); - - // 6. 配置窗口布局(确保只添加一次) - SwingUtilities.invokeLater(() -> { - getContentPane().removeAll(); - getContentPane().setLayout(new BorderLayout()); - - // 透明拖拽层(仅顶部可拖拽) - JPanel dragPanel = new JPanel(new BorderLayout()); - dragPanel.setOpaque(false); - - JPanel titleBar = new JPanel(); - titleBar.setOpaque(false); - titleBar.setPreferredSize(new Dimension(builder.size.width, 20)); - final Point[] dragStart = new Point[1]; - titleBar.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - dragStart[0] = e.getPoint(); - } - - @Override - public void mouseReleased(MouseEvent e) { - dragStart[0] = null; - } - }); - titleBar.addMouseMotionListener(new MouseMotionAdapter() { - @Override - public void mouseDragged(MouseEvent e) { - if (dragStart[0] != null) { - Point curr = e.getLocationOnScreen(); - setLocation(curr.x - dragStart[0].x, curr.y - dragStart[0].y); - } - } - }); - - dragPanel.add(titleBar, BorderLayout.NORTH); - getContentPane().add(dragPanel, BorderLayout.CENTER); - getContentPane().add(browserComponent, BorderLayout.CENTER); - - // 7. 窗口属性设置 - setTitle(builder.title); - setSize(builder.size); - setLocationRelativeTo(null); - setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); - - // 8. 资源释放 - addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent e) { - browser.close(true); - client.dispose(); - } - }); - - setVisible(true); - - }); - return browserComponent; - } catch (Exception e) { - e.printStackTrace(); - JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - } - } else { - isInitialized = false; - SwingUtilities.invokeLater(() -> { - dispose(); - }); - } - return null; - } - - /** - * 更新主题 - */ - public void updateTheme() { - // 1. 获取Java字体信息 - String fontInfo = getSystemFontsInfo(); - boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode(); - injectFontInfoToPage(browser, fontInfo, isDarkTheme); - - // 2. 注入主题信息 - //injectThemeInfoToPage(browser, isDarkTheme); - - //// 3. 刷新浏览器 - //SwingUtilities.invokeLater(() -> { - // browser.reload(); - //}); - } - - /** - * 获取Java字体信息(从UIManager获取) - */ - private String getSystemFontsInfo() { - try { - Gson gson = new Gson(); - JsonObject fontInfo = new JsonObject(); - JsonObject uiFonts = new JsonObject(); - - String[] fontKeys = { - "Label.font", "Button.font", "ToggleButton.font", "RadioButton.font", - "CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font", - "TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font", - "FormattedTextField.font", "Table.font", "TableHeader.font", "List.font", - "Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font", - "PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font", - "Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont", - "OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font" - }; - - for (String key : fontKeys) { - Font font = UIManager.getFont(key); - if (font != null) { - JsonObject fontObj = new JsonObject(); - fontObj.addProperty("name", font.getFontName()); - fontObj.addProperty("family", font.getFamily()); - fontObj.addProperty("size", font.getSize()); - fontObj.addProperty("style", font.getStyle()); - fontObj.addProperty("bold", font.isBold()); - fontObj.addProperty("italic", font.isItalic()); - fontObj.addProperty("plain", font.isPlain()); - uiFonts.add(key, fontObj); - } - } - - fontInfo.add("uiFonts", uiFonts); - fontInfo.addProperty("timestamp", System.currentTimeMillis()); - fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName()); - - return gson.toJson(fontInfo); - } catch (Exception e) { - return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}"; - } - } - - /** - * 注入主题信息到页面 - */ - private void injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) { - if (client == null) { - return; - } - - String themeInfo = String.format( - "{\"isDarkTheme\": %s, \"timestamp\": %d}", - isDarkTheme, - System.currentTimeMillis() - ); - - // 最简单的脚本 - 直接设置和分发事件 - String script = String.format( - "window.javaThemeInfo = %s;" + - "console.log('主题信息已设置:', window.javaThemeInfo);" + - "" + - "var event = new CustomEvent('javaThemeChanged', {" + - " detail: window.javaThemeInfo" + - "});" + - "document.dispatchEvent(event);" + - "console.log('javaThemeChanged事件已分发');", - themeInfo); - - browser.executeJavaScript(script, browser.getURL(), 0); - } - - /** - * 注入字体信息到页面并设置字体 - */ - private void injectFontInfoToPage(CefBrowser browser, String fontInfo,boolean isDarkTheme) { - if (client == null) { - return; - } - client.addLoadHandler(new CefLoadHandlerAdapter() { - @Override - public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { - // 使用更简单的脚本来注入字体信息 - String script = - "if (typeof window.javaFontInfo === 'undefined') {" + - " window.javaFontInfo = " + fontInfo + ";" + - " console.log('Java font information has been loaded:', window.javaFontInfo);" + - " " + - " var event = new CustomEvent('javaFontsLoaded', {" + - " detail: window.javaFontInfo" + - " });" + - " document.dispatchEvent(event);" + - " console.log('The javaFontsLoaded event is dispatched');" + - "}"; - - browser.executeJavaScript(script, browser.getURL(), 0); - - // 添加调试信息 - browser.executeJavaScript( - "console.log('Font information injection is complete,window.javaFontInfo:', typeof window.javaFontInfo);" + - "console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');", - browser.getURL(), 0 - ); - - String themeInfo = String.format( - "{\"isDarkTheme\": %s, \"timestamp\": %d}", - isDarkTheme, - System.currentTimeMillis() - ); - - script = String.format( - "window.javaThemeInfo = %s;" + - "console.log('主题信息已设置:', window.javaThemeInfo);" + - "" + - "var event = new CustomEvent('javaThemeChanged', {" + - " detail: window.javaThemeInfo" + - "});" + - "document.dispatchEvent(event);" + - "console.log('javaThemeChanged事件已分发');", - themeInfo); - - browser.executeJavaScript(script, browser.getURL(), 0); - } - }); - - } - - public static void printStackTrace() { - StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); - for (int i = 2; i < stackTrace.length; i++) { - StackTraceElement element = stackTrace[i]; - System.out.println(element.getClassName() + "." + element.getMethodName() + - "(" + (element.getFileName() != null ? element.getFileName() : "Unknown Source") + - ":" + element.getLineNumber() + ")"); - } - } - - @Override - public void setVisible(boolean b) { - if (b) { - if (browser != null) { - updateTheme(); - } - } - super.setVisible(b); - } - - public Component getBrowserComponent() { - return browserComponent; - } - - private void setupMessageHandlers(WindowOperationHandler handler) { - if (client != null) { - msgRouter = CefMessageRouter.create(); - msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { - @Override - public boolean onQuery(CefBrowser browser, - CefFrame frame, - long queryId, - String request, - boolean persistent, - CefQueryCallback callback) { - if (request.startsWith("system:")) { - String[] parts = request.split(":"); - String operation = parts.length >= 2 ? parts[1] : null; - String targetWindow = parts.length > 2 ? parts[2] : null; - handler.handleOperation( - new WindowOperation(operation, targetWindow, callback) // [!code ++] - ); - return true; - } - if (request.startsWith("java-response:")) { - String[] parts = request.split(":"); - String requestId = parts[1]; - String responseData = parts.length > 2 ? parts[2] : ""; - Consumer handler = WindowRegistry.getInstance().getCallback(requestId); - if (handler != null) { - handler.accept(responseData); - callback.success(""); - } else { - callback.failure(-1, "无效的请求ID"); - } - return true; - } - return false; - } - }, true); - client.addMessageRouter(msgRouter); - } - } - - public String getWindowId() { - return windowId; - } - - /** - * 获取消息路由器 - * @return 消息路由器 - */ - public CefMessageRouter getMsgRouter() { - return msgRouter; - } - - /** - * 获取浏览器对象 - * @return 浏览器对象 - */ - public CefBrowser getBrowser() { - return browser; - } - - public void closeWindow() { - SwingUtilities.invokeLater(() -> { - if (browser != null) { - browser.close(true); - } - dispose(); - cefApp.dispose(); - WindowRegistry.getInstance().unregisterWindow(windowId); - }); - } -} - diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindowJDialog.java b/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindowJDialog.java deleted file mode 100644 index 2a4f4e6..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/browser/BrowserWindowJDialog.java +++ /dev/null @@ -1,833 +0,0 @@ -package com.chuangzhou.vivid2D.browser; - -import com.axis.innovators.box.AxisInnovatorsBox; -import com.axis.innovators.box.events.BrowserCreationCallback; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import org.cef.CefApp; -import org.cef.CefClient; -import org.cef.CefSettings; -import org.cef.browser.CefBrowser; -import org.cef.browser.CefFrame; -import org.cef.browser.CefMessageRouter; -import org.cef.callback.CefContextMenuParams; -import org.cef.callback.CefJSDialogCallback; -import org.cef.callback.CefMenuModel; -import org.cef.callback.CefQueryCallback; -import org.cef.handler.*; -import org.cef.misc.BoolRef; -import org.cef.network.CefRequest; - -import javax.swing.*; -import java.awt.*; -import java.awt.datatransfer.Clipboard; -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.UnsupportedFlavorException; -import java.awt.event.*; -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; -import java.util.function.Consumer; - -import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST; - -/** - * @author tzdwindows 7 - */ -public class BrowserWindowJDialog extends JDialog { - private final String windowId; - private final String htmlUrl; - private CefApp cefApp; - private CefClient client; - private CefBrowser browser; - private final Component browserComponent; - private final String htmlPath; - //private boolean isInitialized = false; - private WindowOperationHandler operationHandler; - private static Thread cefThread; - private CefMessageRouter msgRouter; - - public static class Builder { - private String windowId; - private String title = "JCEF Window"; - private Dimension size = new Dimension(800, 600); - private WindowOperationHandler operationHandler; - private String htmlPath; - private Image icon; - private JFrame parentFrame; - private boolean resizable = true; // 默认允许调整大小 - private boolean maximizable = true; // 默认允许最大化 - private boolean minimizable = true; // 默认允许最小化 - private String htmlUrl = ""; - private BrowserCreationCallback browserCreationCallback; - private boolean openLinksInExternalBrowser = true; // 默认使用外部浏览器 - - public Builder resizable(boolean resizable) { - this.resizable = resizable; - return this; - } - - public Builder maximizable(boolean maximizable) { - this.maximizable = maximizable; - return this; - } - - public Builder minimizable(boolean minimizable) { - this.minimizable = minimizable; - return this; - } - - public Builder(String windowId) { - this.windowId = windowId; - } - - /** - * 设置浏览器窗口标题 - * @param title 标题 - */ - public Builder title(String title) { - this.title = title; - return this; - } - - /** - * 设置链接打开方式 - * - * @param openInBrowser 是否在当前浏览器窗口中打开链接 - * true - 在当前浏览器窗口中打开链接(本地跳转) - * false - 使用系统默认浏览器打开链接(外部跳转) - * @return Builder实例,支持链式调用 - * - * @apiNote 此方法控制两种不同的链接打开行为: - * 1. 当设置为true时: - * - 所有链接将在当前CEF浏览器窗口内打开 - * - * 2. 当设置为false时(默认值): - * - 所有链接将在系统默认浏览器中打开 - * - 更安全,避免潜在的安全风险 - * - 适用于简单的信息展示场景 - * - * @implNote 内部实现说明: - * - 实际存储的是反向值(openLinksInExternalBrowser) - * - 这样设置是为了保持与历史版本的兼容性 - * - 方法名使用"openInBrowser"更符合用户直觉 - * - * @example 使用示例: - * // 在当前窗口打开链接 - * new Builder().openLinksInBrowser(true).build(); - * - * // 使用系统浏览器打开链接(默认) - * new Builder().openLinksInBrowser(false).build(); - * - * @see #openLinksInExternalBrowser 内部存储字段 - * @see CefLifeSpanHandler#onBeforePopup 弹窗处理实现 - * @see CefRequestHandler#onBeforeBrowse 导航处理实现 - */ - public Builder openLinksInBrowser(boolean openInBrowser) { - this.openLinksInExternalBrowser = !openInBrowser; - return this; - } - - - /** - * 设置浏览器创建回调 - * @param callback 回调 - */ - public Builder setBrowserCreationCallback(BrowserCreationCallback callback){ - this.browserCreationCallback = callback; - return this; - } - - /** - * 设置浏览器窗口大小 - * @param width 宽度 - * @param height 高度 - */ - public Builder size(int width, int height) { - this.size = new Dimension(width, height); - return this; - } - - /** - * 设置浏览器窗口父窗口 - * @param parent 父窗口 - */ - public Builder parentFrame(JFrame parent) { - this.parentFrame = parent; - return this; - } - - - /** - * 设置浏览器触发事件 - * @param handler 事件处理器 - */ - public Builder operationHandler(WindowOperationHandler handler) { - this.operationHandler = handler; - return this; - } - - /** - * 设置浏览器图标 - * @param icon 图标 - */ - public Builder icon(Image icon) { - this.icon = icon; - return this; - } - - /** - * 设置HTML路径 - */ - public BrowserWindowJDialog build() { - if (htmlUrl.isEmpty()) { - if (this.htmlPath == null || this.htmlPath.isEmpty()) { - throw new IllegalArgumentException("HTML paths cannot be empty"); - } - File htmlFile = new File(this.htmlPath); - if (!htmlFile.exists()) { - throw new RuntimeException("The HTML file does not exist: " + htmlFile.getAbsolutePath()); - } - } - return new BrowserWindowJDialog(this); - } - - /** - * 设置HTML路径 - * @param path HTML路径 - */ - public Builder htmlPath(String path) { - this.htmlPath = path; - return this; - } - - /** - * 使用Url - * @param htmlUrl Url路径 - */ - public Builder htmlUrl(String htmlUrl) { - this.htmlUrl = htmlUrl; - return this; - } - } - - - private BrowserWindowJDialog(Builder builder) { - // 根据父窗口是否存在,设置是否为模态对话框 - super(builder.parentFrame, builder.title, builder.parentFrame != null); - this.windowId = builder.windowId; - this.htmlPath = builder.htmlPath; - this.htmlUrl = builder.htmlUrl; - this.operationHandler = builder.operationHandler; - - // 设置图标(如果存在) - if (builder.icon != null) { - setIconImage(builder.icon); - } - - // 初始化浏览器组件 - try { - this.browserComponent = initializeCef(builder); - if (operationHandler != null) { - setupMessageHandlers(operationHandler); - } - } catch (Exception e) { - JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage()); - throw new RuntimeException(e); - } - } - private static boolean isInitialized = false; - private Component initializeCef(Builder builder) throws MalformedURLException { - if (!isInitialized) { - isInitialized = true; - try { - this.cefApp = CefAppManager.getInstance(); - //CefAppManager.incrementBrowserCount(); - client = cefApp.createClient(); - client.addDisplayHandler(new CefDisplayHandler (){ - @Override - public void onAddressChange(CefBrowser browser, CefFrame frame, String url) {} - - @Override - public void onTitleChange(CefBrowser browser, String title) {} - - @Override - public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) {} - - @Override - public boolean onTooltip(CefBrowser browser, String text) { - return false; - } - - @Override - public void onStatusMessage(CefBrowser browser, String value) {} - - @Override - public boolean onConsoleMessage( - CefBrowser browser, - CefSettings.LogSeverity level, - String message, - String source, - int line - ) { - // 格式化输出到 Java 控制台 - //if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) { - String log = String.format( - "[Browser Console] %s %s (Line %d) -> %s", - getLogLevelSymbol(level), - source, - line, - message - ); - System.out.println(log); - //} - return false; - } - - @Override - public boolean onCursorChange(CefBrowser browser, int cursorType) { - return false; - } - - private String getLogLevelSymbol(CefSettings.LogSeverity level) { - switch (level) { - case LOGSEVERITY_ERROR: return "⛔"; - case LOGSEVERITY_WARNING: return "⚠️"; - case LOGSEVERITY_DEFAULT: return "🐞"; - default: return "ℹ️"; - } - } - }); - - if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) { - client.addKeyboardHandler(new CefKeyboardHandlerAdapter() { - @Override - public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) { - // 检测 F12 - if (event.windows_key_code == 123) { - browser.getDevTools().createImmediately(); - return true; - } - return false; - } - }); - } - - client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() { - @Override - public boolean onBeforePopup(CefBrowser browser, CefFrame frame, - String targetUrl, String targetFrameName) { - // 处理弹出窗口:根据配置决定打开方式 - if (builder.openLinksInExternalBrowser) { - // 使用默认浏览器打开 - try { - Desktop.getDesktop().browse(new URI(targetUrl)); - } catch (Exception e) { - System.out.println("Failed to open external browser: " + e.getMessage()); - } - return true; // 拦截弹窗 - } else { - // 在当前浏览器中打开 - browser.loadURL(targetUrl); - return true; // 拦截弹窗并在当前窗口打开 - } - } - }); - - client.addRequestHandler(new CefRequestHandlerAdapter() { - @Override - public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame, - CefRequest request, boolean userGesture, boolean isRedirect) { - // 处理主窗口导航 - if (userGesture) { - if (builder.openLinksInExternalBrowser) { - // 使用默认浏览器打开 - try { - Desktop.getDesktop().browse(new URI(request.getURL())); - return true; // 取消内置浏览器导航 - } catch (Exception e) { - System.out.println("Failed to open external browser: " + e.getMessage()); - } - } else { - // 允许在当前浏览器中打开 - return false; - } - } - return false; - } - }); - - client.addContextMenuHandler(new CefContextMenuHandlerAdapter() { - @Override - public void onBeforeContextMenu(CefBrowser browser, CefFrame frame, - CefContextMenuParams params, CefMenuModel model) { - model.clear(); - if (!params.getSelectionText().isEmpty() || params.isEditable()) { - model.addItem(MENU_ID_USER_FIRST, "复制"); - } - - if (params.isEditable()) { - model.addItem(MENU_ID_USER_FIRST + 1, "粘贴"); - model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本"); - } - } - - @Override - public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame, - CefContextMenuParams params, int commandId, int eventFlags) { - if (commandId == MENU_ID_USER_FIRST) { - if (params.isEditable()) { - browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0); - } else { - browser.executeJavaScript( - "window.getSelection().toString();", - browser.getURL(), - 0 - ); - } - return true; - } else if (commandId == MENU_ID_USER_FIRST + 1) { - pasteContent(browser, false); - return true; - } else if (commandId == MENU_ID_USER_FIRST + 2) { - pasteContent(browser, true); - return true; - } - - return false; - } - - /** - * 处理粘贴操作 - * @param plainText 是否去除格式(纯文本模式) - */ - private void pasteContent(CefBrowser browser, boolean plainText) { - try { - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) { - String text = (String) clipboard.getData(DataFlavor.stringFlavor); - - if (plainText) { - text = text.replaceAll("<[^>]+>", ""); - } - - String escapedText = text - .replace("\\", "\\\\") - .replace("'", "\\'") - .replace("\n", "\\n") - .replace("\r", "\\r"); - - String script = String.format( - "if (document.activeElement) {\n" + - " document.activeElement.value += '%s';\n" + // 简单追加文本 - " document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件 - "}", - escapedText - ); - browser.executeJavaScript(script, browser.getURL(), 0); - } - } catch (UnsupportedFlavorException | IOException e) { - e.printStackTrace(); - } - } - }); - - // 添加 alert 弹窗监控处理 - client.addJSDialogHandler(new CefJSDialogHandlerAdapter() { - @Override - public boolean onJSDialog(CefBrowser browser, String origin_url, JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) { - if (dialog_type == JSDialogType.JSDIALOGTYPE_ALERT) { - SwingUtilities.invokeLater(() -> { - JOptionPane.showMessageDialog( - BrowserWindowJDialog.this, - message_text, - "警告", - JOptionPane.INFORMATION_MESSAGE - ); - }); - callback.Continue(true, ""); - return true; - } - return false; - } - }); - - - - // 3. 拦截所有新窗口(关键修复点!) - //client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() { - // @Override - // public boolean onBeforePopup(CefBrowser browser, - // CefFrame frame, String target_url, String target_frame_name) { - // return true; // 返回true表示拦截弹窗 - // } - //}); - - - Thread.currentThread().setName("BrowserRenderThread"); - - - // 4. 加载HTML - if (htmlUrl.isEmpty()) { - String fileUrl = new File(htmlPath).toURI().toURL().toString(); - System.out.println("Loading HTML from: " + fileUrl); - - // 5. 创建浏览器组件(直接添加到内容面板) - browser = client.createBrowser(fileUrl, false, false); - } else { - System.out.println("Loading HTML from: " + htmlUrl); - browser = client.createBrowser(htmlUrl, false, false); - } - - Component browserComponent = browser.getUIComponent(); - - if (builder.browserCreationCallback != null) { - boolean handled = builder.browserCreationCallback.onLayoutCustomization( - this, // 当前窗口 - getContentPane(), // 内容面板 - browserComponent, // 浏览器组件 - builder // 构建器对象 - ); - - // 如果回调返回true,跳过默认布局 - if (handled) { - // 设置窗口基本属性 - setTitle(builder.title); - setSize(builder.size); - setLocationRelativeTo(builder.parentFrame); - setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); - - // 添加资源释放监听器 - addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent e) { - browser.close(true); - client.dispose(); - } - }); - - setVisible(true); - return browserComponent; // 直接返回,跳过默认布局 - } - } - - updateTheme(); - - CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig(); - config.jsQueryFunction = "javaQuery";// 定义方法 - config.jsCancelFunction = "javaQueryCancel";// 定义取消方法 - - - // 6. 配置窗口布局(确保只添加一次) - SwingUtilities.invokeLater(() -> { - getContentPane().removeAll(); - getContentPane().setLayout(new BorderLayout()); - - // 透明拖拽层(仅顶部可拖拽) - JPanel dragPanel = new JPanel(new BorderLayout()); - dragPanel.setOpaque(false); - - JPanel titleBar = new JPanel(); - titleBar.setOpaque(false); - titleBar.setPreferredSize(new Dimension(builder.size.width, 20)); - final Point[] dragStart = new Point[1]; - titleBar.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - dragStart[0] = e.getPoint(); - } - - @Override - public void mouseReleased(MouseEvent e) { - dragStart[0] = null; - } - }); - titleBar.addMouseMotionListener(new MouseMotionAdapter() { - @Override - public void mouseDragged(MouseEvent e) { - if (dragStart[0] != null) { - Point curr = e.getLocationOnScreen(); - setLocation(curr.x - dragStart[0].x, curr.y - dragStart[0].y); - } - } - }); - - - - dragPanel.add(titleBar, BorderLayout.NORTH); - getContentPane().add(dragPanel, BorderLayout.CENTER); - getContentPane().add(browserComponent, BorderLayout.CENTER); - - // 7. 窗口属性设置 - setTitle(builder.title); - setSize(builder.size); - setLocationRelativeTo(builder.parentFrame); - setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); - - // 8. 资源释放 - addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent e) { - browser.close(true); - client.dispose(); - } - }); - - setVisible(true); - - }); - return browserComponent; - } catch (Exception e) { - e.printStackTrace(); - JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - } - } else { - isInitialized = false; - SwingUtilities.invokeLater(() -> { - dispose(); - }); - } - return null; - } - - - - /** - * 更新主题 - */ - public void updateTheme() { - // 1. 获取Java字体信息 - String fontInfo = getSystemFontsInfo(); - injectFontInfoToPage(browser, fontInfo); - - // 2. 注入主题信息 - if (AxisInnovatorsBox.getMain() != null) { - boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode(); - injectThemeInfoToPage(browser, isDarkTheme); - } - - //// 3. 刷新浏览器 - //SwingUtilities.invokeLater(() -> { - // browser.reload(); - //}); - - } - private void injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) { - if (client == null) { - return; - } - - client.addLoadHandler(new CefLoadHandlerAdapter() { - @Override - public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { - String themeInfo = String.format( - "{\"isDarkTheme\": %s, \"timestamp\": %d}", - isDarkTheme, - System.currentTimeMillis() - ); - - String script = - "window.javaThemeInfo = " + themeInfo + ";\n" + - "console.log('Java theme information has been loaded:', window.javaThemeInfo);\n" + - "\n" + - "if (typeof applyJavaTheme === 'function') {\n" + - " applyJavaTheme(window.javaThemeInfo);\n" + - "}\n" + - "\n" + - "var event = new CustomEvent('javaThemeChanged', {\n" + - " detail: window.javaThemeInfo\n" + - "});\n" + - "document.dispatchEvent(event);\n" + - "console.log('The javaThemeChanged event is dispatched');"; - - browser.executeJavaScript(script, browser.getURL(), 0); - - browser.executeJavaScript( - "console.log('Theme information injection is complete,window.javaThemeInfo:', typeof window.javaThemeInfo);" + - "console.log('Number of theme event listeners:', document.eventListeners ? document.eventListeners('javaThemeChanged') : '无法获取');", - browser.getURL(), 0 - ); - } - }); - } - - - /** - * 获取Java字体信息(从UIManager获取) - */ - private String getSystemFontsInfo() { - try { - Gson gson = new Gson(); - JsonObject fontInfo = new JsonObject(); - JsonObject uiFonts = new JsonObject(); - - String[] fontKeys = { - "Label.font", "Button.font", "ToggleButton.font", "RadioButton.font", - "CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font", - "TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font", - "FormattedTextField.font", "Table.font", "TableHeader.font", "List.font", - "Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font", - "PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font", - "Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont", - "OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font" - }; - - for (String key : fontKeys) { - Font font = UIManager.getFont(key); - if (font != null) { - JsonObject fontObj = new JsonObject(); - fontObj.addProperty("name", font.getFontName()); - fontObj.addProperty("family", font.getFamily()); - fontObj.addProperty("size", font.getSize()); - fontObj.addProperty("style", font.getStyle()); - fontObj.addProperty("bold", font.isBold()); - fontObj.addProperty("italic", font.isItalic()); - fontObj.addProperty("plain", font.isPlain()); - uiFonts.add(key, fontObj); - } - } - - fontInfo.add("uiFonts", uiFonts); - fontInfo.addProperty("timestamp", System.currentTimeMillis()); - fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName()); - - return gson.toJson(fontInfo); - } catch (Exception e) { - return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}"; - } - } - - /** - * 注入字体信息到页面并设置字体 - */ - private void injectFontInfoToPage(CefBrowser browser, String fontInfo) { - if (client == null) { - return; - } - client.addLoadHandler(new CefLoadHandlerAdapter() { - @Override - public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { - // 使用更简单的脚本来注入字体信息 - String script = - "if (typeof window.javaFontInfo === 'undefined') {" + - " window.javaFontInfo = " + fontInfo + ";" + - " console.log('Java font information has been loaded:', window.javaFontInfo);" + - " " + - " var event = new CustomEvent('javaFontsLoaded', {" + - " detail: window.javaFontInfo" + - " });" + - " document.dispatchEvent(event);" + - " console.log('The javaFontsLoaded event is dispatched');" + - "}"; - - System.out.println("正在注入字体信息到页面..."); - browser.executeJavaScript(script, browser.getURL(), 0); - - // 添加调试信息 - browser.executeJavaScript( - "console.log('Font information injection is complete,window.javaFontInfo:', typeof window.javaFontInfo);" + - "console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');", - browser.getURL(), 0 - ); - } - }); - - } - - public static void printStackTrace() { - StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); - for (int i = 2; i < stackTrace.length; i++) { - StackTraceElement element = stackTrace[i]; - System.out.println(element.getClassName() + "." + element.getMethodName() + - "(" + (element.getFileName() != null ? element.getFileName() : "Unknown Source") + - ":" + element.getLineNumber() + ")"); - } - } - - @Override - public void setVisible(boolean b) { - if (b) { - if (browser != null) { - updateTheme(); - } - } - super.setVisible(b); - } - - public Component getBrowserComponent() { - return browserComponent; - } - - private void setupMessageHandlers(WindowOperationHandler handler) { - if (client != null) { - msgRouter = CefMessageRouter.create(); - msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { - @Override - public boolean onQuery(CefBrowser browser, - CefFrame frame, - long queryId, - String request, - boolean persistent, - CefQueryCallback callback) { - if (request.startsWith("system:")) { - String[] parts = request.split(":"); - String operation = parts.length >= 2 ? parts[1] : null; - String targetWindow = parts.length > 2 ? parts[2] : null; - handler.handleOperation( - new WindowOperation(operation, targetWindow, callback) // [!code ++] - ); - return true; - } - if (request.startsWith("java-response:")) { - String[] parts = request.split(":"); - String requestId = parts[1]; - String responseData = parts.length > 2 ? parts[2] : ""; - Consumer handler = WindowRegistry.getInstance().getCallback(requestId); - if (handler != null) { - handler.accept(responseData); - callback.success(""); - } else { - callback.failure(-1, "无效的请求ID"); - } - return true; - } - return false; - } - }, true); - client.addMessageRouter(msgRouter); - } - } - - public String getWindowId() { - return windowId; - } - - /** - * 获取消息路由器 - * @return 消息路由器 - */ - public CefMessageRouter getMsgRouter() { - return msgRouter; - } - - /** - * 获取浏览器对象 - * @return 浏览器对象 - */ - public CefBrowser getBrowser() { - return browser; - } - - public void closeWindow() { - SwingUtilities.invokeLater(() -> { - if (browser != null) { - browser.close(true); - } - dispose(); - cefApp.dispose(); - WindowRegistry.getInstance().unregisterWindow(windowId); - }); - } -} - diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/CefAppManager.java b/src/main/java/com/chuangzhou/vivid2D/browser/CefAppManager.java deleted file mode 100644 index f858675..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/browser/CefAppManager.java +++ /dev/null @@ -1,280 +0,0 @@ -package com.chuangzhou.vivid2D.browser; - -import com.axis.innovators.box.AxisInnovatorsBox; -import com.axis.innovators.box.register.LanguageManager; -import com.axis.innovators.box.tools.FolderCreator; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.cef.CefApp; -import org.cef.CefSettings; -import org.cef.callback.CefCommandLine; -import org.cef.handler.CefAppHandlerAdapter; - -import java.io.File; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -/** - * 增强版CEF应用管理器 - * 特性: - * 1. 多级锁并发控制 - * 2. 设置冲突自动恢复 - * 3. 状态跟踪和验证 - * 4. 增强的异常处理 - * - * @author tzdwindows 7 - */ -public class CefAppManager { - private static final Logger logger = LogManager.getLogger(CefAppManager.class); - private static volatile CefApp cefApp; - private static final CefSettings settings = new CefSettings(); - - // 状态跟踪 - private static final AtomicBoolean isInitialized = new AtomicBoolean(false); - private static final AtomicBoolean settingsApplied = new AtomicBoolean(false); - private static final AtomicBoolean isDisposing = new AtomicBoolean(false); - - // 并发控制 - private static final Lock initLock = new ReentrantLock(); - private static final Lock disposeLock = new ReentrantLock(); - private static final AtomicBoolean shutdownHookRegistered = new AtomicBoolean(false); - - static { - initializeDefaultSettings(); - registerShutdownHook(); - } - - private static void registerShutdownHook() { - if (shutdownHookRegistered.compareAndSet(false, true)) { - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - logger.info("JVM shutdown hook triggered"); - dispose(true); - })); - logger.debug("Shutdown hook registered successfully"); - } - } - - /** - * 初始化Cef - */ - private static void initializeDefaultSettings() { - initLock.lock(); - try { - settings.windowless_rendering_enabled = false; - settings.javascript_flags = ""; - settings.cache_path = FolderCreator.getLibraryFolder() + "/jcef/cache"; - settings.root_cache_path = FolderCreator.getLibraryFolder() + "/jcef/cache"; - settings.persist_session_cookies = false; - settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_WARNING; - - String subprocessPath = FolderCreator.getLibraryFolder() + "/jcef/lib/win64/jcef_helper.exe"; - validateSubprocessPath(subprocessPath); - settings.browser_subprocess_path = subprocessPath; - - //settings.background_color = new Color(255, 255, 255, 0); - settings.command_line_args_disabled = false; - - // 转换语言标识格式:system:zh_CN -> zh-CN - - CefApp.addAppHandler(new CefAppHandlerAdapter(null) { - @Override - public void onBeforeCommandLineProcessing( - String processType, - CefCommandLine commandLine - ) { - //commandLine.appendSwitch("disable-dev-tools"); - //commandLine.appendSwitch("disable-view-source"); - - LanguageManager.loadSavedLanguage(); - LanguageManager.Language currentLang = LanguageManager.getLoadedLanguages(); - if (currentLang != null){ - String langCode = currentLang.getRegisteredName() - .replace("system:", "") - .replace("_", "-") - .toLowerCase(); - settings.locale = langCode; - commandLine.appendSwitchWithValue("--lang", langCode); - commandLine.appendSwitchWithValue("--accept-language", langCode); - } - - boolean isDarkTheme = isDarkTheme(); - if (isDarkTheme) { - commandLine.appendSwitch("force-dark-mode"); - commandLine.appendSwitchWithValue("enable-features", "WebContentsForceDark"); - } - - logger.info("CEF commandLine: {}", commandLine.getSwitches()); - } - }); - - logger.info("Optimized CEF settings initialized"); - } finally { - initLock.unlock(); - } - } - - /** - * 判断地区主题是否为黑色主题 - * @return 是否 - */ - private static boolean isDarkTheme() { - if (AxisInnovatorsBox.getMain() == null){ - return false; - } - return AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode(); - } - - public static CefApp getInstance() { - if (cefApp == null) { - if (initLock.tryLock()) { - try { - performSafeInitialization(); - } finally { - initLock.unlock(); - } - } else { - handleConcurrentInitialization(); - } - } - return cefApp; - } - - private static void performSafeInitialization() { - if (cefApp != null) return; - if (isDisposing.get()) { - throw new IllegalStateException("CEF is during disposal process"); - } - - try { - // 阶段1:启动CEF运行时 - if (!CefApp.startup(new String[0])) { - throw new IllegalStateException("CEF native startup failed"); - } - - // 阶段2:应用设置(仅首次) - if (settingsApplied.compareAndSet(false, true)) { - cefApp = CefApp.getInstance(settings); - logger.info("CEF initialized with custom settings"); - } else { - cefApp = CefApp.getInstance(); - logger.info("CEF reused existing instance"); - } - - isInitialized.set(true); - } catch (IllegalStateException ex) { - handleInitializationError(ex); - } - } - - private static void handleConcurrentInitialization() { - try { - if (initLock.tryLock(3, TimeUnit.SECONDS)) { - try { - if (cefApp == null) { - performSafeInitialization(); - } - } finally { - initLock.unlock(); - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.warn("CEF initialization interrupted"); - } - } - - private static void handleInitializationError(IllegalStateException ex) { - if (ex.getMessage().contains("Settings can only be passed")) { - logger.warn("Settings conflict detected, recovering..."); - recoverFromSettingsConflict(); - } else if (ex.getMessage().contains("was terminated")) { - handleTerminatedState(ex); - } else { - logger.error("Critical CEF error", ex); - throw new RuntimeException("CEF initialization failed", ex); - } - } - - private static void recoverFromSettingsConflict() { - disposeLock.lock(); - try { - if (cefApp == null) { - cefApp = CefApp.getInstance(); - settingsApplied.set(true); - isInitialized.set(true); - logger.info("Recovered from settings conflict"); - } - } finally { - disposeLock.unlock(); - } - } - - private static void handleTerminatedState(IllegalStateException ex) { - disposeLock.lock(); - try { - logger.warn("CEF terminated state detected"); - dispose(false); - performEmergencyRecovery(); - } finally { - disposeLock.unlock(); - } - } - - private static void performEmergencyRecovery() { - try { - logger.info("Attempting emergency recovery..."); - CefApp.startup(new String[0]); - cefApp = CefApp.getInstance(); - isInitialized.set(true); - settingsApplied.set(true); - logger.info("Emergency recovery successful"); - } catch (Exception e) { - logger.error("Emergency recovery failed", e); - throw new RuntimeException("Unrecoverable CEF state", e); - } - } - - public static synchronized void dispose(boolean isShutdownHook) { - disposeLock.lock(); - try { - if (cefApp == null || isDisposing.get()) return; - - isDisposing.set(true); - try { - logger.info("Disposing CEF resources..."); - cefApp.dispose(); - - if (!isShutdownHook) { - cefApp = null; - isInitialized.set(false); - settingsApplied.set(false); - } - logger.info("CEF resources released"); - } catch (Exception e) { - logger.error("Disposal error", e); - } finally { - isDisposing.set(false); - } - } finally { - disposeLock.unlock(); - } - } - - private static void validateSubprocessPath(String path) { - File exeFile = new File(path); - if (!exeFile.exists()) { - String errorMsg = "JCEF helper executable missing: " + path; - logger.error(errorMsg); - throw new IllegalStateException(errorMsg); - } - logger.debug("Validated JCEF helper at: {}", path); - } - - // 状态查询接口 - public static String getInitStatus() { - return String.format("Initialized: %s, SettingsApplied: %s, Disposing: %s", - isInitialized.get(), settingsApplied.get(), isDisposing.get()); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/MainApplication.java b/src/main/java/com/chuangzhou/vivid2D/browser/MainApplication.java deleted file mode 100644 index 810f6c4..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/browser/MainApplication.java +++ /dev/null @@ -1,2002 +0,0 @@ -package com.chuangzhou.vivid2D.browser; - -import com.axis.innovators.box.AxisInnovatorsBox; -import com.axis.innovators.box.tools.FolderCreator; -import com.chuangzhou.vivid2D.browser.util.CodeExecutor; -import com.chuangzhou.vivid2D.browser.util.DatabaseConnectionManager; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import org.cef.browser.CefBrowser; -import org.cef.browser.CefFrame; -import org.cef.browser.CefMessageRouter; -import org.cef.callback.CefQueryCallback; -import org.cef.handler.CefMessageRouterHandlerAdapter; -import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.Value; -import org.json.JSONArray; -import org.json.JSONObject; -import org.tzd.lm.LM; - -import javax.swing.*; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.sql.*; -import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicReference; - -/** - * 这是一个简单的示例程序,用于展示如何使用JCEF来创建一个简单的浏览器窗口。 - * @author tzdwindows 7 - */ - -public class MainApplication { - private static final ExecutorService executor = Executors.newCachedThreadPool(); - private static long modelHandle; - private static long ctxHandle; - private static boolean isSystem = true; - public static void main(String[] args) { - AtomicReference window = new AtomicReference<>(); - WindowRegistry.getInstance().createNewWindow("main", builder -> - window.set(builder.title("Axis Innovators Box AI 工具箱") - .size(1280, 720) - .htmlUrl("https://www.bilibili.com/") - .openLinksInBrowser(true) - .operationHandler(createOperationHandler()) - .build()) - ); - } - - - /** - * 弹出AI窗口 - * @param parent 父窗口 - */ - public static void popupAIWindow(JFrame parent) { - LM.loadLibrary(LM.CUDA); - modelHandle = LM.llamaLoadModelFromFile(LM.DEEP_SEEK); - ctxHandle = LM.createContext(modelHandle); - - AtomicReference window = new AtomicReference<>(); - SwingUtilities.invokeLater(() -> { - WindowRegistry.getInstance().createNewChildWindow("main", builder -> - window.set(builder.title("Axis Innovators Box AI 工具箱") - .parentFrame(parent) - .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) - .size(1280, 720) - .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "AIaToolbox_dark.html") - .operationHandler(createOperationHandler()) - .build()) - ); - - CefMessageRouter msgRouter = window.get().getMsgRouter(); - if (msgRouter != null) { - msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { - @Override - public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, - String request, boolean persistent, CefQueryCallback callback) { - // 处理浏览器请求 - handleBrowserQuery(browser, request, callback); - return true; - } - - @Override - public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { - // 处理请求取消 - } - }, true); - } - - }); - } - - public static void popupCCodeEditorWindow() { - AtomicReference window = new AtomicReference<>(); - SwingUtilities.invokeLater(() -> { - WindowRegistry.getInstance().createNewWindow("main", builder -> - window.set(builder.title("TzdC 代码编辑器") - .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) - .size(1487, 836) - .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "CCodeEditor.html") - .operationHandler(createOperationHandler()) - .build()) - ); - - CefMessageRouter msgRouter = window.get().getMsgRouter(); - if (msgRouter != null) { - msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { - @Override - public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, - String request, boolean persistent, CefQueryCallback callback) { - try { - JSONObject requestJson = new JSONObject(request); - if ("executeCode".equals(requestJson.optString("type"))) { - String code = requestJson.optString("code"); - String language = requestJson.optString("language"); - - // 调用代码执行逻辑 - String result = CodeExecutor.executeCode(code, language,null); - - JSONObject response = new JSONObject(); - response.put("status", "success"); - response.put("output", result); - callback.success(response.toString()); - return true; - } - } catch (Exception e) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", e.getMessage()); - callback.failure(500, error.toString()); - } - return false; - } - - @Override - public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { - // 处理请求取消 - } - }, true); - } - }); - } - - public static void popupCodeEditorWindow() { - AtomicReference window = new AtomicReference<>(); - SwingUtilities.invokeLater(() -> { - WindowRegistry.getInstance().createNewWindow("main", builder -> - window.set(builder.title("代码编辑器") - .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) - .size(1487, 836) - .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "CodeEditor.html") - .operationHandler(createOperationHandler()) - .build()) - ); - - CefMessageRouter msgRouter = window.get().getMsgRouter(); - if (msgRouter != null) { - msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { - @Override - public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, - String request, boolean persistent, CefQueryCallback callback) { - try { - JSONObject requestJson = new JSONObject(request); - if ("executeCode".equals(requestJson.optString("type"))) { - String code = requestJson.optString("code"); - String language = requestJson.optString("language"); - - // 调用代码执行逻辑 - String result = executeCode(code, language); - - JSONObject response = new JSONObject(); - response.put("status", "success"); - response.put("output", result); - callback.success(response.toString()); - return true; - } - } catch (Exception e) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", e.getMessage()); - callback.failure(500, error.toString()); - } - return false; - } - - @Override - public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { - // 处理请求取消 - } - }, true); - } - }); - } - - public static String executeCode(String code, String language) { - return CodeExecutor.executeCode(code, language,new CodeExecutor.OutputListener() { - @Override - public void onOutput(String newOutput) {} - }); - } - - private static Value executeC(Context context, String code) { - return context.eval("c", code); - } - - /** - * 弹出html预览窗口 - */ - public static void popupHTMLWindow(String path) { - AtomicReference window = new AtomicReference<>(); - SwingUtilities.invokeLater(() -> { - WindowRegistry.getInstance().createNewWindow("main", builder -> - window.set(builder.title("Axis Innovators Box HTML查看器") - .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) - .size(1487, 836) - .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "HtmlViewer.html") - .operationHandler(createOperationHandler()) - .build()) - ); - - CefMessageRouter msgRouter = window.get().getMsgRouter(); - if (msgRouter != null) { - msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { - @Override - public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, - String request, boolean persistent, CefQueryCallback callback) { - try { - // 解析JSON请求 - JsonObject requestJson = JsonParser.parseString(request).getAsJsonObject(); - if (requestJson.has("type") && "loadInitialContent".equals(requestJson.get("type").getAsString())) { - - Path filePath = Paths.get(path); - - // 验证文件存在性 - if (!Files.exists(filePath)) { - callback.failure(404, "{\"code\":404,\"message\":\"文件未找到\"}"); - return true; - } - - // 读取文件内容 - String content = Files.readString(filePath, StandardCharsets.UTF_8); - callback.success(content); - return true; - } - } catch (Exception e) { - callback.failure(500, "{\"code\":500,\"message\":\"" + e.getMessage() + "\"}"); - } - return false; - } - - @Override - public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { - // 处理请求取消 - } - }, true); - } - }); - } - - private static void handleBrowserQuery(CefBrowser browser, String request, CefQueryCallback callback) { - try { - String[] parts = request.split(":", 3); - if (parts.length < 3) { - callback.failure(400, "请求格式错误"); - return; - } - - String operation = parts[0]; - String requestId = parts[1]; - String prompt = parts[2]; - - if ("ai-inference".equals(operation)) { - executor.execute(() -> { - if (isSystem) { - isSystem = false; - } - List messageList = new ArrayList<>(List.of()); - // 修改后的推理回调处理 - String jsCode = String.format( - "if (typeof updateResponse === 'function') {" + - " updateResponse('%s', '%s');" + - "}", - requestId, "

" + - "\\n" + - "推理内容" - ); - browser.executeJavaScript(jsCode, null, 0); - LM.inference(modelHandle, ctxHandle, 0.6f, prompt + "\n","", - new LM.MessageCallback() { - private boolean thinkingClosed = false; -// - @Override - public void onMessage(String message) { - messageList.add(message); - SwingUtilities.invokeLater(() -> { - // 统一转义处理 - String escaped = message - .replace("\\", "\\\\") - .replace("'", "\\'") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", "\\r"); -// - if (messageList.contains("") && !thinkingClosed) { - String endJs = String.format( - "if (typeof updateResponse === 'function') {" + - " updateResponse('%s', '%s');" + - "}", - requestId, "
" - ); - browser.executeJavaScript(endJs, null, 0); - thinkingClosed = true; - } -// - // 实时更新内容 - System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); -// - String jsCode = String.format( - "if (typeof updateResponse === 'function') {" + - " updateResponse('%s', '%s');" + - "}", - requestId, escaped - ); - browser.executeJavaScript(jsCode, null, 0); - }); - } - },isSystem); - messageList.clear(); - //jsCode = String.format( - // "if (typeof updateResponse === 'function') {" + - // " updateResponse('%s', '%s');" + - // "}", - // requestId, "嗯,用户问的是“请直接告诉我傅里叶变换公式”。首先,我需要回忆一下傅里叶变换的基本知识。傅里叶变换是将一个时间域的信号转换为频率域的信号,它在工程和科学研究中有着广泛的应用。\n\n接下来,我要确定傅里叶变换的数学表达式。标准形式应该是$$F(\\omega) = \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt$$。这里,$f(t)$是原函数,$e^{-i\\omega t}$是指数函数,$\\omega$是频率变量。\n\n然后,我需要考虑是否有其他形式的傅里叶变换,比如离散形式或逆变换。通常,离散傅里叶变换(DFT)使用$$X[k] = \\sum_{n=0}^{N-1} x[n] e^{-i2\\pi kn/N}$$来表示,而逆变换则是$$x[n] = \\frac{1}{N} \\sum_{k=0}^{N-1} X[k] e^{i2\\pi kn/N}$$。不过,用户的问题比较直接,可能只关注基本的连续形式。\n\n最后,我要确保回答准确无误,并且按照用户的格式要求使用标准的 LaTeX æ式来呈现。\n\n\n傅里叶变换的基本公式是:$$F(\\omega) = \\int_{-\\infty}^{\\infty} f(t) e^{-i\\omega t} dt$$".replace("\\", "\\\\") - // .replace("'", "\\'") - // .replace("\"", "\\\"") - // .replace("\n", "\\n") - // .replace("\r", "\\r") - //); - //browser.executeJavaScript(jsCode, null, 0); - callback.success("COMPLETED:" + requestId); - }); - } - } catch (Exception e) { - callback.failure(500, "服务器错误: " + e.getMessage()); - } - } - - private static WindowOperationHandler createOperationHandler() { - return new WindowOperationHandler.Builder() - .withDefaultOperations() - .build(); - } - - public static void popupDataBaseWindow() { - // 预加载常用 JDBC 驱动(警告级别,不阻塞 UI) - try { - try { Class.forName("org.h2.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.h2.Driver 未找到"); } - try { Class.forName("org.sqlite.JDBC"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.sqlite.JDBC 未找到"); } - try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.postgresql.Driver 未找到"); } - try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: com.mysql.cj.jdbc.Driver 未找到"); } - try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: oracle.jdbc.OracleDriver 未找到"); } - } catch (Throwable t) { - System.err.println("预加载 JDBC 驱动时发生异常: " + t.getMessage()); - } - - AtomicReference window = new AtomicReference<>(); - SwingUtilities.invokeLater(() -> { - WindowRegistry.getInstance().createNewWindow("main", builder -> - window.set(builder.title("Axis Innovators Box 数据库管理工具") - .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) - .size(1487, 836) - .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "DatabaseTool.html") - .operationHandler(createOperationHandler()) - .build()) - ); - - if (window.get() == null) { - System.err.println("popupDataBaseWindow: window 创建失败,window.get() == null"); - return; - } - - CefMessageRouter msgRouter = window.get().getMsgRouter(); - if (msgRouter != null) { - msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { - @Override - public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, - String request, boolean persistent, CefQueryCallback callback) { - try { - JSONObject requestJson = new JSONObject(request); - String type = requestJson.optString("type", ""); - - // 补默认端口与标准化 driver 名称 - String drv = requestJson.optString("driver", "").toLowerCase(); - if (!drv.isEmpty()) { - String port = requestJson.optString("port", "").trim(); - if (port.isEmpty()) { - switch (drv) { - case "mysql": - requestJson.put("port", "3306"); - break; - case "postgresql": - case "postgres": - requestJson.put("port", "5432"); - break; - case "oracle": - requestJson.put("port", "1521"); - break; - } - } - if ("postgres".equals(drv)) requestJson.put("driver", "postgresql"); - } - - // 安全标识符校验 pattern(表名/列名等仅允许常见字符) - final java.util.regex.Pattern SAFE_IDENT = java.util.regex.Pattern.compile("^[A-Za-z0-9_\\.\\$]+$"); - - switch (type) { - // 已有功能:继续使用现有处理函数(假定这些方法在类中定义) - case "connectDatabase": - handleDatabaseConnect(requestJson, callback); - break; - case "createLocalDatabase": - handleCreateLocalDatabase(requestJson, callback); - break; - case "disconnectDatabase": - handleDisconnectDatabase(requestJson, callback); - break; - case "executeQuery": - handleExecuteQuery(requestJson, callback); - break; - case "getTables": - handleGetTables(requestJson, callback); - break; - case "getTableData": - handleGetTableData(requestJson, callback); - break; - case "getTableStructure": - handleGetTableStructure(requestJson, callback); - break; - case "updateTheme": - handleUpdateTheme(requestJson, callback); - break; - case "getFonts": - handleGetFonts(requestJson, callback); - break; - - // 新增后端支持:analyzeQuery(EXPLAIN / EXPLAIN ANALYZE) - case "analyzeQuery": { - String connectionId = requestJson.optString("connectionId", ""); - String query = requestJson.optString("query", "").trim(); - if (connectionId.isEmpty() || query.isEmpty()) { - callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 或 query 为空").toString()); - break; - } - Connection conn = DatabaseConnectionManager.getConnection(connectionId); - if (conn == null || conn.isClosed()) { - callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); - break; - } - // 依据驱动选择 EXPLAIN 语法 - DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); - String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase()); - String explainSql = "EXPLAIN " + query; - if ("postgresql".equals(drvName)) { - explainSql = "EXPLAIN ANALYZE " + query; - } - try (Statement st = conn.createStatement(); - ResultSet rs = st.executeQuery(explainSql)) { - JSONArray out = new JSONArray(); - while (rs.next()) { - // EXPLAIN 输出常为单列文本 - out.put(rs.getString(1)); - } - JSONObject resp = new JSONObject(); - resp.put("status","success"); - resp.put("explain", out); - callback.success(resp.toString()); - } catch (SQLException ex) { - JSONObject err = new JSONObject(); - err.put("status","error"); - err.put("message","分析失败: " + ex.getMessage()); - callback.failure(500, err.toString()); - } - break; - } - - // exportData -> 导出为 CSV 或 JSON,写入用户目录下 .axis_innovators_box/exports/ - case "exportData": { - String connectionId = requestJson.optString("connectionId", ""); - String table = requestJson.optString("table", ""); - String format = requestJson.optString("format", "csv").toLowerCase(); - if (connectionId.isEmpty() || table.isEmpty()) { - callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 或 table 为空").toString()); - break; - } - if (!SAFE_IDENT.matcher(table).matches()) { - callback.failure(400, new JSONObject().put("status","error").put("message","非法表名").toString()); - break; - } - Connection conn = DatabaseConnectionManager.getConnection(connectionId); - if (conn == null || conn.isClosed()) { - callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); - break; - } - - Path exportDir = Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "exports"); - try { Files.createDirectories(exportDir); } catch (Exception e) { /* ignore */ } - - String filenameBase = table + "_" + System.currentTimeMillis(); - Path outPath = exportDir.resolve(filenameBase + (format.equals("json") ? ".json" : ".csv")); - - String query = "SELECT * FROM " + table; - try (Statement st = conn.createStatement(); - ResultSet rs = st.executeQuery(query)) { - - ResultSetMetaData md = rs.getMetaData(); - int cols = md.getColumnCount(); - - if ("json".equals(format)) { - JSONArray arr = new JSONArray(); - while (rs.next()) { - JSONObject obj = new JSONObject(); - for (int i = 1; i <= cols; i++) { - Object val = rs.getObject(i); - obj.put(md.getColumnLabel(i), val == null ? JSONObject.NULL : val); - } - arr.put(obj); - } - Files.write(outPath, arr.toString(2).getBytes(StandardCharsets.UTF_8)); - } else { - try (java.io.BufferedWriter writer = Files.newBufferedWriter( - outPath, - StandardCharsets.UTF_8 - )) { - // 写入 UTF-8 BOM - writer.write('\uFEFF'); - - for (int i = 1; i <= cols; i++) { - if (i > 1) writer.write(","); - writer.write("\"" + md.getColumnLabel(i).replace("\"", "\"\"") + "\""); - } - writer.write("\n"); - - while (rs.next()) { - for (int i = 1; i <= cols; i++) { - if (i > 1) writer.write(","); - Object val = rs.getObject(i); - String cell = val == null ? "" : String.valueOf(val); - writer.write("\"" + cell.replace("\"", "\"\"") + "\""); - } - writer.write("\n"); - } - } - - } - - try { - String pathStr = outPath.toAbsolutePath().toString(); - String os = System.getProperty("os.name").toLowerCase(); - if (os.contains("win")) { - new ProcessBuilder("explorer.exe", "/select," + pathStr).start(); - } - } catch (Exception ignore) { - } - - JSONObject resp = new JSONObject(); - resp.put("status","success"); - resp.put("path", outPath.toAbsolutePath().toString()); - resp.put("message","导出成功"); - callback.success(resp.toString()); - } catch (SQLException | java.io.IOException ex) { - JSONObject err = new JSONObject(); - err.put("status","error"); - err.put("message","导出失败: " + ex.getMessage()); - callback.failure(500, err.toString()); - } - break; - } - - // importCsv -> 从给定 path 导入 CSV,要求首行为列名且列名匹配表字段 - case "importCsv": { - String connectionId = requestJson.optString("connectionId", ""); - String table = requestJson.optString("table", ""); - String path = requestJson.optString("path", ""); - if (connectionId.isEmpty() || table.isEmpty() || path.isEmpty()) { - callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); - break; - } - if (!SAFE_IDENT.matcher(table).matches()) { - callback.failure(400, new JSONObject().put("status","error").put("message","非法表名").toString()); - break; - } - Path csvPath = Paths.get(path); - if (!Files.exists(csvPath)) { - callback.failure(400, new JSONObject().put("status","error").put("message","CSV 文件不存在").toString()); - break; - } - Connection conn = DatabaseConnectionManager.getConnection(connectionId); - if (conn == null || conn.isClosed()) { - callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); - break; - } - - // 读取首行作为列头 - try (java.io.BufferedReader br = Files.newBufferedReader(csvPath, StandardCharsets.UTF_8)) { - String headerLine = br.readLine(); - if (headerLine == null) { - callback.failure(400, new JSONObject().put("status","error").put("message","CSV 为空").toString()); - break; - } - // 简单 CSV 解析(支持双引号),但要求列名没有逗号内部双引号结构复杂情形 - String[] columns = headerLine.split(","); - for (int i = 0; i < columns.length; i++) { - columns[i] = columns[i].trim().replaceAll("^\"|\"$", ""); // 去掉可能的两端引号 - if (!SAFE_IDENT.matcher(columns[i]).matches()) { - callback.failure(400, new JSONObject().put("status","error").put("message","非法列名: " + columns[i]).toString()); - return true; - } - } - // 构建 INSERT SQL - StringBuilder placeholders = new StringBuilder(); - for (int i = 0; i < columns.length; i++) { - if (i > 0) placeholders.append(","); - placeholders.append("?"); - } - String insertSql = "INSERT INTO " + table + " (" + String.join(",", columns) + ") VALUES (" + placeholders.toString() + ")"; - conn.setAutoCommit(false); - int imported = 0; - try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) { - String line; - while ((line = br.readLine()) != null) { - // 简单分割(不处理复杂引号内部逗号) - String[] parts = line.split(",", -1); - for (int i = 0; i < columns.length; i++) { - String cell = i < parts.length ? parts[i].trim().replaceAll("^\"|\"$", "") : ""; - pstmt.setString(i + 1, cell.isEmpty() ? null : cell); - } - pstmt.addBatch(); - if (++imported % 500 == 0) pstmt.executeBatch(); - } - pstmt.executeBatch(); - conn.commit(); - } catch (SQLException ex) { - conn.rollback(); - throw ex; - } finally { - conn.setAutoCommit(true); - } - JSONObject resp = new JSONObject(); - resp.put("status","success"); - resp.put("imported", imported); - resp.put("message", "导入完成"); - callback.success(resp.toString()); - } catch (Exception ex) { - JSONObject err = new JSONObject(); - err.put("status","error"); - err.put("message","导入失败: " + ex.getMessage()); - callback.failure(500, err.toString()); - } - break; - } - - // generateEr -> 收集表、列信息并返回 JSON(前端可据此生成 ER 图) - case "generateEr": { - String connectionId = requestJson.optString("connectionId", ""); - if (connectionId.isEmpty()) { - callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString()); - break; - } - Connection conn = DatabaseConnectionManager.getConnection(connectionId); - if (conn == null || conn.isClosed()) { - callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); - break; - } - try { - DatabaseMetaData meta = conn.getMetaData(); - JSONArray tablesArr = new JSONArray(); - - try (ResultSet rsTables = meta.getTables(conn.getCatalog(), null, null, new String[]{"TABLE"})) { - while (rsTables.next()) { - String tbl = rsTables.getString("TABLE_NAME"); - JSONObject tObj = new JSONObject(); - tObj.put("name", tbl); - JSONArray cols = new JSONArray(); - try (ResultSet rsCols = meta.getColumns(conn.getCatalog(), null, tbl, null)) { - while (rsCols.next()) { - JSONObject c = new JSONObject(); - c.put("name", rsCols.getString("COLUMN_NAME")); - c.put("type", rsCols.getString("TYPE_NAME")); - c.put("size", rsCols.getInt("COLUMN_SIZE")); - c.put("nullable", rsCols.getInt("NULLABLE") == DatabaseMetaData.columnNullable); - cols.put(c); - } - } - tObj.put("columns", cols); - tablesArr.put(tObj); - } - } - - JSONObject resp = new JSONObject(); - resp.put("status","success"); - resp.put("er", new JSONObject().put("tables", new JSONArray(tablesArr.toString()))); - callback.success(resp.toString()); - } catch (SQLException ex) { - JSONObject err = new JSONObject(); - err.put("status","error"); - err.put("message","生成 ER 失败: " + ex.getMessage()); - callback.failure(500, err.toString()); - } - break; - } - - // analyzePerformance -> 尝试返回当前数据库会话/进程信息(简单实现,依据数据库类型) - case "analyzePerformance": { - String connectionId = requestJson.optString("connectionId", ""); - if (connectionId.isEmpty()) { - callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString()); - break; - } - Connection conn = DatabaseConnectionManager.getConnection(connectionId); - if (conn == null || conn.isClosed()) { - callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); - break; - } - DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); - String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase()); - try { - JSONArray out = new JSONArray(); - if ("postgresql".equals(drvName)) { - try (Statement st = conn.createStatement(); - ResultSet rs = st.executeQuery("SELECT pid, usename, state, query FROM pg_stat_activity LIMIT 50")) { - while (rs.next()) { - JSONObject r = new JSONObject(); - r.put("pid", rs.getObject("pid")); - r.put("user", rs.getString("usename")); - r.put("state", rs.getString("state")); - r.put("query", rs.getString("query")); - out.put(r); - } - } - } else if ("mysql".equals(drvName)) { - try (Statement st = conn.createStatement(); - ResultSet rs = st.executeQuery("SHOW PROCESSLIST")) { - while (rs.next()) { - JSONObject r = new JSONObject(); - r.put("Id", rs.getObject("Id")); - r.put("User", rs.getString("User")); - r.put("Host", rs.getString("Host")); - r.put("db", rs.getString("db")); - r.put("Command", rs.getString("Command")); - r.put("Time", rs.getString("Time")); - r.put("State", rs.getString("State")); - r.put("Info", rs.getString("Info")); - out.put(r); - } - } - } else { - // 通用替代:返回当前时间与简单连接信息 - JSONObject r = new JSONObject(); - r.put("now", java.time.Instant.now().toString()); - r.put("message","未实现针对该数据库的详细性能查询,返回通用信息"); - out.put(r); - } - JSONObject resp = new JSONObject(); - resp.put("status","success"); - resp.put("data", new JSONArray(out.toString())); - callback.success(resp.toString()); - } catch (SQLException ex) { - JSONObject err = new JSONObject(); - err.put("status","error"); - err.put("message","性能分析失败: " + ex.getMessage()); - callback.failure(500, err.toString()); - } - break; - } - - // listUsers -> 列出数据库用户(尝试常用查询) - case "listUsers": { - String connectionId = requestJson.optString("connectionId", ""); - if (connectionId.isEmpty()) { - callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString()); - break; - } - Connection conn = DatabaseConnectionManager.getConnection(connectionId); - if (conn == null || conn.isClosed()) { - callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); - break; - } - DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); - String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase()); - try { - JSONArray out = new JSONArray(); - if ("postgresql".equals(drvName)) { - try (Statement st = conn.createStatement(); - ResultSet rs = st.executeQuery("SELECT usename FROM pg_user")) { - while (rs.next()) { - out.put(rs.getString(1)); - } - } - } else if ("mysql".equals(drvName)) { - try (Statement st = conn.createStatement(); - ResultSet rs = st.executeQuery("SELECT User, Host FROM mysql.user")) { - while (rs.next()) { - JSONObject u = new JSONObject(); - u.put("user", rs.getString("User")); - u.put("host", rs.getString("Host")); - out.put(u); - } - } - } else { - // H2 / SQLite: 列出连接用户或简单返回空 - out.put("not_supported_for_db"); - } - JSONObject resp = new JSONObject(); - resp.put("status","success"); - resp.put("users", new JSONArray(out.toString())); - callback.success(resp.toString()); - } catch (SQLException ex) { - JSONObject err = new JSONObject(); - err.put("status","error"); - err.put("message","列出用户失败: " + ex.getMessage()); - callback.failure(500, err.toString()); - } - break; - } - case "insertRow": { - String connectionId = requestJson.optString("connectionId", ""); - String tableName = requestJson.optString("tableName", ""); - JSONObject rowData = requestJson.optJSONObject("rowData"); - - if (connectionId.isEmpty() || tableName.isEmpty() || rowData == null) { - callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); - break; - } - - Connection conn = DatabaseConnectionManager.getConnection(connectionId); - if (conn == null || conn.isClosed()) { - callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); - break; - } - - try { - // 构建INSERT语句 - StringBuilder columns = new StringBuilder(); - StringBuilder placeholders = new StringBuilder(); - List values = new ArrayList<>(); - - Iterator keys = rowData.keys(); - while (keys.hasNext()) { - String key = keys.next(); - if (columns.length() > 0) { - columns.append(", "); - placeholders.append(", "); - } - columns.append(key); - placeholders.append("?"); - values.add(rowData.get(key)); - } - - String sql = "INSERT INTO " + tableName + " (" + columns.toString() + ") VALUES (" + placeholders.toString() + ")"; - - try (PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { - for (int i = 0; i < values.size(); i++) { - pstmt.setObject(i + 1, values.get(i)); - } - - int affectedRows = pstmt.executeUpdate(); - - // 获取自增ID(如果有) - int newId = -1; - try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) { - if (generatedKeys.next()) { - newId = generatedKeys.getInt(1); - } - } - - JSONObject resp = new JSONObject(); - resp.put("status", "success"); - resp.put("affectedRows", affectedRows); - if (newId != -1) { - resp.put("newId", newId); - } - resp.put("message", "插入成功"); - callback.success(resp.toString()); - } - } catch (SQLException ex) { - JSONObject err = new JSONObject(); - err.put("status", "error"); - err.put("message", "插入失败: " + ex.getMessage()); - callback.failure(500, err.toString()); - } - break; - } - - case "updateRow": { - String connectionId = requestJson.optString("connectionId", ""); - String tableName = requestJson.optString("tableName", ""); - JSONObject originalData = requestJson.optJSONObject("originalData"); - JSONObject updatedData = requestJson.optJSONObject("updatedData"); - - if (connectionId.isEmpty() || tableName.isEmpty() || originalData == null || updatedData == null) { - callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); - break; - } - - Connection conn = DatabaseConnectionManager.getConnection(connectionId); - if (conn == null || conn.isClosed()) { - callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); - break; - } - - try { - // 构建UPDATE语句 - StringBuilder setClause = new StringBuilder(); - StringBuilder whereClause = new StringBuilder(); - List values = new ArrayList<>(); - - // SET部分 - Iterator updateKeys = updatedData.keys(); - while (updateKeys.hasNext()) { - String key = updateKeys.next(); - if (setClause.length() > 0) { - setClause.append(", "); - } - setClause.append(key).append(" = ?"); - values.add(updatedData.get(key)); - } - - // WHERE部分(使用原始数据识别要更新的行) - Iterator originalKeys = originalData.keys(); - while (originalKeys.hasNext()) { - String key = originalKeys.next(); - if (whereClause.length() > 0) { - whereClause.append(" AND "); - } - whereClause.append(key).append(" = ?"); - values.add(originalData.get(key)); - } - - String sql = "UPDATE " + tableName + " SET " + setClause.toString() + " WHERE " + whereClause.toString(); - - try (PreparedStatement pstmt = conn.prepareStatement(sql)) { - for (int i = 0; i < values.size(); i++) { - pstmt.setObject(i + 1, values.get(i)); - } - - int affectedRows = pstmt.executeUpdate(); - - JSONObject resp = new JSONObject(); - resp.put("status", "success"); - resp.put("affectedRows", affectedRows); - resp.put("message", "更新成功"); - callback.success(resp.toString()); - } - } catch (SQLException ex) { - JSONObject err = new JSONObject(); - err.put("status", "error"); - err.put("message", "更新失败: " + ex.getMessage()); - callback.failure(500, err.toString()); - } - break; - } - - case "deleteRow": { - String connectionId = requestJson.optString("connectionId", ""); - String tableName = requestJson.optString("tableName", ""); - JSONObject rowData = requestJson.optJSONObject("rowData"); - - if (connectionId.isEmpty() || tableName.isEmpty() || rowData == null) { - callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); - break; - } - - Connection conn = DatabaseConnectionManager.getConnection(connectionId); - if (conn == null || conn.isClosed()) { - callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); - break; - } - - try { - // 构建DELETE语句 - StringBuilder whereClause = new StringBuilder(); - List values = new ArrayList<>(); - - Iterator keys = rowData.keys(); - while (keys.hasNext()) { - String key = keys.next(); - if (whereClause.length() > 0) { - whereClause.append(" AND "); - } - whereClause.append(key).append(" = ?"); - values.add(rowData.get(key)); - } - - String sql = "DELETE FROM " + tableName + " WHERE " + whereClause.toString(); - - try (PreparedStatement pstmt = conn.prepareStatement(sql)) { - for (int i = 0; i < values.size(); i++) { - pstmt.setObject(i + 1, values.get(i)); - } - - int affectedRows = pstmt.executeUpdate(); - - JSONObject resp = new JSONObject(); - resp.put("status", "success"); - resp.put("affectedRows", affectedRows); - resp.put("message", "删除成功"); - callback.success(resp.toString()); - } - } catch (SQLException ex) { - JSONObject err = new JSONObject(); - err.put("status", "error"); - err.put("message", "删除失败: " + ex.getMessage()); - callback.failure(500, err.toString()); - } - break; - } - - case "createTable": { - String connectionId = requestJson.optString("connectionId", ""); - String tableName = requestJson.optString("tableName", ""); - JSONArray columns = requestJson.optJSONArray("columns"); - JSONObject tableOptions = requestJson.optJSONObject("tableOptions"); - JSONObject fileSettings = requestJson.optJSONObject("fileSettings"); - - if (connectionId.isEmpty() || tableName.isEmpty() || columns == null || columns.length() == 0) { - callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); - break; - } - - Connection conn = DatabaseConnectionManager.getConnection(connectionId); - if (conn == null || conn.isClosed()) { - callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); - break; - } - - try { - // 构建CREATE TABLE语句 - StringBuilder sql = new StringBuilder("CREATE TABLE "); - sql.append(tableName).append(" ("); - - // 添加列定义 - for (int i = 0; i < columns.length(); i++) { - JSONObject column = columns.getJSONObject(i); - String colName = column.optString("name", ""); - String colType = column.optString("type", "VARCHAR(255)"); - String colAttributes = column.optString("attributes", ""); - - if (colName.isEmpty()) { - callback.failure(400, new JSONObject().put("status","error").put("message","列名不能为空").toString()); - return true; - } - - if (i > 0) sql.append(", "); - sql.append(colName).append(" ").append(colType); - - if (!colAttributes.isEmpty()) { - sql.append(" ").append(colAttributes); - } - } - - sql.append(")"); - - // 添加表选项 - if (tableOptions != null) { - String engine = tableOptions.optString("engine", "InnoDB"); - String charset = tableOptions.optString("charset", "utf8mb4"); - String collation = tableOptions.optString("collation", "utf8mb4_unicode_ci"); - String comment = tableOptions.optString("comment", ""); - - sql.append(" ENGINE=").append(engine); - sql.append(" DEFAULT CHARSET=").append(charset); - sql.append(" COLLATE=").append(collation); - - if (!comment.isEmpty()) { - sql.append(" COMMENT='").append(comment.replace("'", "''")).append("'"); - } - } - - try (Statement stmt = conn.createStatement()) { - stmt.executeUpdate(sql.toString()); - - // 保存文件设置(如果提供了) - if (fileSettings != null) { - //saveFileSettings(connectionId, tableName, fileSettings); - } - - JSONObject resp = new JSONObject(); - resp.put("status", "success"); - resp.put("message", "表创建成功"); - callback.success(resp.toString()); - } - } catch (SQLException ex) { - JSONObject err = new JSONObject(); - err.put("status", "error"); - err.put("message", "创建表失败: " + ex.getMessage()); - callback.failure(500, err.toString()); - } - break; - } - - case "alterTable": { - String connectionId = requestJson.optString("connectionId", ""); - String tableName = requestJson.optString("tableName", ""); - String newTableName = requestJson.optString("newTableName", ""); - JSONArray columns = requestJson.optJSONArray("columns"); - JSONObject tableOptions = requestJson.optJSONObject("tableOptions"); - JSONObject fileSettings = requestJson.optJSONObject("fileSettings"); - - if (connectionId.isEmpty() || tableName.isEmpty() || columns == null || columns.length() == 0) { - callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); - break; - } - - Connection conn = DatabaseConnectionManager.getConnection(connectionId); - if (conn == null || conn.isClosed()) { - callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); - break; - } - - try { - // 这里简化处理:在实际应用中,需要更复杂的ALTER TABLE逻辑 - // 包括检测哪些列需要添加、修改或删除 - - // 首先获取当前表结构 - DatabaseMetaData meta = conn.getMetaData(); - List existingColumns = new ArrayList<>(); - try (ResultSet rs = meta.getColumns(conn.getCatalog(), null, tableName, null)) { - while (rs.next()) { - existingColumns.add(rs.getString("COLUMN_NAME")); - } - } - - // 构建ALTER TABLE语句(简化版) - // 注意:实际应用中需要更复杂的逻辑来处理不同的ALTER操作 - StringBuilder sql = new StringBuilder(); - - // 重命名表(如果需要) - if (!newTableName.isEmpty() && !newTableName.equals(tableName)) { - sql.append("ALTER TABLE ").append(tableName).append(" RENAME TO ").append(newTableName).append("; "); - tableName = newTableName; // 更新表名用于后续操作 - } - - // 这里简化处理:实际应用中需要更复杂的列变更逻辑 - // 可以添加新列,但不能删除或修改现有列(简化版) - for (int i = 0; i < columns.length(); i++) { - JSONObject column = columns.getJSONObject(i); - String colName = column.optString("name", ""); - String colType = column.optString("type", "VARCHAR(255)"); - String colAttributes = column.optString("attributes", ""); - - if (colName.isEmpty()) continue; - - if (!existingColumns.contains(colName)) { - // 添加新列 - if (sql.length() > 0 && !sql.toString().endsWith("; ")) { - sql.append("; "); - } - sql.append("ALTER TABLE ").append(tableName).append(" ADD COLUMN ") - .append(colName).append(" ").append(colType); - - if (!colAttributes.isEmpty()) { - sql.append(" ").append(colAttributes); - } - } - // 注意:在实际应用中,还需要处理修改和删除列的情况 - } - - // 执行ALTER语句 - if (sql.length() > 0) { - try (Statement stmt = conn.createStatement()) { - // 处理多条SQL语句 - String[] sqlStatements = sql.toString().split(";"); - for (String sqlStmt : sqlStatements) { - if (!sqlStmt.trim().isEmpty()) { - stmt.executeUpdate(sqlStmt.trim()); - } - } - } - } - - // 更新文件设置(如果提供了) - if (fileSettings != null) { - //saveFileSettings(connectionId, tableName, fileSettings); - } - - JSONObject resp = new JSONObject(); - resp.put("status", "success"); - resp.put("message", "表结构修改成功"); - callback.success(resp.toString()); - - } catch (SQLException ex) { - JSONObject err = new JSONObject(); - err.put("status", "error"); - err.put("message", "修改表结构失败: " + ex.getMessage()); - callback.failure(500, err.toString()); - } - break; - } -/* - case "uploadFile": { - String connectionId = requestJson.optString("connectionId", ""); - String tableName = requestJson.optString("tableName", ""); - String columnName = requestJson.optString("columnName", ""); - String fileName = requestJson.optString("fileName", ""); - long fileSize = requestJson.optLong("fileSize", 0); - - if (connectionId.isEmpty() || tableName.isEmpty() || columnName.isEmpty() || fileName.isEmpty()) { - callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); - break; - } - - // 在实际应用中,这里应该处理文件上传 - // 由于CEF的限制,文件上传可能需要通过其他方式处理 - // 这里提供一个模拟实现 - - try { - // 获取文件存储设置 - JSONObject fileSettings = getFileSettings(connectionId, tableName); - String storageType = fileSettings.optString("storageType", "filesystem"); - String storagePath = fileSettings.optString("storagePath", "./uploads"); - long maxFileSize = fileSettings.optLong("maxFileSize", 10 * 1024 * 1024); // 默认10MB - JSONArray allowedTypes = fileSettings.optJSONArray("allowedTypes"); - - // 检查文件大小 - if (fileSize > maxFileSize) { - callback.failure(400, new JSONObject().put("status","error") - .put("message","文件大小超过限制: " + (maxFileSize/1024/1024) + "MB").toString()); - break; - } - - // 检查文件类型 - if (allowedTypes != null && allowedTypes.length() > 0) { - String fileExt = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); - boolean allowed = false; - - // 简化文件类型检查 - for (int i = 0; i < allowedTypes.length(); i++) { - String type = allowedTypes.getString(i); - if (("image".equals(type) && isImageFile(fileExt)) || - ("document".equals(type) && isDocumentFile(fileExt)) || - ("audio".equals(type) && isAudioFile(fileExt)) || - ("video".equals(type) && isVideoFile(fileExt)) || - ("archive".equals(type) && isArchiveFile(fileExt))) { - allowed = true; - break; - } - } - - if (!allowed) { - callback.failure(400, new JSONObject().put("status","error") - .put("message","不允许的文件类型").toString()); - break; - } - } - - // 生成文件路径 - String fileId = "file_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 1000); - String fileExtension = fileName.substring(fileName.lastIndexOf('.')); - String filePath; - - if ("database".equals(storageType)) { - // 存储在数据库中 - 返回文件ID - filePath = fileId; - } else { - // 存储在文件系统中 - java.nio.file.Path uploadDir = java.nio.file.Paths.get(storagePath); - try { - java.nio.file.Files.createDirectories(uploadDir); - } catch (Exception e) { - // 忽略创建目录失败 - } - - filePath = uploadDir.resolve(fileId + fileExtension).toString(); - - // 在实际应用中,这里应该保存文件到指定路径 - // 由于CEF限制,这里只是模拟 - } - - JSONObject resp = new JSONObject(); - resp.put("status", "success"); - resp.put("fileId", fileId); - resp.put("path", filePath); - resp.put("message", "文件上传成功"); - callback.success(resp.toString()); - - } catch (Exception ex) { - JSONObject err = new JSONObject(); - err.put("status", "error"); - err.put("message", "文件上传失败: " + ex.getMessage()); - callback.failure(500, err.toString()); - } - break; - } -*/ - default: { - JSONObject err = new JSONObject(); - err.put("status", "error"); - err.put("message", "未知的操作类型: " + type); - callback.failure(400, err.toString()); - } - } - return true; - } catch (org.json.JSONException je) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", "请求解析失败: " + je.getMessage()); - callback.failure(400, error.toString()); - return true; - } catch (Exception e) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", e.getMessage() == null ? e.toString() : e.getMessage()); - callback.failure(500, error.toString()); - return true; - } - } - - @Override - public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { - // 查询取消,可选地记录日志 - } - }, true); - } else { - System.err.println("popupDataBaseWindow: msgRouter 为 null,消息路由无法注册"); - } - }); - } - - // 处理数据库连接 - private static void handleDatabaseConnect(JSONObject request, CefQueryCallback callback) { - try { - String driver = request.optString("driver", "mysql"); - String host = request.optString("host", "localhost"); - String port = request.optString("port", "3306"); - String database = request.optString("database", ""); - String username = request.optString("username", ""); - String password = request.optString("password", ""); - - // 验证必要参数 - if (database.isEmpty()) { - throw new IllegalArgumentException("数据库名不能为空"); - } - - // 建立真实数据库连接 - String connectionId = DatabaseConnectionManager.connect(driver, host, port, database, username, password); - - JSONObject response = new JSONObject(); - response.put("status", "success"); - response.put("message", "数据库连接成功"); - response.put("connectionId", connectionId); - - DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); - response.put("database", info.database); - response.put("driver", info.driver); - - callback.success(response.toString()); - - } catch (Exception e) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", "连接失败: " + e.getMessage()); - callback.failure(500, error.toString()); - } - } - - // 处理创建本地数据库 - private static void handleCreateLocalDatabase(JSONObject request, CefQueryCallback callback) { - try { - String driver = request.optString("driver", "sqlite"); - String dbName = request.optString("dbName", "my_database"); - - if (dbName.isEmpty()) { - throw new IllegalArgumentException("数据库名称不能为空"); - } - - // 创建本地数据库 - String connectionId = DatabaseConnectionManager.createLocalDatabase(driver, dbName); - - JSONObject response = new JSONObject(); - response.put("status", "success"); - response.put("message", "本地数据库创建成功"); - response.put("connectionId", connectionId); - response.put("database", dbName); - response.put("driver", driver); - - callback.success(response.toString()); - - } catch (Exception e) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", "创建数据库失败: " + e.getMessage()); - callback.failure(500, error.toString()); - } - } - - // 处理断开数据库连接 - private static void handleDisconnectDatabase(JSONObject request, CefQueryCallback callback) { - try { - String connectionId = request.optString("connectionId", ""); - - if (connectionId.isEmpty()) { - throw new IllegalArgumentException("连接ID不能为空"); - } - - DatabaseConnectionManager.disconnect(connectionId); - - JSONObject response = new JSONObject(); - response.put("status", "success"); - response.put("message", "数据库连接已断开"); - callback.success(response.toString()); - - } catch (Exception e) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", "断开连接失败: " + e.getMessage()); - callback.failure(500, error.toString()); - } - } - - // 处理SQL查询执行 - private static void handleExecuteQuery(JSONObject request, CefQueryCallback callback) { - Connection connection = null; - Statement statement = null; - ResultSet resultSet = null; - - try { - String query = request.optString("query", "").trim(); - String connectionId = request.optString("connectionId", ""); - - if (connectionId.isEmpty()) { - throw new IllegalArgumentException("连接ID不能为空"); - } - if (query.isEmpty()) { - throw new IllegalArgumentException("SQL查询不能为空"); - } - - connection = DatabaseConnectionManager.getConnection(connectionId); - if (connection == null || connection.isClosed()) { - throw new SQLException("数据库连接已断开或不存在"); - } - - long startTime = System.currentTimeMillis(); - - JSONObject response = new JSONObject(); - - // 判断查询类型 - boolean isSelect = query.toLowerCase().startsWith("select"); - boolean isUpdate = query.toLowerCase().startsWith("update") || - query.toLowerCase().startsWith("insert") || - query.toLowerCase().startsWith("delete"); - - if (isSelect) { - statement = connection.createStatement(); - resultSet = statement.executeQuery(query); - - // 获取元数据 - ResultSetMetaData metaData = resultSet.getMetaData(); - int columnCount = metaData.getColumnCount(); - - // 构建列信息 - JSONArray columns = new JSONArray(); - for (int i = 1; i <= columnCount; i++) { - columns.put(metaData.getColumnName(i)); - } - - // 构建数据 - JSONArray data = new JSONArray(); - int rowCount = 0; - while (resultSet.next()) { - JSONObject row = new JSONObject(); - for (int i = 1; i <= columnCount; i++) { - String columnName = metaData.getColumnName(i); - Object value = resultSet.getObject(i); - row.put(columnName, value != null ? value.toString() : null); - } - data.put(row); - rowCount++; - } - - long endTime = System.currentTimeMillis(); - double executionTime = (endTime - startTime) / 1000.0; - - response.put("status", "success"); - response.put("executionTime", String.format("%.3fs", executionTime)); - response.put("rowCount", rowCount); - response.put("columns", columns); - response.put("data", data); - - } else if (isUpdate) { - statement = connection.createStatement(); - int affectedRows = statement.executeUpdate(query); - - long endTime = System.currentTimeMillis(); - double executionTime = (endTime - startTime) / 1000.0; - - response.put("status", "success"); - response.put("executionTime", String.format("%.3fs", executionTime)); - response.put("affectedRows", affectedRows); - response.put("message", "操作成功,影响 " + affectedRows + " 行"); - - } else { - // 其他类型的查询(CREATE, DROP, ALTER等) - statement = connection.createStatement(); - boolean hasResults = statement.execute(query); - - long endTime = System.currentTimeMillis(); - double executionTime = (endTime - startTime) / 1000.0; - - response.put("status", "success"); - response.put("executionTime", String.format("%.3fs", executionTime)); - response.put("message", "查询执行成功"); - - if (hasResults) { - resultSet = statement.getResultSet(); - ResultSetMetaData metaData = resultSet.getMetaData(); - int columnCount = metaData.getColumnCount(); - - JSONArray columns = new JSONArray(); - for (int i = 1; i <= columnCount; i++) { - columns.put(metaData.getColumnName(i)); - } - - JSONArray data = new JSONArray(); - int rowCount = 0; - while (resultSet.next()) { - JSONObject row = new JSONObject(); - for (int i = 1; i <= columnCount; i++) { - String columnName = metaData.getColumnName(i); - Object value = resultSet.getObject(i); - row.put(columnName, value != null ? value.toString() : null); - } - data.put(row); - rowCount++; - } - - response.put("rowCount", rowCount); - response.put("columns", columns); - response.put("data", data); - } - } - - callback.success(response.toString()); - - } catch (Exception e) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", "查询执行失败: " + e.getMessage()); - callback.failure(500, error.toString()); - } finally { - // 关闭资源 - try { - if (resultSet != null) resultSet.close(); - if (statement != null) statement.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - } - - // 处理获取表列表 - private static void handleGetTables(JSONObject request, CefQueryCallback callback) { - Connection connection = null; - ResultSet resultSet = null; - - try { - String connectionId = request.optString("connectionId", ""); - - if (connectionId.isEmpty()) { - throw new IllegalArgumentException("连接ID不能为空"); - } - - connection = DatabaseConnectionManager.getConnection(connectionId); - if (connection == null || connection.isClosed()) { - throw new SQLException("数据库连接已断开或不存在"); - } - - DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); - JSONArray tables = new JSONArray(); - - // 根据数据库类型获取表信息 - String catalog = null; - String schema = null; - - switch (info.driver.toLowerCase()) { - case "mysql": - catalog = info.database; - break; - case "postgresql": - schema = "public"; - break; - case "sqlite": - case "h2": - // SQLite和H2不需要特定的catalog或schema - break; - } - - DatabaseMetaData metaData = connection.getMetaData(); - resultSet = metaData.getTables(catalog, schema, null, new String[]{"TABLE"}); - - while (resultSet.next()) { - String tableName = resultSet.getString("TABLE_NAME"); - String tableType = resultSet.getString("TABLE_TYPE"); - - // 获取表的行数 - int rowCount = 0; - try (Statement countStmt = connection.createStatement(); - ResultSet countRs = countStmt.executeQuery("SELECT COUNT(*) FROM " + tableName)) { - if (countRs.next()) { - rowCount = countRs.getInt(1); - } - } catch (SQLException e) { - // 如果无法获取行数,忽略错误 - } - - JSONObject table = new JSONObject(); - table.put("name", tableName); - table.put("type", tableType); - table.put("rows", rowCount); - tables.put(table); - } - - JSONObject response = new JSONObject(); - response.put("status", "success"); - response.put("tables", tables); - callback.success(response.toString()); - - } catch (Exception e) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", "获取表列表失败: " + e.getMessage()); - callback.failure(500, error.toString()); - } finally { - try { - if (resultSet != null) resultSet.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - } - - // 处理获取表数据 - private static void handleGetTableData(JSONObject request, CefQueryCallback callback) { - Connection connection = null; - Statement statement = null; - ResultSet resultSet = null; - - try { - String tableName = request.optString("tableName", ""); - String connectionId = request.optString("connectionId", ""); - int limit = request.optInt("limit", 50); - int offset = request.optInt("offset", 0); - - if (connectionId.isEmpty()) { - throw new IllegalArgumentException("连接ID不能为空"); - } - if (tableName.isEmpty()) { - throw new IllegalArgumentException("表名不能为空"); - } - - connection = DatabaseConnectionManager.getConnection(connectionId); - if (connection == null || connection.isClosed()) { - throw new SQLException("数据库连接已断开或不存在"); - } - - // 构建分页查询 - DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); - String query; - - switch (info.driver.toLowerCase()) { - case "mysql": - query = String.format("SELECT * FROM `%s` LIMIT %d OFFSET %d", tableName, limit, offset); - break; - case "postgresql": - query = String.format("SELECT * FROM \"%s\" LIMIT %d OFFSET %d", tableName, limit, offset); - break; - default: - query = String.format("SELECT * FROM %s LIMIT %d OFFSET %d", tableName, limit, offset); - } - - statement = connection.createStatement(); - resultSet = statement.executeQuery(query); - - // 获取元数据 - ResultSetMetaData metaData = resultSet.getMetaData(); - int columnCount = metaData.getColumnCount(); - - // 构建列信息 - JSONArray columns = new JSONArray(); - for (int i = 1; i <= columnCount; i++) { - columns.put(metaData.getColumnName(i)); - } - - // 构建数据 - JSONArray data = new JSONArray(); - while (resultSet.next()) { - JSONObject row = new JSONObject(); - for (int i = 1; i <= columnCount; i++) { - String columnName = metaData.getColumnName(i); - Object value = resultSet.getObject(i); - row.put(columnName, value != null ? value.toString() : null); - } - data.put(row); - } - - // 获取总行数 - int total = 0; - try (Statement countStmt = connection.createStatement(); - ResultSet countRs = countStmt.executeQuery("SELECT COUNT(*) FROM " + tableName)) { - if (countRs.next()) { - total = countRs.getInt(1); - } - } catch (SQLException e) { - // 如果无法获取总行数,使用当前数据行数 - total = data.length(); - } - - JSONObject response = new JSONObject(); - response.put("status", "success"); - response.put("tableName", tableName); - response.put("columns", columns); - response.put("data", data); - response.put("total", total); - response.put("offset", offset); - response.put("limit", limit); - - callback.success(response.toString()); - - } catch (Exception e) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", "获取表数据失败: " + e.getMessage()); - callback.failure(500, error.toString()); - } finally { - try { - if (resultSet != null) resultSet.close(); - if (statement != null) statement.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - } - - // 处理获取表结构 - private static void handleGetTableStructure(JSONObject request, CefQueryCallback callback) { - Connection connection = null; - ResultSet rs = null; - ResultSet idxRs = null; - ResultSet pkRs = null; - ResultSet fkRs = null; - PreparedStatement ps = null; - - try { - String tableName = request.optString("tableName", ""); - String connectionId = request.optString("connectionId", ""); - - if (connectionId.isEmpty()) { - throw new IllegalArgumentException("连接ID不能为空"); - } - if (tableName.isEmpty()) { - throw new IllegalArgumentException("表名不能为空"); - } - - connection = DatabaseConnectionManager.getConnection(connectionId); - if (connection == null || connection.isClosed()) { - throw new SQLException("数据库连接已断开或不存在"); - } - - DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); - DatabaseMetaData metaData = connection.getMetaData(); - - String catalog = null; - String schema = null; - - if (info != null && info.driver != null) { - switch (info.driver.toLowerCase()) { - case "mysql": - catalog = info.database; - break; - case "postgresql": - schema = "public"; - break; - default: - // leave null - } - } - - // 读取列信息 - rs = metaData.getColumns(catalog, schema, tableName, null); - JSONArray columns = new JSONArray(); - while (rs.next()) { - JSONObject column = new JSONObject(); - - String rawType = rs.getString("TYPE_NAME"); // eg. VARCHAR, INT, DECIMAL - int colSize = rs.getInt("COLUMN_SIZE"); - int decimalDigits = 0; - try { decimalDigits = rs.getInt("DECIMAL_DIGITS"); } catch (Exception ignored) {} - - // 生成更友好的类型展示(匹配前端类型选项) - String displayType = rawType != null ? rawType : ""; - if (rawType != null) { - String rt = rawType.toUpperCase(); - if (rt.equals("VARCHAR") || rt.equals("CHAR")) { - displayType = rt + "(" + colSize + ")"; - } else if (rt.equals("DECIMAL") || rt.equals("NUMERIC")) { - displayType = "DECIMAL(" + colSize + "," + decimalDigits + ")"; - } else if (rt.equals("DOUBLE") || rt.equals("FLOAT")) { - // 保持为原始类型 - displayType = rt; - } else { - // 有时候 TYPE_NAME 已经包含长度(如 VARCHAR(255)),保留原样 - displayType = rawType; - } - } - - column.put("name", rs.getString("COLUMN_NAME")); - column.put("type", displayType); - column.put("rawType", rawType); - column.put("size", colSize); - column.put("decimalDigits", decimalDigits); - int nullableFlag = rs.getInt("NULLABLE"); - column.put("nullable", nullableFlag == DatabaseMetaData.columnNullable); - column.put("defaultValue", rs.getString("COLUMN_DEF")); - // 尝试获取是否自增(部分驱动返回 IS_AUTOINCREMENT) - try { - String isAuto = rs.getString("IS_AUTOINCREMENT"); - column.put("autoIncrement", "YES".equalsIgnoreCase(isAuto)); - } catch (Exception ignored) { - // ignore - } - - columns.put(column); - } - if (rs != null) { rs.close(); rs = null; } - - // 读取主键(用于在 constraints 中显示) - JSONArray constraints = new JSONArray(); - pkRs = metaData.getPrimaryKeys(catalog, schema, tableName); - List pkColumns = new ArrayList<>(); - while (pkRs != null && pkRs.next()) { - String pkName = pkRs.getString("PK_NAME"); - String pkCol = pkRs.getString("COLUMN_NAME"); - if (pkCol != null) pkColumns.add(pkCol); - } - if (!pkColumns.isEmpty()) { - JSONObject pkObj = new JSONObject(); - pkObj.put("name", "PRIMARY"); - pkObj.put("type", "PRIMARY KEY"); - pkObj.put("definition", "PRIMARY KEY (" + String.join(",", pkColumns) + ")"); - constraints.put(pkObj); - } - if (pkRs != null) { pkRs.close(); pkRs = null; } - - // 读取外键 - fkRs = metaData.getImportedKeys(catalog, schema, tableName); - while (fkRs != null && fkRs.next()) { - String fkName = fkRs.getString("FK_NAME"); - String fkCol = fkRs.getString("FKCOLUMN_NAME"); - String pkTable = fkRs.getString("PKTABLE_NAME"); - String pkCol = fkRs.getString("PKCOLUMN_NAME"); - JSONObject fkObj = new JSONObject(); - fkObj.put("name", fkName == null ? ("fk_" + fkCol) : fkName); - fkObj.put("type", "FOREIGN KEY"); - fkObj.put("definition", String.format("FOREIGN KEY (%s) REFERENCES %s(%s)", fkCol, pkTable, pkCol)); - constraints.put(fkObj); - } - if (fkRs != null) { fkRs.close(); fkRs = null; } - - // 读取索引信息(不重复合并同名索引的列) - idxRs = metaData.getIndexInfo(catalog, schema, tableName, false, false); - Map idxMap = new LinkedHashMap<>(); - while (idxRs != null && idxRs.next()) { - String idxName = idxRs.getString("INDEX_NAME"); - if (idxName == null) continue; // driver-specific - boolean nonUnique = idxRs.getBoolean("NON_UNIQUE"); - String colName = idxRs.getString("COLUMN_NAME"); - if (!idxMap.containsKey(idxName)) { - JSONObject idxObj = new JSONObject(); - idxObj.put("name", idxName); - idxObj.put("type", nonUnique ? "INDEX" : "UNIQUE"); - idxObj.put("columns", colName == null ? "" : colName); - idxMap.put(idxName, idxObj); - } else { - JSONObject idxObj = idxMap.get(idxName); - String prevCols = idxObj.optString("columns", ""); - if (colName != null && !prevCols.contains(colName)) { - if (prevCols.isEmpty()) idxObj.put("columns", colName); - else idxObj.put("columns", prevCols + "," + colName); - } - } - } - JSONArray indexes = new JSONArray(); - for (JSONObject v : idxMap.values()) indexes.put(v); - if (idxRs != null) { idxRs.close(); idxRs = null; } - - // 表级信息(MySQL: ENGINE, COLLATION, COMMENT) - String engine = null; - String collation = null; - String charset = null; - String tableComment = null; - if (info != null && info.driver != null && "mysql".equalsIgnoreCase(info.driver) && info.database != null) { - String sql = "SELECT ENGINE, TABLE_COLLATION, TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?"; - try { - ps = connection.prepareStatement(sql); - ps.setString(1, info.database); - ps.setString(2, tableName); - ResultSet tRs = ps.executeQuery(); - if (tRs.next()) { - engine = tRs.getString("ENGINE"); - collation = tRs.getString("TABLE_COLLATION"); - tableComment = tRs.getString("TABLE_COMMENT"); - if (collation != null && collation.contains("_")) { - charset = collation.split("_")[0]; - } - } - if (tRs != null) { tRs.close(); tRs = null; } - } catch (Exception ignored) { - // 忽略信息 schema 查询失败的情况(可能权限或 driver 不支持) - } finally { - if (ps != null) { try { ps.close(); } catch (SQLException ignored) {} ps = null; } - } - } - - JSONObject response = new JSONObject(); - response.put("status", "success"); - response.put("tableName", tableName); - response.put("engine", engine == null ? JSONObject.NULL : engine); - response.put("charset", charset == null ? JSONObject.NULL : charset); - response.put("collation", collation == null ? JSONObject.NULL : collation); - response.put("comment", tableComment == null ? JSONObject.NULL : tableComment); - response.put("columns", columns); - response.put("indexes", indexes); - response.put("constraints", constraints); - - callback.success(response.toString()); - - } catch (Exception e) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", "获取表结构失败: " + e.getMessage()); - callback.failure(500, error.toString()); - } finally { - try { if (rs != null) rs.close(); } catch (SQLException ignored) {} - try { if (idxRs != null) idxRs.close(); } catch (SQLException ignored) {} - try { if (pkRs != null) pkRs.close(); } catch (SQLException ignored) {} - try { if (fkRs != null) fkRs.close(); } catch (SQLException ignored) {} - try { if (ps != null) ps.close(); } catch (SQLException ignored) {} - } - } - - - // 处理主题更新(保持不变) - // 这里自己实现了一个一般使用事件实现 - private static void handleUpdateTheme(JSONObject request, CefQueryCallback callback) { - try { - // 通过 AxisInnovatorsBox 获取当前主题状态 - boolean isDarkMode = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode(); - String theme = isDarkMode ? "dark" : "light"; - - JSONObject response = new JSONObject(); - response.put("status", "success"); - response.put("theme", theme); - response.put("message", "当前主题: " + theme); - callback.success(response.toString()); - - } catch (Exception e) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", "获取主题失败: " + e.getMessage()); - callback.failure(500, error.toString()); - } - } - - // 处理字体获取(保持不变) - private static void handleGetFonts(JSONObject request, CefQueryCallback callback) { - try { - JSONArray fonts = new JSONArray(); - String[] fontList = { - "Segoe UI", "Microsoft YaHei", "SimSun", "Arial", - "Helvetica", "Times New Roman", "Courier New", "Verdana" - }; - - for (String font : fontList) { - fonts.put(font); - } - - JSONObject response = new JSONObject(); - response.put("status", "success"); - response.put("fonts", fonts); - callback.success(response.toString()); - - } catch (Exception e) { - JSONObject error = new JSONObject(); - error.put("status", "error"); - error.put("message", "获取字体失败: " + e.getMessage()); - callback.failure(500, error.toString()); - } - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/WindowOperation.java b/src/main/java/com/chuangzhou/vivid2D/browser/WindowOperation.java deleted file mode 100644 index d68af1b..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/browser/WindowOperation.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.chuangzhou.vivid2D.browser; - -import org.cef.callback.CefQueryCallback; - -/** - * @author tzdwindows 7 - */ -public record WindowOperation(String type, String targetWindow, CefQueryCallback callback) { -} diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/WindowOperationHandler.java b/src/main/java/com/chuangzhou/vivid2D/browser/WindowOperationHandler.java deleted file mode 100644 index c55a1b0..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/browser/WindowOperationHandler.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.chuangzhou.vivid2D.browser; - -import javax.swing.*; -import java.awt.*; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; - -/** - * @author tzdwindows 7 - */ -public class WindowOperationHandler { - private final WindowRegistry registry; - private final Map> operations; - private final Component attachedComponent; - - public static class Builder { - private WindowRegistry registry = WindowRegistry.getInstance(); - private final Map> operations = new ConcurrentHashMap<>(); - private Component attachedComponent; - - public Builder attachTo(Component component) { - this.attachedComponent = component; - return this; - } - public Builder withDefaultOperations() { - this.operations.put("open", target -> { - registry.getWindow(target).setVisible(true); - }); - this.operations.put("close", target -> { - if (target != null) { - System.out.println("Close window: " + target); - registry.unregisterWindow(target); - } else if (attachedComponent != null) { - Window window = SwingUtilities.getWindowAncestor(attachedComponent); - if (window instanceof BrowserWindow) { - ((BrowserWindow) window).closeWindow(); - } - } - }); - return this; - } - - public Builder onOperation(String operation, Consumer handler) { - this.operations.put(operation, handler); - return this; - } - - public WindowOperationHandler build() { - return new WindowOperationHandler(this); - } - - private void handleOpen(String targetWindow) { - registry.createNewWindow(targetWindow, builder -> - builder.title("New Window") - ); - } - - private void handleClose(String targetWindow) { - if (targetWindow != null) { - registry.unregisterWindow(targetWindow); - } else { - handleCurrentWindowClose(); - } - } - - private void handleCurrentWindowClose() { - if (attachedComponent != null) { - BrowserWindow currentWindow = (BrowserWindow) - SwingUtilities.getWindowAncestor(attachedComponent); - if (currentWindow != null) { - registry.unregisterWindow(currentWindow.getWindowId()); - } - } - } - - } - - private WindowOperationHandler(Builder builder) { - this.registry = builder.registry; - this.attachedComponent = builder.attachedComponent; - this.operations = new ConcurrentHashMap<>(builder.operations); - } - - public void handleOperation(WindowOperation operation) { - Consumer handler = operations.get(operation.type()); - if (handler != null) { - handler.accept(operation.targetWindow()); - operation.callback().success("操作成功: " + operation.type()); - } else { - operation.callback().failure(-1, "未定义的操作: " + operation.type()); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/WindowRegistry.java b/src/main/java/com/chuangzhou/vivid2D/browser/WindowRegistry.java deleted file mode 100644 index f0a6ae5..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/browser/WindowRegistry.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.chuangzhou.vivid2D.browser; - -import com.axis.innovators.box.tools.FolderCreator; -import org.cef.browser.CefBrowser; -import org.cef.browser.CefFrame; -import org.cef.handler.CefLoadHandlerAdapter; -import org.json.JSONObject; - -import java.io.File; -import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.function.Consumer; - -public class WindowRegistry { - private static WindowRegistry instance; - private final ConcurrentMap windows = - new ConcurrentHashMap<>(); - private final ConcurrentMap childWindows = - new ConcurrentHashMap<>(); - private final Map> callbacks = new ConcurrentHashMap<>(); - - private WindowRegistry() {} - - public static synchronized WindowRegistry getInstance() { - if (instance == null) { - instance = new WindowRegistry(); - } - return instance; - } - - public void registerWindow(BrowserWindow window) { - windows.put(window.getWindowId(), window); - } - - public void registerChildWindow(BrowserWindowJDialog window) { - childWindows.put(window.getWindowId(), window); - } - - public void registerCallback(String requestId, Consumer handler) { - callbacks.put(requestId, handler); - } - - public Consumer getCallback(String requestId) { - return callbacks.remove(requestId); - } - - public void unregisterWindow(String windowId) { - BrowserWindow window = windows.remove(windowId); - if (window != null) { - window.closeWindow(); - } - } - - public BrowserWindow getWindow(String windowId) { - return windows.get(windowId); - } - - public void update() { - for (BrowserWindow window : windows.values()) { - if (window != null) { - window.updateTheme(); - } - } - - for (BrowserWindowJDialog window : childWindows.values()) { - if (window != null) { - window.updateTheme(); - } - } - } - - /** - * 创建一个新的窗口 - * @param windowId 窗口ID - * @param config 窗口配置 - */ - public void createNewWindow(String windowId, Consumer config) { - BrowserWindow.Builder builder = new BrowserWindow.Builder(windowId); - config.accept(builder); - BrowserWindow window = builder.build(); - registerWindow(window); - - loadExtLibsPath(window); - } - - /** - * 创建一个新的子窗口 - * @param windowId 窗口ID - * @param config 窗口配置 - */ - public void createNewChildWindow(String windowId, Consumer config) { - BrowserWindowJDialog.Builder builder = new BrowserWindowJDialog.Builder(windowId); - config.accept(builder); - BrowserWindowJDialog window = builder.build(); - registerChildWindow(window); - - loadExtLibsPath(window); - } - - private void loadExtLibsPath(BrowserWindow window) { - CefBrowser cefBrowser = window.getBrowser(); - - if (cefBrowser != null) - - // 使用 CefClient 的调度方法(如果可用)或直接添加 LoadHandler - cefBrowser.getClient().addLoadHandler(new CefLoadHandlerAdapter() { - @Override - public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { - if (frame.isMain()) { - try { - String extLibsPath = FolderCreator.getJavaScriptFolder() + "\\" + "extLibs"; - File extLibsDir = new File(extLibsPath); - if (!extLibsDir.exists() || !extLibsDir.isDirectory()) { - throw new IOException("extLibs目录无效: " + extLibsPath); - } - String script = "window.extLibsPath = " + JSONObject.valueToString(extLibsPath) + ";"; - browser.executeJavaScript(script, frame.getURL(), 0); - } catch (Exception e) { - System.err.println("注入extLibsPath失败: " + e.getMessage()); - e.printStackTrace(); - } - } - } - }); - } - private void loadExtLibsPath(BrowserWindowJDialog window) { - CefBrowser cefBrowser = window.getBrowser(); - - if (cefBrowser != null) - // 使用 CefClient 的调度方法(如果可用)或直接添加 LoadHandler - cefBrowser.getClient().addLoadHandler(new CefLoadHandlerAdapter() { - @Override - public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { - if (frame.isMain()) { - try { - String extLibsPath = FolderCreator.getJavaScriptFolder() + "\\" + "extLibs"; - File extLibsDir = new File(extLibsPath); - if (!extLibsDir.exists() || !extLibsDir.isDirectory()) { - throw new IOException("extLibs目录无效: " + extLibsPath); - } - String script = "window.extLibsPath = " + JSONObject.valueToString(extLibsPath) + ";"; - browser.executeJavaScript(script, frame.getURL(), 0); - } catch (Exception e) { - System.err.println("注入extLibsPath失败: " + e.getMessage()); - e.printStackTrace(); - } - } - } - }); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/util/CodeExecutor.java b/src/main/java/com/chuangzhou/vivid2D/browser/util/CodeExecutor.java deleted file mode 100644 index 60232f7..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/browser/util/CodeExecutor.java +++ /dev/null @@ -1,346 +0,0 @@ -package com.chuangzhou.vivid2D.browser.util; - -import javax.tools.JavaCompiler; -import javax.tools.StandardJavaFileManager; -import javax.tools.ToolProvider; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -public class CodeExecutor { - // 用于捕获输出的回调接口 - public interface OutputListener { - void onOutput(String newOutput); - } - - /** - * 执行代码 - * @param code 代码字符串 - * @param language 代码类型 - * @param listener 回调,代码的输出回调 - * @return 返回i执行结果 - */ - public static String executeCode(String code, String language, OutputListener listener) { - switch (language.toLowerCase()) { - case "python": - return executePythonNative(code, listener); - case "c": - case "cpp": - return executeC(code, listener); - case "java": - return executeJavaCode(code, listener); - default: - return "不支持的语言类型: " + language; - } - } - - /** - * 执行Java代码 - * @return 返回执行结果 - */ - public static String executeJavaCode(String code, OutputListener listener) { - Path tempDir = null; - try { - // ===== 1. 创建临时目录 ===== - tempDir = Files.createTempDirectory("javaCode"); - - // ===== 2. 写入Java源文件(强制UTF-8)===== - Path javaFile = tempDir.resolve("Main.java"); - Files.writeString(javaFile, - code, - StandardCharsets.UTF_8 - ); - - // ===== 3. 编译时指定编码 ===== - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8); - - List options = new ArrayList<>(); - options.add("-encoding"); - options.add("UTF-8"); - options.add("-d"); - options.add(tempDir.toString()); - - JavaCompiler.CompilationTask task = compiler.getTask( - null, - fileManager, - null, - options, - null, - fileManager.getJavaFileObjects(javaFile) - ); - - if (!task.call()) { - return "编译失败"; - } - - // ===== 4. 执行配置 ===== - String javaExe = Path.of(System.getProperty("java.home"), "bin", "java").toString(); - ProcessBuilder pb = new ProcessBuilder( - javaExe, - "-Dfile.encoding=UTF-8", - "-Dsun.stdout.encoding=UTF-8", // 针对OpenJDK的特殊设置 - "-Dsun.stderr.encoding=UTF-8", - "-cp", - tempDir.toString(), - "Main" - ); - - // ===== 5. 设置环境变量 ===== - Map env = pb.environment(); - env.put("JAVA_TOOL_OPTIONS", "-Dfile.encoding=UTF-8"); - env.put("LANG", "en_US.UTF-8"); // Linux/macOS - env.put("LC_ALL", "en_US.UTF-8"); - - // ===== 6. 输出处理 ===== - pb.redirectErrorStream(true); - Process process = pb.start(); - - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - - StringBuilder output = new StringBuilder(); - char[] buffer = new char[4096]; - int charsRead; - - while ((charsRead = reader.read(buffer)) != -1) { - String chunk = new String(buffer, 0, charsRead); - output.append(chunk); - if (listener != null) { - // 处理控制台编码转换(Windows专用) - if (System.getProperty("os.name").startsWith("Windows")) { - chunk = new String(chunk.getBytes(StandardCharsets.UTF_8), "GBK"); - } - listener.onOutput(chunk); - } - } - - int exitCode = process.waitFor(); - return output.toString() + "\n退出码: " + exitCode; - } - - } catch (Exception e) { - return "执行错误: " + e.getMessage(); - } finally { - // 清理代码... - } - } - - - /** - * 需要用户安装python环境 - */ - private static String executePythonNative(String code, OutputListener listener) { - try { - Path pythonFile = Files.createTempFile("script_", ".py"); - Files.writeString(pythonFile, - "# -*- coding: utf-8 -*-\n" + code, - StandardCharsets.UTF_8 - ); - - ProcessBuilder pb = new ProcessBuilder("python", pythonFile.toString()) - .redirectErrorStream(true); - - Map env = pb.environment(); - env.put("PYTHONIOENCODING", "UTF-8"); - env.put("PYTHONUTF8", "1"); - - Process process = pb.start(); - - StringBuilder output = new StringBuilder(); - Thread outputThread = new Thread(() -> { - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - String finalLine = line + "\n"; - output.append(finalLine); - if (listener != null) { - listener.onOutput(finalLine); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - }); - outputThread.start(); - - // 等待执行完成 - int exitCode = process.waitFor(); - outputThread.join(); - - // 清理文件 - Files.deleteIfExists(pythonFile); - - return String.format("退出码: %d\n输出内容:\n%s", exitCode, output); - - } catch (Exception e) { - return "执行错误: " + e.getMessage(); - } - } - - /** - * 执行C代码 - */ - private static String executeC(String code, OutputListener listener) { - Path tempDir = null; - Path cFile = null; - Path exeFile = null; - try { - // 创建临时工作目录 - tempDir = Files.createTempDirectory("c_compile_"); - - // 生成C源代码文件 - cFile = tempDir.resolve("program.c"); - Files.writeString(cFile, code, StandardCharsets.UTF_8); - - // 生成可执行文件路径 - exeFile = tempDir.resolve("program.exe"); - - // 1. 编译代码 ------------------------------------------------- - String tccPath =System.getProperty("user.dir") + "/library/tcc/tcc.exe"; - - Process compileProcess; - - if (listener != null) { - compileProcess = new ProcessBuilder( - tccPath, - "-o", exeFile.toString(), - cFile.toString() - ) - .directory(tempDir.toFile()) - .redirectErrorStream(true) - .start(); - - // 捕获编译输出 - StringBuilder compileOutput = new StringBuilder(); - Thread compileOutputThread = new Thread(() -> { - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(compileProcess.getInputStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - compileOutput.append(line).append("\n"); - if (listener != null) { - listener.onOutput("[编译输出] " + line + "\n"); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - }); - compileOutputThread.start(); - - // 等待编译完成 - int compileExitCode = compileProcess.waitFor(); - compileOutputThread.join(1000); - - if (compileExitCode != 0) { - return "编译失败:\n" + compileOutput; - } - - // 2. 执行程序 ------------------------------------------------- - Process executeProcess = new ProcessBuilder(exeFile.toString()) - .directory(tempDir.toFile()) - .redirectErrorStream(true) - .start(); - - // 实时输出处理 - AtomicReference execOutput = new AtomicReference<>(new StringBuilder()); - Thread executeOutputThread = new Thread(() -> { - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(executeProcess.getInputStream(), StandardCharsets.UTF_8))) { - char[] buffer = new char[1024]; - int charsRead; - while ((charsRead = reader.read(buffer)) != -1) { - String outputChunk = new String(buffer, 0, charsRead); - execOutput.get().append(outputChunk); - if (listener != null) { - listener.onOutput(outputChunk); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - }); - executeOutputThread.start(); - - // 等待执行完成(最多10秒) - boolean finished = executeProcess.waitFor(10, TimeUnit.SECONDS); - executeOutputThread.join(1000); - - if (!finished) { - executeProcess.destroyForcibly(); - return "执行超时\n部分输出:\n" + execOutput.get(); - } - - // 获取最终输出 - String finalOutput = execOutput.get().toString(); - int exitCode = executeProcess.exitValue(); - return String.format("执行结果: %s\n退出码: %d\n输出内容:\n%s", - exitCode == 0 ? "成功" : "失败", - exitCode, - finalOutput); - } else { - new ProcessBuilder( - tccPath, - "-o", exeFile.toString(), - cFile.toString() - ) - .directory(tempDir.toFile()) - .redirectErrorStream(true) - .start(); - - new ProcessBuilder( - "cmd.exe", - "/c", - "start", - "\"Tzd输出窗口\"", - "cmd.exe", - "/K", - "chcp 65001 & ", - exeFile.toString() - ).start(); - return String.format("执行结果: %s\n退出码: %d\n输出内容:\n%s", - "成功", - 0, - ""); - } - - } catch (Exception e) { - return "执行错误: " + e.getMessage(); - } finally { - // 清理临时文件 - try { - if (listener != null){ - if (cFile != null) Files.deleteIfExists(cFile); - if (exeFile != null) Files.deleteIfExists(exeFile); - if (tempDir != null) Files.deleteIfExists(tempDir); - } - } catch (IOException e) { - System.err.println("临时文件清理失败: " + e.getMessage()); - } - } - } - - - // 使用方法 - public static void main(String[] args) { - String pythonCode = "#include \n" + - "\n" + - "int main() {\n" + - " while (1){\n" + - " printf(\"Hello World\\n\");\n" + - "}\n" + - " return 0;\n" + - "}"; - executeCode(pythonCode, "c", null); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/browser/util/DatabaseConnectionManager.java b/src/main/java/com/chuangzhou/vivid2D/browser/util/DatabaseConnectionManager.java deleted file mode 100644 index 57796ac..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/browser/util/DatabaseConnectionManager.java +++ /dev/null @@ -1,399 +0,0 @@ -package com.chuangzhou.vivid2D.browser.util; - -import java.sql.*; -import java.util.Map; -import java.util.Properties; - -/** - * 数据库连接管理器 - * @author tzdwindows 7 - */ -public class DatabaseConnectionManager { - private static final Map connections = new java.util.concurrent.ConcurrentHashMap<>(); - private static final Map connectionInfo = new java.util.concurrent.ConcurrentHashMap<>(); - - public static class DatabaseInfo { - public String driver; - public String url; - public String host; - public String port; - public String database; - public String username; - - public DatabaseInfo(String driver, String url, String host, String port, String database, String username) { - this.driver = driver; - this.url = url; - this.host = host; - this.port = port; - this.database = database; - this.username = username; - } - } - - public static String connect(String driver, String host, String port, - String database, String username, String password) throws SQLException { - String connectionId = "conn_" + System.currentTimeMillis(); - - String drv = driver == null ? "" : driver.toLowerCase(); - - // 规范化 database 路径(特别是 Windows 反斜杠问题) - if (database != null) { - database = database.replace("\\", "/"); - } else { - database = ""; - } - - // 先显式加载驱动,避免因为 classloader 问题找不到驱动 - try { - switch (drv) { - case "mysql": - Class.forName("com.mysql.cj.jdbc.Driver"); - break; - case "postgresql": - Class.forName("org.postgresql.Driver"); - break; - case "sqlite": - Class.forName("org.sqlite.JDBC"); - break; - case "oracle": - Class.forName("oracle.jdbc.OracleDriver"); - break; - case "h2": - Class.forName("org.h2.Driver"); - break; - default: - // 不抛出,使后续 URL 构造仍可检查类型 - } - } catch (ClassNotFoundException e) { - throw new SQLException("JDBC 驱动未找到,请确认对应驱动已加入 classpath: " + e.getMessage(), e); - } - - String url = buildConnectionUrl(driver, host, port, database); - - Connection connection; - Properties props = new Properties(); - if (username != null && !username.isEmpty()) props.setProperty("user", username); - if (password != null && !password.isEmpty()) props.setProperty("password", password); - - switch (drv) { - case "mysql": - props.setProperty("useSSL", "false"); - props.setProperty("serverTimezone", "UTC"); - props.setProperty("allowPublicKeyRetrieval", "true"); - props.setProperty("useUnicode", "true"); - props.setProperty("characterEncoding", "UTF-8"); - connection = DriverManager.getConnection(url, props); - break; - case "postgresql": - connection = DriverManager.getConnection(url, props); - break; - case "sqlite": - // sqlite 不需要 props,URL 已经是文件路径(已做过替换) - connection = DriverManager.getConnection(url); - break; - case "oracle": - connection = DriverManager.getConnection(url, props); - break; - case "h2": - // H2 使用默认用户 sa / 空密码(如果需要可调整) - connection = DriverManager.getConnection(url, "sa", ""); - break; - default: - throw new SQLException("不支持的数据库类型: " + driver); - } - - connections.put(connectionId, connection); - connectionInfo.put(connectionId, new DatabaseInfo(driver, url, host, port, database, username)); - return connectionId; - } - - public static void disconnect(String connectionId) throws SQLException { - Connection connection = connections.get(connectionId); - if (connection != null && !connection.isClosed()) { - connection.close(); - } - connections.remove(connectionId); - connectionInfo.remove(connectionId); - } - - public static Connection getConnection(String connectionId) { - return connections.get(connectionId); - } - - public static DatabaseInfo getConnectionInfo(String connectionId) { - return connectionInfo.get(connectionId); - } - - private static String buildConnectionUrl(String driver, String host, String port, String database) { - String drv = driver == null ? "" : driver.toLowerCase(); - switch (drv) { - case "mysql": - return "jdbc:mysql://" + host + ":" + port + "/" + database; - case "postgresql": - return "jdbc:postgresql://" + host + ":" + port + "/" + database; - case "sqlite": - // 对于 SQLite,database 可能是绝对路径或相对文件名,先把反斜杠替成正斜杠 - if (database == null || database.isEmpty()) { - return "jdbc:sqlite::memory:"; - } - String normalized = database.replace("\\", "/"); - // 如果看起来像相对文件名(不含冒号也不以 / 开头),则当作相对于用户目录的路径 - if (!normalized.contains(":") && !normalized.startsWith("/")) { - String userHome = System.getProperty("user.home").replace("\\", "/"); - normalized = userHome + "/" + normalized; - } - return "jdbc:sqlite:" + normalized; - case "oracle": - return "jdbc:oracle:thin:@" + host + ":" + port + ":" + database; - case "h2": - // H2 文件路径同样做反斜杠处理 - if (database == null || database.isEmpty()) { - String userHome = System.getProperty("user.home").replace("\\", "/"); - return "jdbc:h2:file:" + userHome + "/.axis_innovators_box/databases/h2db"; - } else { - String norm = database.replace("\\", "/"); - // 如果传入仅是名字(无斜杠或冒号),则存到用户目录下 - if (!norm.contains("/") && !norm.contains(":")) { - String userHome = System.getProperty("user.home").replace("\\", "/"); - norm = userHome + "/.axis_innovators_box/databases/" + norm; - } - return "jdbc:h2:file:" + norm; - } - default: - throw new IllegalArgumentException("不支持的数据库类型: " + driver); - } - } - - - /** - * 在服务器上创建数据库(MySQL / PostgreSQL / Oracle(示例)) - * @param driver mysql | postgresql | oracle - * @param host 数据库主机 - * @param port 端口 - * @param dbName 要创建的数据库名(或 schema 名) - * @param adminUser 管理员用户名(用于创建数据库) - * @param adminPassword 管理员密码 - * @return 如果创建成功返回一个简短消息,否则抛出 SQLException - * @throws SQLException - */ - public static String createDatabaseOnServer(String driver, String host, String port, - String dbName, String adminUser, String adminPassword) throws SQLException { - if (driver == null) throw new SQLException("driver 不能为空"); - String drv = driver.toLowerCase().trim(); - - // 简单校验 dbName(避免注入)——只允许字母数字下划线 - if (dbName == null || !dbName.matches("[A-Za-z0-9_]+")) { - throw new SQLException("不合法的数据库名: " + dbName); - } - - try { - switch (drv) { - case "mysql": - // 加载驱动(如果尚未加载) - try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { - throw new SQLException("MySQL 驱动未找到,请加入 mysql-connector-java 到 classpath", e); - } - // 连接到服务器的默认库(不指定数据库)以执行 CREATE DATABASE - String mysqlUrl = "jdbc:mysql://" + host + ":" + port + "/?useSSL=false&serverTimezone=UTC"; - try (Connection conn = DriverManager.getConnection(mysqlUrl, adminUser, adminPassword); - Statement st = conn.createStatement()) { - String sql = "CREATE DATABASE `" + dbName + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"; - st.executeUpdate(sql); - } - return "MySQL 数据库创建成功: " + dbName; - - case "postgresql": - case "postgres": - try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e) { - throw new SQLException("Postgres 驱动未找到,请加入 postgresql 到 classpath", e); - } - // 连接到默认 postgres 数据库以创建新数据库 - String pgUrl = "jdbc:postgresql://" + host + ":" + port + "/postgres"; - try (Connection conn = DriverManager.getConnection(pgUrl, adminUser, adminPassword); - Statement st = conn.createStatement()) { - String sql = "CREATE DATABASE " + dbName + " WITH ENCODING 'UTF8'"; - st.executeUpdate(sql); - } - return "PostgreSQL 数据库创建成功: " + dbName; - - case "oracle": - // Oracle 数据库“创建数据库”通常由 DBA 完成(复杂),这里示例创建用户/模式(更常见) - try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException e) { - throw new SQLException("Oracle 驱动未找到,请把 ojdbc.jar 加入 classpath", e); - } - // 需使用具有足够权限的账户(通常为 sys or system),并且 URL 需要正确(SID / ServiceName) - // 下面示例假设通过 SID 连接: jdbc:oracle:thin:@host:port:SID - String oracleUrl = "jdbc:oracle:thin:@" + host + ":" + port + ":" + "ORCL"; // 把 ORCL 换成实际 SID - try (Connection conn = DriverManager.getConnection(oracleUrl, adminUser, adminPassword); - Statement st = conn.createStatement()) { - // 创建 user(schema)示例 - String pwd = adminPassword; // 实际应使用独立密码,不推荐用 adminPassword - String createUser = "CREATE USER " + dbName + " IDENTIFIED BY \"" + pwd + "\""; - String grant = "GRANT CONNECT, RESOURCE TO " + dbName; - st.executeUpdate(createUser); - st.executeUpdate(grant); - } catch (SQLException ex) { - // Oracle 操作更容易失败,给出提示 - throw new SQLException("Oracle: 无法创建用户/模式,请检查权限和 URL(通常需由 DBA 操作): " + ex.getMessage(), ex); - } - return "Oracle 用户/模式创建成功(注意:真正的 DB 实例通常由 DBA 管理): " + dbName; - - default: - throw new SQLException("不支持的数据库类型: " + driver); - } - } catch (SQLException se) { - // 透传 SQLException,调用方会拿到 message 并反馈给前端 - throw se; - } catch (Exception e) { - throw new SQLException("创建数据库时发生异常: " + e.getMessage(), e); - } - } - public static String createLocalDatabase(String driver, String dbName) throws SQLException { - switch (driver.toLowerCase()) { - case "sqlite": - // 创建目录并构造规范化路径(确保路径使用正斜杠) - String dbFileName = dbName.endsWith(".db") ? dbName : (dbName + ".db"); - java.nio.file.Path dbDir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases"); - try { - java.nio.file.Files.createDirectories(dbDir); - } catch (Exception e) { - throw new SQLException("无法创建数据库目录: " + e.getMessage(), e); - } - String dbPath = dbDir.resolve(dbFileName).toAbsolutePath().toString().replace("\\", "/"); - - // 显式加载 sqlite 驱动(避免 No suitable driver) - try { - Class.forName("org.sqlite.JDBC"); - } catch (ClassNotFoundException e) { - throw new SQLException("未找到 sqlite 驱动,请确认 sqlite-jdbc 已加入 classpath", e); - } - - // 直接使用 connect 构建连接(connect 中会通过 buildConnectionUrl 处理 path) - String connectionId = connect("sqlite", "", "", dbPath, "", ""); - - // 创建示例表 - createSampleTables(connectionId); - - return connectionId; - - case "h2": - java.nio.file.Path h2Dir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases"); - try { - java.nio.file.Files.createDirectories(h2Dir); - } catch (Exception e) { - throw new SQLException("无法创建数据库目录: " + e.getMessage(), e); - } - String h2Path = h2Dir.resolve(dbName).toAbsolutePath().toString().replace("\\", "/"); - String h2Url = "jdbc:h2:file:" + h2Path; - - try { - Class.forName("org.h2.Driver"); - } catch (ClassNotFoundException e) { - throw new SQLException("未找到 H2 驱动,请确认 h2.jar 已加入 classpath", e); - } - - Connection h2Conn = DriverManager.getConnection(h2Url, "sa", ""); - String h2ConnectionId = "conn_" + System.currentTimeMillis(); - connections.put(h2ConnectionId, h2Conn); - connectionInfo.put(h2ConnectionId, new DatabaseInfo("h2", h2Url, "localhost", "", dbName, "sa")); - - createSampleTables(h2ConnectionId); - - return h2ConnectionId; - - default: - throw new SQLException("不支持创建本地数据库类型: " + driver); - } - } - - private static void createSampleTables(String connectionId) throws SQLException { - Connection conn = getConnection(connectionId); - DatabaseInfo info = getConnectionInfo(connectionId); - - if ("sqlite".equals(info.driver) || "h2".equals(info.driver)) { - try (Statement stmt = conn.createStatement()) { - // 创建用户表 - stmt.execute("CREATE TABLE IF NOT EXISTS users (" + - "id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "username VARCHAR(50) NOT NULL UNIQUE, " + - "email VARCHAR(100) NOT NULL, " + - "password VARCHAR(100) NOT NULL, " + - "status VARCHAR(20) DEFAULT 'active', " + - "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" + - ")"); - - // 创建产品表 - stmt.execute("CREATE TABLE IF NOT EXISTS products (" + - "id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "name VARCHAR(100) NOT NULL, " + - "description TEXT, " + - "price DECIMAL(10,2) NOT NULL, " + - "stock INTEGER DEFAULT 0, " + - "category VARCHAR(50), " + - "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" + - ")"); - - // 创建订单表 - stmt.execute("CREATE TABLE IF NOT EXISTS orders (" + - "id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "user_id INTEGER, " + - "product_id INTEGER, " + - "quantity INTEGER NOT NULL, " + - "total_price DECIMAL(10,2) NOT NULL, " + - "status VARCHAR(20) DEFAULT 'pending', " + - "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + - "FOREIGN KEY (user_id) REFERENCES users(id), " + - "FOREIGN KEY (product_id) REFERENCES products(id)" + - ")"); - - // 插入示例数据 - insertSampleData(conn); - } - } - } - - private static void insertSampleData(Connection conn) throws SQLException { - // 检查是否已有数据 - try (Statement checkStmt = conn.createStatement(); - ResultSet rs = checkStmt.executeQuery("SELECT COUNT(*) FROM users")) { - if (rs.next() && rs.getInt(1) == 0) { - // 插入用户数据 - try (PreparedStatement pstmt = conn.prepareStatement( - "INSERT INTO users (username, email, password) VALUES (?, ?, ?)")) { - String[][] users = { - {"admin", "admin@example.com", "password123"}, - {"user1", "user1@example.com", "password123"}, - {"user2", "user2@example.com", "password123"} - }; - - for (String[] user : users) { - pstmt.setString(1, user[0]); - pstmt.setString(2, user[1]); - pstmt.setString(3, user[2]); - pstmt.executeUpdate(); - } - } - - // 插入产品数据 - try (PreparedStatement pstmt = conn.prepareStatement( - "INSERT INTO products (name, description, price, stock, category) VALUES (?, ?, ?, ?, ?)")) { - Object[][] products = { - {"笔记本电脑", "高性能笔记本电脑", 5999.99, 50, "电子"}, - {"智能手机", "最新款智能手机", 3999.99, 100, "电子"}, - {"办公椅", "舒适办公椅", 299.99, 30, "家居"}, - {"咖啡机", "全自动咖啡机", 899.99, 20, "家电"} - }; - - for (Object[] product : products) { - pstmt.setString(1, (String) product[0]); - pstmt.setString(2, (String) product[1]); - pstmt.setDouble(3, (Double) product[2]); - pstmt.setInt(4, (Integer) product[3]); - pstmt.setString(5, (String) product[4]); - pstmt.executeUpdate(); - } - } - } - } - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/events/Event.java b/src/main/java/com/chuangzhou/vivid2D/events/Event.java deleted file mode 100644 index 177e6ba..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/events/Event.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.chuangzhou.vivid2D.events; - -/** - * 事件基础接口 - * 所有事件都应实现此接口 - * @author tzdwindows 7 - */ -public interface Event { - /** - * @return 事件是否已被取消 - */ - boolean isCancelled(); - - /** - * 设置事件的取消状态 - * @param cancelled true 表示取消事件,后续的订阅者将不会收到此事件 - */ - void setCancelled(boolean cancelled); -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/events/EventBus.java b/src/main/java/com/chuangzhou/vivid2D/events/EventBus.java deleted file mode 100644 index 7474d62..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/events/EventBus.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.chuangzhou.vivid2D.events; - -import com.axis.innovators.box.events.SubscribeEvent; - -import java.lang.reflect.Method; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * 事件总线 - * - * @author tzdwindows 7 - */ -public class EventBus { - private static int maxID = 0; - private final int busID; - // 使用线程安全的集合以支持并发环境 - private final Map, List> eventSubscribers = new ConcurrentHashMap<>(); - private final Map> targetSubscribers = new ConcurrentHashMap<>(); - private volatile boolean shutdown; - - public EventBus() { - this.busID = maxID++; - } - - private static class Subscriber implements Comparable { - final Object target; - final Method method; - final Class eventType; - final int priority; // 新增优先级字段 - - Subscriber(Object target, Method method, Class eventType, int priority) { - this.target = target; - this.method = method; - this.eventType = eventType; - this.priority = priority; - } - - @Override - public int compareTo(Subscriber other) { - // 按优先级降序排序 - return Integer.compare(other.priority, this.priority); - } - } - - /** - * 注册目标对象的事件监听器 - * - * @param target 目标对象 - */ - public void register(Object target) { - if (targetSubscribers.containsKey(target)) { - return; - } - - List subs = new CopyOnWriteArrayList<>(); - for (Method method : getAnnotatedMethods(target)) { - SubscribeEvent annotation = method.getAnnotation(SubscribeEvent.class); - if (annotation == null) { - continue; - } - - Class[] paramTypes = method.getParameterTypes(); - if (paramTypes.length != 1) { - System.err.println("Method " + method.getName() + " has @SubscribeEvent annotation but requires " + paramTypes.length + " parameters. Only one is allowed."); - continue; - } - - Class eventType = paramTypes[0]; - // 确保事件参数实现了 Event 接口 - if (!Event.class.isAssignableFrom(eventType)) { - System.err.println("Method " + method.getName() + " has @SubscribeEvent annotation, but its parameter " + eventType.getName() + " does not implement the Event interface."); - continue; - } - - Subscriber sub = new Subscriber(target, method, eventType, annotation.priority()); - // 使用 computeIfAbsent 简化代码并保证线程安全 - List eventSubs = eventSubscribers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>()); - eventSubs.add(sub); - // 每次添加后都进行排序,以保证优先级顺序 - Collections.sort(eventSubs); - subs.add(sub); - } - - if (!subs.isEmpty()) { - targetSubscribers.put(target, subs); - } - } - - /** - * 获取目标对象中所有带有 @SubscribeEvent 注解的方法 - * - * @param target 目标对象 - * @return 方法集合 - */ - private Set getAnnotatedMethods(Object target) { - Set methods = new HashSet<>(); - Class clazz = target.getClass(); - while (clazz != null) { - for (Method method : clazz.getDeclaredMethods()) { - if (method.isAnnotationPresent(SubscribeEvent.class)) { - methods.add(method); - } - } - clazz = clazz.getSuperclass(); - } - return methods; - } - - /** - * 注销目标对象的事件监听器 - * - * @param target 目标对象 - */ - public void unregister(Object target) { - List subs = targetSubscribers.remove(target); - if (subs == null) { - return; - } - - for (Subscriber sub : subs) { - List eventSubs = eventSubscribers.get(sub.eventType); - if (eventSubs != null) { - eventSubs.remove(sub); - if (eventSubs.isEmpty()) { - eventSubscribers.remove(sub.eventType); - } - } - } - } - - /** - * 发布事件 - * - * @param event 事件对象,必须实现 Event 接口 - * @return 返回一个 PostResult 对象,其中包含事件是否被取消的状态 - */ - public PostResult post(Event event) { - if (shutdown) { - return new PostResult(event.isCancelled(), null); - } - - Class eventType = event.getClass(); - List subs = eventSubscribers.get(eventType); - - if (subs == null || subs.isEmpty()) { - return new PostResult(false, null); - } - - for (Subscriber sub : subs) { - try { - // 无需再创建副本,因为我们使用了 CopyOnWriteArrayList - sub.method.setAccessible(true); - sub.method.invoke(sub.target, event); - - // 如果事件被任何一个订阅者取消,则立即停止分发 - if (event.isCancelled()) { - break; - } - } catch (Exception e) { - handleException(event, sub, e); - } - } - - // 默认返回一个空的 Map,您可以根据需要进行修改 - Map additionalInfo = new HashMap<>(); - return new PostResult(event.isCancelled(), additionalInfo); - } - - /** - * 关闭事件总线,停止处理事件 - */ - public void shutdown() { - shutdown = true; - eventSubscribers.clear(); - targetSubscribers.clear(); - } - - /** - * 处理事件处理过程中出现的异常 - * - * @param event 事件 - * @param subscriber 发生异常的订阅者 - * @param e 异常 - */ - private void handleException(Event event, Subscriber subscriber, Exception e) { - System.err.println("Exception thrown by subscriber " + subscriber.target.getClass().getName() + - "#" + subscriber.method.getName() + " when handling event " + event.getClass().getName()); - e.printStackTrace(); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/events/GlobalEventBus.java b/src/main/java/com/chuangzhou/vivid2D/events/GlobalEventBus.java deleted file mode 100644 index ee8f7be..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/events/GlobalEventBus.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.chuangzhou.vivid2D.events; - -/** - * @author tzdwindows 7 - */ -public class GlobalEventBus { - /** - * 全局事件总线 - */ - public static final EventBus EVENT_BUS = new EventBus(); -} diff --git a/src/main/java/com/chuangzhou/vivid2D/events/PostResult.java b/src/main/java/com/chuangzhou/vivid2D/events/PostResult.java deleted file mode 100644 index a5ebf31..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/events/PostResult.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.chuangzhou.vivid2D.events; - -import java.util.Collections; -import java.util.Map; - -/** - * 事件发布后的结果 - * @author tzdwindows 7 - */ -public class PostResult { - private final boolean cancelled; - private final Map additionalInfo; - - public PostResult(boolean cancelled, Map additionalInfo) { - this.cancelled = cancelled; - this.additionalInfo = additionalInfo != null ? additionalInfo : Collections.emptyMap(); - } - - /** - * @return 事件是否在处理过程中被取消 - */ - public boolean isCancelled() { - return cancelled; - } - - /** - * @return 一个包含附加信息的Map,默认为空 - */ - public Map getAdditionalInfo() { - return additionalInfo; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/events/render/Mesh2DRender.java b/src/main/java/com/chuangzhou/vivid2D/events/render/Mesh2DRender.java deleted file mode 100644 index b8f90b0..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/events/render/Mesh2DRender.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.chuangzhou.vivid2D.events.render; - -import com.chuangzhou.vivid2D.events.Event; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import org.joml.Matrix3f; - -/** - * 这是一个用于组织 Mesh2D 相关渲染事件的容器类。 - * 它不应该被实例化。 - */ -public final class Mesh2DRender { - - /** - * 私有构造函数,防止该容器类被实例化。 - */ - private Mesh2DRender() {} - - /** - * 在 Mesh2D 对象开始渲染前发布的事件。 - * 这个事件是可取消的。如果被取消,后续的渲染操作将不会执行。 - */ - public static class Start implements Event { - private boolean cancelled = false; - public final Mesh2D mesh; - public final int shaderProgram; - public final Matrix3f modelMatrix; - - public Start(Mesh2D mesh, int shaderProgram, Matrix3f modelMatrix) { - this.mesh = mesh; - this.shaderProgram = shaderProgram; - this.modelMatrix = modelMatrix; - } - - @Override - public boolean isCancelled() { - return this.cancelled; - } - - @Override - public void setCancelled(boolean cancelled) { - this.cancelled = cancelled; - } - } - - /** - * 在 Mesh2D 对象完成渲染后发布的事件。 - * 这个事件不可取消。 - */ - public static class End implements Event { - public final Mesh2D mesh; - public final int shaderProgram; - public final Matrix3f modelMatrix; - - public End(Mesh2D mesh, int shaderProgram, Matrix3f modelMatrix) { - this.mesh = mesh; - this.shaderProgram = shaderProgram; - this.modelMatrix = modelMatrix; - } - - @Override - public boolean isCancelled() { - return false; - } - - @Override - public void setCancelled(boolean cancelled) { - // 不支持取消 - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java deleted file mode 100644 index c91c71b..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java +++ /dev/null @@ -1,1386 +0,0 @@ -package com.chuangzhou.vivid2D.render; - -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.util.LightSource; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; -import com.chuangzhou.vivid2D.render.systems.Camera; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; -import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; -import com.chuangzhou.vivid2D.render.systems.sources.CompleteShader; -import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement; -import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram; -import org.joml.Matrix3f; -import org.joml.Vector2f; -import org.joml.Vector3f; -import org.joml.Vector4f; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL15; -import org.lwjgl.opengl.GL20; -import org.lwjgl.opengl.GL30; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * vivid2D 模型完整渲染系统 - * - *

该系统提供了完整的 vivid2D 模型加载、渲染和显示功能,支持多种渲染模式和效果:

- * - *
    - *
  • 基础模型渲染
  • - *
  • 光照效果渲染
  • - *
  • 纹理贴图渲染
  • - *
  • 模型加载与解析
  • - *
- * - *

使用示例:

- *
    - *
  • {@link com.chuangzhou.vivid2D.test.ModelLoadTest} - 模型加载测试
  • - *
  • {@link com.chuangzhou.vivid2D.test.ModelRenderLightingTest} - 光照渲染测试
  • - *
  • {@link com.chuangzhou.vivid2D.test.ModelRenderTest} - 基础渲染测试
  • - *
  • {@link com.chuangzhou.vivid2D.test.ModelRenderTest2} - 进阶渲染测试
  • - *
  • {@link com.chuangzhou.vivid2D.test.ModelRenderTextureTest} - 纹理渲染测试
  • - *
  • {@link com.chuangzhou.vivid2D.test.ModelTest} - 基础模型测试
  • - *
  • {@link com.chuangzhou.vivid2D.test.ModelTest2} - 进阶模型测试
  • - *
- * - * @author tzdwindows 7 - * @version 1.2 - * @since 2025-10-13 - */ -public final class ModelRender { - /** - * 渲染系统日志记录器,用于记录渲染过程中的调试信息、错误和性能数据 - */ - private static final Logger logger = LoggerFactory.getLogger(ModelRender.class); - - /** - * 私有构造函数,防止外部实例化 - 这是一个工具类,只包含静态方法 - */ - private ModelRender() { /* no instances */ } - -// ================== 全局状态 ================== - - /** - * 渲染系统初始化状态标志,确保系统只初始化一次 - * - * @see #initialize() - * @see #isInitialized() - */ - private static boolean initialized = false; - - /** - * 视口宽度(像素),定义渲染区域的大小 - * 默认值:800像素 - * - * @see #setViewport(int, int) - */ - static int viewportWidth = 800; - - /** - * 视口高度(像素),定义渲染区域的大小 - * 默认值:600像素 - * - * @see #setViewport(int, int) - */ - static int viewportHeight = 600; - - /** - * 清除颜色(RGBA),用于在每帧开始时清空颜色缓冲区 - * 默认值:黑色不透明 (0.0f, 0.0f, 0.0f, 1.0f) - * - * @see RenderSystem#clearColor(float, float, float, float) - */ - private static final Vector4f CLEAR_COLOR = new Vector4f(0.0f, 0.0f, 0.0f, 1.0f); - - /** - * 深度测试启用标志,控制是否进行深度缓冲测试 - * 在2D渲染中通常禁用以提高性能 - * 默认值:false(禁用) - * - * @see RenderSystem#enableDepthTest() - * @see RenderSystem#disableDepthTest() - */ - private static final boolean enableDepthTest = false; - - /** - * 混合功能启用标志,控制透明度和颜色混合 - * 默认值:true(启用) - * - * @see RenderSystem#enableBlend() - * @see RenderSystem#disableBlend() - */ - private static final boolean enableBlending = true; - - /** - * 最大光源数量,用于限制同时启用的光源数量 - * 默认值:80 - */ - private static final int MAX_LIGHTS = 80; - -// ================== 着色器与资源管理 ================== - - /** - * 默认着色器程序,用于大多数模型的渲染 - * 包含基础的光照、纹理和变换功能 - * - * @see #compileDefaultShader() - */ - private static ShaderProgram defaultProgram = null; - - /** - * 网格GPU资源缓存,管理已上传到GPU的网格数据 - * 键:Mesh2D对象 - * 值:对应的OpenGL资源(VAO、VBO、EBO) - * - * @see MeshGLResources - */ - private static final Map meshResources = new HashMap<>(); - - /** - * 纹理单元分配器,用于管理多个纹理的绑定 - * 确保不同的纹理绑定到正确的纹理单元 - * 默认从0开始递增分配 - */ - private static final AtomicInteger textureUnitAllocator = new AtomicInteger(0); - - /** - * 默认白色纹理ID,当模型没有指定纹理时使用 - * 这是一个1x1的纯白色纹理,确保模型有基本的颜色显示 - * - * @see #createDefaultTexture() - */ - private static int defaultTextureId = 0; - -// ================== 碰撞箱渲染配置 ================== - - /** - * 碰撞箱渲染开关,控制是否在场景中显示物理碰撞体的轮廓 - * 调试时非常有用,可以直观看到碰撞边界 - * 默认值:true(启用) - */ - public static boolean renderColliders = true; - - /** - * 碰撞箱线框宽度,控制碰撞体轮廓线的粗细 - * 单位:像素 - * 默认值:1.0f - */ - public static float colliderLineWidth = 1.0f; - - /** - * 碰撞箱颜色(RGBA),定义碰撞体轮廓的显示颜色 - * 默认值:白色不透明 (1.0f, 1.0f, 1.0f, 1.0f) - */ - public static Vector4f colliderColor = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); - - /** - * 圆形碰撞体细分数量,控制圆形碰撞体的平滑度 - * 值越高圆形越平滑,但渲染开销也越大 - * 默认值:32(在性能和视觉效果间取得平衡) - */ - private static final int CIRCLE_SEGMENTS = 32; - - /** - * 光源位置渲染开关,控制是否在场景中显示光源的位置 - * 用点状标记显示每个启用的光源位置 - * 默认值:true(启用) - */ - public static boolean renderLightPositions = true; - -// ================== 摄像机状态 ================== - - /** - * 默认摄像机,用于控制场景的视图和缩放 - * 默认位置:(0, 0) - */ - private static final Camera camera = new Camera(); - - // ================== 字体管理 ================== - private static TextRenderer defaultTextRenderer = null; - private static final int FONT_BITMAP_WIDTH = 512; - private static final int FONT_BITMAP_HEIGHT = 512; - private static final int FONT_FIRST_CHAR = 32; - private static final int FONT_CHAR_COUNT = 96; - - // ================== 摄像机API方法 ================== - - /** - * 获取全局摄像机实例 - */ - public static Camera getCamera() { - return camera; - } - - /** - * 设置摄像机位置 - */ - public static void setCameraPosition(float x, float y) { - camera.setPosition(x, y); - } - - /** - * 设置摄像机缩放 - */ - public static void setCameraZoom(float zoom) { - camera.setZoom(zoom); - } - - /** - * 设置摄像机Z轴位置 - */ - public static void setCameraZPosition(float z) { - camera.setZPosition(z); - } - - /** - * 移动摄像机 - */ - public static void moveCamera(float dx, float dy) { - camera.move(dx, dy); - } - - /** - * 缩放摄像机 - */ - public static void zoomCamera(float factor) { - camera.zoom(factor); - } - - /** - * 重置摄像机 - */ - public static void resetCamera() { - camera.reset(); - } - - /** - * 启用/禁用摄像机 - */ - public static void setCameraEnabled(boolean enabled) { - camera.setEnabled(enabled); - } - - /** - * 构建考虑摄像机变换的投影矩阵 - */ - private static Matrix3f buildCameraProjection(int width, int height) { - Matrix3f m = new Matrix3f(); - - if (camera.isEnabled()) { - // 考虑摄像机缩放和平移 - float zoom = camera.getZoom(); - Vector2f pos = camera.getPosition(); - - m.set( - 2.0f * zoom / width, 0.0f, -1.0f - (2.0f * zoom * pos.x / width), - 0.0f, -2.0f * zoom / height, 1.0f + (2.0f * zoom * pos.y / height), - 0.0f, 0.0f, 1.0f - ); - } else { - // 原始投影矩阵 - m.set( - 2.0f / width, 0.0f, -1.0f, - 0.0f, -2.0f / height, 1.0f, - 0.0f, 0.0f, 1.0f - ); - } - - return m; - } - - // ================== 内部类:MeshGLResources ================== - private static class MeshGLResources { - int vao = 0; - int vbo = 0; - int ebo = 0; - boolean initialized = false; - - void dispose() { - if (ebo != 0) { - GL15.glDeleteBuffers(ebo); - ebo = 0; - } - if (vbo != 0) { - GL15.glDeleteBuffers(vbo); - vbo = 0; - } - if (vao != 0) { - GL30.glDeleteVertexArrays(vao); - vao = 0; - } - initialized = false; - } - } - - // ================== 初始化 / 清理 ================== - public static synchronized void initialize() { - if (initialized) return; - - logger.info("Initializing ModelRender..."); - - // 初始化渲染系统 - RenderSystem.beginInitialization(); - RenderSystem.initRenderThread(); - - logGLInfo(); - setupGLState(); - - try { - compileDefaultShader(); - - // 初始化所有非默认着色器的基础信息 - initNonDefaultShaders(); - - } catch (RuntimeException ex) { - logger.error("Failed to compile default shader: {}", ex.getMessage()); - throw ex; - } - - - createDefaultTexture(); - RenderSystem.viewport(0, 0, viewportWidth, viewportHeight); - RenderSystem.finishInitialization(); - - try { - // 初始化默认字体(可替换为你自己的 TTF 数据) - ByteBuffer fontData = null; - try { - fontData = RenderSystem.loadFont("FZYTK.TTF"); - } catch (Exception e) { - logger.warn("Failed to load Arial.ttf, trying fallback fonts", e); - // 尝试其他字体 - try { - fontData = RenderSystem.loadFont("arial.ttf"); - } catch (Exception e2) { - try { - fontData = RenderSystem.loadFont("times.ttf"); - } catch (Exception e3) { - logger.error("All font loading attempts failed"); - } - } - } - - if (fontData != null && fontData.capacity() > 0) { - defaultTextRenderer = new TextRenderer(FONT_BITMAP_WIDTH, FONT_BITMAP_HEIGHT, FONT_FIRST_CHAR, FONT_CHAR_COUNT); - RenderSystem.checkGLError("TextRenderer constructor"); - - defaultTextRenderer.initialize(fontData, 20.0f); - RenderSystem.checkGLError("defaultTextRenderer initialization"); - - if (!defaultTextRenderer.isInitialized()) { - logger.error("TextRenderer failed to initialize properly"); - } - } else { - logger.error("No valid font data available for text rendering"); - } - } catch (Exception e) { - logger.warn("Failed to initialize default text renderer", e); - } - - initialized = true; - logger.info("ModelRender initialized successfully"); - } - - /** - * 初始化所有非默认着色器的基础信息(顶点坐标等) - */ - private static void initNonDefaultShaders() { - List shaderList = ShaderManagement.getShaderList(); - if (shaderList == null || shaderList.isEmpty()) { - logger.info("No shaders found to initialize"); - return; - } - - int nonDefaultCount = 0; - for (CompleteShader shader : shaderList) { - // 跳过默认着色器,只初始化非默认的 - if (shader.isDefaultShader()) { - continue; - } - - try { - // 获取着色器程序 - ShaderProgram program = ShaderManagement.getShaderProgram(shader.getShaderName()); - if (program == null) { - logger.warn("Shader program not found for: {}", shader.getShaderName()); - continue; - } - - // 设置着色器的基础uniforms(主要是顶点坐标相关的) - initShaderBasicUniforms(program, shader); - nonDefaultCount++; - - logger.debug("Initialized non-default shader: {}", shader.getShaderName()); - - } catch (Exception e) { - logger.error("Failed to initialize non-default shader: {}", shader.getShaderName(), e); - } - } - - logger.info("Initialized {} non-default shaders", nonDefaultCount); - } - - /** - * 初始化着色器的基础uniforms(顶点坐标相关) - */ - private static void initShaderBasicUniforms(ShaderProgram program, CompleteShader shader) { - program.use(); - - try { - // 设置基础的变换矩阵为单位矩阵 - setUniformMatrix3(program, "uModelMatrix", new Matrix3f().identity()); - setUniformMatrix3(program, "uViewMatrix", new Matrix3f().identity()); - - // 设置投影矩阵(使用当前视口尺寸) - Matrix3f projection = buildOrthoProjection(viewportWidth, viewportHeight); - setUniformMatrix3(program, "uProjectionMatrix", projection); - - // 设置基础颜色为白色 - setUniformVec4Internal(program, "uColor", new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); - - // 设置基础不透明度 - setUniformFloatInternal(program, "uOpacity", 1.0f); - - // 设置纹理单元(如果有纹理的话) - setUniformIntInternal(program, "uTexture", 0); - - RenderSystem.checkGLError("initShaderBasicUniforms_" + shader.getShaderName()); - - } finally { - program.stop(); - } - } - - private static void logGLInfo() { - RenderSystem.logDetailedGLInfo(); - } - - private static void uploadLightsToShader(ShaderProgram sp, Model2D model) { - List lights = model.getLights(); - int idx = 0; - - for (int i = 0; i < lights.size() && idx < MAX_LIGHTS; i++) { - com.chuangzhou.vivid2D.render.model.util.LightSource l = lights.get(i); - if (!l.isEnabled()) continue; - - // 基础属性 - setUniformVec2Internal(sp, "uLightsPos[" + idx + "]", l.isAmbient() ? new org.joml.Vector2f(0f, 0f) : l.getPosition()); - setUniformVec3Internal(sp, "uLightsColor[" + idx + "]", l.getColor()); - setUniformFloatInternal(sp, "uLightsIntensity[" + idx + "]", l.getIntensity()); - setUniformIntInternal(sp, "uLightsIsAmbient[" + idx + "]", l.isAmbient() ? 1 : 0); - - // 辉光相关(如果没有被设置也安全地上传默认值) - setUniformIntInternal(sp, "uLightsIsGlow[" + idx + "]", l.isGlow() ? 1 : 0); - setUniformVec2Internal(sp, "uLightsGlowDir[" + idx + "]", l.getGlowDirection() != null ? l.getGlowDirection() : new org.joml.Vector2f(0f, 0f)); - setUniformFloatInternal(sp, "uLightsGlowIntensity[" + idx + "]", l.getGlowIntensity()); - setUniformFloatInternal(sp, "uLightsGlowRadius[" + idx + "]", l.getGlowRadius()); - setUniformFloatInternal(sp, "uLightsGlowAmount[" + idx + "]", l.getGlowAmount()); - - idx++; - } - - // 上传实际有效光源数量 - setUniformIntInternal(sp, "uLightCount", idx); - - // 禁用剩余槽位(确保 shader 中不会读取到垃圾值) - for (int i = idx; i < MAX_LIGHTS; i++) { - setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", 0f); - setUniformIntInternal(sp, "uLightsIsAmbient[" + i + "]", 0); - setUniformVec3Internal(sp, "uLightsColor[" + i + "]", new org.joml.Vector3f(0f, 0f, 0f)); - setUniformVec2Internal(sp, "uLightsPos[" + i + "]", new org.joml.Vector2f(0f, 0f)); - - // 关闭辉光槽 - setUniformIntInternal(sp, "uLightsIsGlow[" + i + "]", 0); - setUniformVec2Internal(sp, "uLightsGlowDir[" + i + "]", new org.joml.Vector2f(0f, 0f)); - setUniformFloatInternal(sp, "uLightsGlowIntensity[" + i + "]", 0f); - setUniformFloatInternal(sp, "uLightsGlowRadius[" + i + "]", 0f); - setUniformFloatInternal(sp, "uLightsGlowAmount[" + i + "]", 0f); - } - } - - - private static void setupGLState() { - RenderSystem.checkGLError("setupGLState_start"); - - RenderSystem.clearColor(CLEAR_COLOR.x, CLEAR_COLOR.y, CLEAR_COLOR.z, CLEAR_COLOR.w); - RenderSystem.checkGLError("after_clearColor"); - - if (enableBlending) { - RenderSystem.enableBlend(); - RenderSystem.checkGLError("after_enableBlend"); - - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - RenderSystem.checkGLError("after_blendFunc"); - } else { - RenderSystem.disableBlend(); - RenderSystem.checkGLError("after_disableBlend"); - } - - if (enableDepthTest) { - RenderSystem.enableDepthTest(); - RenderSystem.checkGLError("after_enableDepthTest"); - - RenderSystem.depthFunc(GL11.GL_LEQUAL); - RenderSystem.checkGLError("after_depthFunc"); - - RenderSystem.depthMask(true); - RenderSystem.checkGLError("after_depthMask"); - - RenderSystem.clearDepth(1.0); - RenderSystem.checkGLError("after_clearDepth"); - } else { - RenderSystem.disableDepthTest(); - RenderSystem.checkGLError("after_disableDepthTest"); - } - - RenderSystem.checkGLError("after_disableCullFace"); - } - - private static void compileDefaultShader() { - ShaderManagement.compileAllShaders(); - defaultProgram = ShaderManagement.getDefaultProgram(); - if (defaultProgram == null) { - throw new RuntimeException("Failed to compile default shader: no default shader found"); - } - } - - private static void createDefaultTexture() { - RenderSystem.assertOnRenderThread(); - defaultTextureId = RenderSystem.createDefaultTexture(); - RenderSystem.checkGLError("createDefaultTexture"); - } - - public static synchronized void cleanup() { - if (!initialized) return; - - logger.info("Cleaning up ModelRender..."); - - // mesh resources - for (MeshGLResources r : meshResources.values()) r.dispose(); - meshResources.clear(); - - // 使用新的着色器管理系统清理着色器 - ShaderManagement.cleanup(); - defaultProgram = null; - - // textures - if (defaultTextureId != 0) { - RenderSystem.deleteTextures(defaultTextureId); - defaultTextureId = 0; - } - - initialized = false; - logger.info("ModelRender cleaned up"); - } - - // ================== 渲染流程 (已修改) ================== - public static void render(float deltaTime, Model2D model) { - if (!initialized) throw new IllegalStateException("ModelRender not initialized"); - if (model == null) return; - - // 确保在渲染线程 - RenderSystem.assertOnRenderThread(); - - // 添加前置错误检查 - RenderSystem.checkGLError("render_start"); - - // 物理系统更新 - PhysicsSystem physics = model.getPhysics(); - if (physics != null && physics.isEnabled()) { - physics.update(deltaTime, model); - } - - model.update(deltaTime); - - // 检查清除操作前的状态 - RenderSystem.checkGLError("before_clear"); - RenderSystem.clear(GL11.GL_COLOR_BUFFER_BIT | (enableDepthTest ? GL11.GL_DEPTH_BUFFER_BIT : 0)); - RenderSystem.checkGLError("after_clear"); - - // 检查着色器程序 - if (defaultProgram == null || defaultProgram.programId == 0) { - logger.error("Default shader program is not initialized"); - return; - } - - // 设置投影与视图矩阵(使用摄像机变换) - Matrix3f proj = buildCameraProjection(viewportWidth, viewportHeight); - Matrix3f view = new Matrix3f().identity(); - - // 1. 首先设置默认着色器 - defaultProgram.use(); - RenderSystem.checkGLError("after_use_default_program"); - - // 设置默认着色器的投影与视图 - setUniformMatrix3(defaultProgram, "uProjectionMatrix", proj); - setUniformMatrix3(defaultProgram, "uViewMatrix", view); - RenderSystem.checkGLError("after_set_default_matrices"); - - // 设置摄像机Z轴位置(如果着色器支持) - setUniformFloatInternal(defaultProgram, "uCameraZ", camera.getZPosition()); - RenderSystem.checkGLError("after_set_camera_z"); - - // 添加光源数据上传到默认着色器 - uploadLightsToShader(defaultProgram, model); - RenderSystem.checkGLError("after_upload_lights"); - - // 2. 设置非默认着色器的顶点坐标相关uniform - setupNonDefaultShaders(proj, view); - RenderSystem.checkGLError("after_setup_non_default_shaders"); - - // 在渲染光源位置前检查 - RenderSystem.checkGLError("before_render_light_positions"); - renderLightPositions(model); - RenderSystem.checkGLError("after_render_light_positions"); - - // 递归渲染所有根部件(使用默认着色器) - Matrix3f identity = new Matrix3f().identity(); - for (ModelPart p : model.getParts()) { - if (p.getParent() != null) continue; - renderPartRecursive(p, identity); - } - RenderSystem.checkGLError("after_render_parts"); - - if (renderColliders && physics != null) { - renderPhysicsColliders(physics); - RenderSystem.checkGLError("after_render_colliders"); - } - - //if (defaultTextRenderer != null) { - // String camInfo = String.format("Camera X: %.2f Y: %.2f Zoom: %.2f", - // camera.getPosition().x, - // camera.getPosition().y, - // camera.getZoom()); - // float x = 10.0f; - // float y = viewportHeight - 30.0f; - // Vector4f color = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); - // renderText(camInfo, x, y, color); - // RenderSystem.checkGLError("renderText"); - //} - - RenderSystem.checkGLError("render_end"); - } - - // ================== 缩略图渲染方法 ================== - - /** - * 渲染模型缩略图(图层式渲染,不受摄像机控制) - * - *

该方法提供类似PS图层预览的缩略图渲染功能:

- *
    - *
  • 固定位置和大小,不受摄像机影响
  • - *
  • 自动缩放确保模型完全可见
  • - *
  • 禁用复杂效果以提高性能
  • - *
  • 独立的渲染状态管理
  • - *
- * - * @param model 要渲染的模型 - * @param x 缩略图左上角X坐标(屏幕坐标) - * @param y 缩略图左上角Y坐标(屏幕坐标) - * @param width 缩略图宽度 - * @param height 缩略图高度 - */ - public static void renderThumbnail(Model2D model, float x, float y, float width, float height) { - if (!initialized) throw new IllegalStateException("ModelRender not initialized"); - if (model == null) return; - - RenderSystem.assertOnRenderThread(); - RenderSystem.checkGLError("renderThumbnail_start"); - - // 保存原始状态以便恢复 - boolean originalRenderColliders = renderColliders; - boolean originalRenderLightPositions = renderLightPositions; - int originalViewportWidth = viewportWidth; - int originalViewportHeight = viewportHeight; - - try { - // 设置缩略图专用状态 - renderColliders = false; - renderLightPositions = false; - - // 设置缩略图视口(屏幕坐标) - RenderSystem.viewport((int)x, (int)y, (int)width, (int)height); - - // 清除缩略图区域 - RenderSystem.clear(GL11.GL_COLOR_BUFFER_BIT | (enableDepthTest ? GL11.GL_DEPTH_BUFFER_BIT : 0)); - RenderSystem.checkGLError("thumbnail_after_clear"); - - // 简化版的模型更新(跳过物理系统) - model.update(0.016f); // 使用固定时间步长 - - // 计算模型边界和缩放比例 - ThumbnailBounds bounds = calculateThumbnailBounds(model, width, height); - - // 设置缩略图专用的正交投影(固定位置,不受摄像机影响) - Matrix3f proj = buildThumbnailProjection(width, height); - Matrix3f view = new Matrix3f().identity(); - - // 使用默认着色器 - defaultProgram.use(); - RenderSystem.checkGLError("thumbnail_after_use_program"); - - // 设置基础变换矩阵 - setUniformMatrix3(defaultProgram, "uProjectionMatrix", proj); - setUniformMatrix3(defaultProgram, "uViewMatrix", view); - setUniformFloatInternal(defaultProgram, "uCameraZ", 0f); // 固定Z位置 - RenderSystem.checkGLError("thumbnail_after_set_matrices"); - - // 简化光源:只使用环境光 - setupThumbnailLighting(defaultProgram, model); - RenderSystem.checkGLError("thumbnail_after_setup_lighting"); - - // 应用缩放和平移确保模型完全可见 - Matrix3f thumbnailTransform = new Matrix3f( - bounds.scale, 0, bounds.offsetX, - 0, bounds.scale, bounds.offsetY, - 0, 0, 1 - ); - - // 递归渲染所有根部件(应用缩略图专用变换) - for (ModelPart p : model.getParts()) { - if (p.getParent() != null) continue; - renderPartForThumbnail(p, thumbnailTransform); - } - RenderSystem.checkGLError("thumbnail_after_render_parts"); - - } finally { - // 恢复原始状态 - renderColliders = originalRenderColliders; - renderLightPositions = originalRenderLightPositions; - RenderSystem.viewport(0, 0, originalViewportWidth, originalViewportHeight); - } - - RenderSystem.checkGLError("renderThumbnail_end"); - } - - /** - * 缩略图边界计算结果 - */ - private static class ThumbnailBounds { - public float minX, maxX, minY, maxY; - public float scale; - public float offsetX, offsetY; - } - - /** - * 计算模型的边界和合适的缩放比例 - */ - private static ThumbnailBounds calculateThumbnailBounds(Model2D model, float thumbWidth, float thumbHeight) { - ThumbnailBounds bounds = new ThumbnailBounds(); - - // 初始化为极值 - bounds.minX = Float.MAX_VALUE; - bounds.maxX = Float.MIN_VALUE; - bounds.minY = Float.MAX_VALUE; - bounds.maxY = Float.MIN_VALUE; - - // 计算模型的世界坐标边界(递归遍历所有部件) - calculateModelBounds(model, bounds, new Matrix3f().identity()); - - // 如果模型没有有效边界,使用默认值 - if (bounds.minX > bounds.maxX) { - bounds.minX = -50f; - bounds.maxX = 50f; - bounds.minY = -50f; - bounds.maxY = 50f; - } - - // 计算模型宽度和高度 - float modelWidth = bounds.maxX - bounds.minX; - float modelHeight = bounds.maxY - bounds.minY; - - // 计算中心点 - float centerX = (bounds.minX + bounds.maxX) * 0.5f; - float centerY = (bounds.minY + bounds.maxY) * 0.5f; - - // 计算缩放比例(考虑边距) - float margin = 0.1f; // 10%边距 - float scaleX = (thumbWidth * (1 - margin)) / modelWidth; - float scaleY = (thumbHeight * (1 - margin)) / modelHeight; - bounds.scale = Math.min(scaleX, scaleY); - - // 计算偏移量(将模型中心对齐到缩略图中心) - bounds.offsetX = -centerX; - bounds.offsetY = -centerY; - - return bounds; - } - - /** - * 递归计算模型的边界 - */ - private static void calculateModelBounds(Model2D model, ThumbnailBounds bounds, Matrix3f parentTransform) { - for (ModelPart part : model.getParts()) { - if (part.getParent() != null) continue; // 只处理根部件 - - // 计算部件的世界变换 - part.updateWorldTransform(parentTransform, false); - Matrix3f worldTransform = part.getWorldTransform(); - - // 计算部件的边界 - calculatePartBounds(part, bounds, worldTransform); - - // 递归处理子部件 - for (ModelPart child : part.getChildren()) { - calculateModelBoundsForPart(child, bounds, worldTransform); - } - } - } - - /** - * 递归计算部件及其子部件的边界 - */ - private static void calculateModelBoundsForPart(ModelPart part, ThumbnailBounds bounds, Matrix3f parentTransform) { - part.updateWorldTransform(parentTransform, false); - Matrix3f worldTransform = part.getWorldTransform(); - - calculatePartBounds(part, bounds, worldTransform); - - for (ModelPart child : part.getChildren()) { - calculateModelBoundsForPart(child, bounds, worldTransform); - } - } - - /** - * 计算单个部件的边界 - */ - private static void calculatePartBounds(ModelPart part, ThumbnailBounds bounds, Matrix3f worldTransform) { - for (Mesh2D mesh : part.getMeshes()) { - if (!mesh.isVisible()) continue; - - // 获取网格的顶点数据 - float[] vertices = mesh.getVertices(); // 假设有这个方法获取原始顶点 - if (vertices == null) continue; - - // 变换顶点并更新边界 - for (int i = 0; i < vertices.length; i += 3) { // 假设顶点格式:x, y, z - float x = vertices[i]; - float y = vertices[i + 1]; - - // 应用世界变换 - Vector3f transformed = new Vector3f(x, y, 1.0f); - worldTransform.transform(transformed); - - // 更新边界 - bounds.minX = Math.min(bounds.minX, transformed.x); - bounds.maxX = Math.max(bounds.maxX, transformed.x); - bounds.minY = Math.min(bounds.minY, transformed.y); - bounds.maxY = Math.max(bounds.maxY, transformed.y); - } - } - } - - /** - * 构建缩略图专用的正交投影矩阵 - */ - private static Matrix3f buildThumbnailProjection(float width, float height) { - Matrix3f m = new Matrix3f(); - // 标准正交投影,不受摄像机影响 - m.set( - 2.0f / width, 0.0f, -1.0f, - 0.0f, -2.0f / height, 1.0f, - 0.0f, 0.0f, 1.0f - ); - return m; - } - - /** - * 缩略图专用的部件渲染 - */ - public static void renderPartForThumbnail(ModelPart part, Matrix3f parentTransform) { - part.updateWorldTransform(parentTransform, false); - Matrix3f world = part.getWorldTransform(); - - setPartUniforms(defaultProgram, part); - setUniformMatrix3(defaultProgram, "uModelMatrix", world); - - for (Mesh2D mesh : part.getMeshes()) { - renderMeshForThumbnail(mesh, world); - } - - for (ModelPart child : part.getChildren()) { - renderPartForThumbnail(child, world); - } - } - - /** - * 缩略图专用的网格渲染 - */ - private static void renderMeshForThumbnail(Mesh2D mesh, Matrix3f modelMatrix) { - if (!mesh.isVisible()) return; - - Matrix3f matToUse = mesh.isBakedToWorld() ? new Matrix3f().identity() : new Matrix3f(modelMatrix); - - if (mesh.getTexture() != null) { - mesh.getTexture().bind(0); - setUniformIntInternal(defaultProgram, "uTexture", 0); - } else { - RenderSystem.bindTexture(defaultTextureId); - setUniformIntInternal(defaultProgram, "uTexture", 0); - } - - setUniformMatrix3(defaultProgram, "uModelMatrix", matToUse); - mesh.draw(defaultProgram.programId, matToUse); - - RenderSystem.checkGLError("renderMeshForThumbnail"); - } - - /** - * 设置缩略图专用的简化光照 - */ - private static void setupThumbnailLighting(ShaderProgram sp, Model2D model) { - List lights = model.getLights(); - int ambientLightCount = 0; - - // 查找环境光 - for (int i = 0; i < lights.size() && ambientLightCount < 1; i++) { - LightSource light = lights.get(i); - if (light.isEnabled() && light.isAmbient()) { - setUniformVec2Internal(sp, "uLightsPos[0]", new Vector2f(0f, 0f)); - setUniformVec3Internal(sp, "uLightsColor[0]", light.getColor()); - setUniformFloatInternal(sp, "uLightsIntensity[0]", light.getIntensity()); - setUniformIntInternal(sp, "uLightsIsAmbient[0]", 1); - setUniformIntInternal(sp, "uLightsIsGlow[0]", 0); - ambientLightCount++; - } - } - - // 如果没有环境光,创建一个默认的环境光 - if (ambientLightCount == 0) { - setUniformVec2Internal(sp, "uLightsPos[0]", new Vector2f(0f, 0f)); - setUniformVec3Internal(sp, "uLightsColor[0]", new Vector3f(0.8f, 0.8f, 0.8f)); - setUniformFloatInternal(sp, "uLightsIntensity[0]", 1.0f); - setUniformIntInternal(sp, "uLightsIsAmbient[0]", 1); - setUniformIntInternal(sp, "uLightsIsGlow[0]", 0); - ambientLightCount = 1; - } - - setUniformIntInternal(sp, "uLightCount", ambientLightCount); - - // 禁用所有其他光源槽位 - for (int i = ambientLightCount; i < MAX_LIGHTS; i++) { - setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", 0f); - setUniformIntInternal(sp, "uLightsIsAmbient[" + i + "]", 0); - } - } - - - /** - * 设置所有非默认着色器的顶点坐标相关uniform - */ - private static void setupNonDefaultShaders(Matrix3f projection, Matrix3f view) { - List shaderList = ShaderManagement.getShaderList(); - if (shaderList == null || shaderList.isEmpty()) { - return; - } - int currentProgram = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM); - try { - for (CompleteShader shader : shaderList) { - if (shader.isDefaultShader()) { - continue; - } - try { - ShaderProgram program = ShaderManagement.getShaderProgram(shader.getShaderName()); - if (program == null || program.programId == 0) { - continue; - } - program.use(); - setUniformMatrix3(program, "uProjectionMatrix", projection); - setUniformMatrix3(program, "uViewMatrix", view); - setUniformMatrix3(program, "uModelMatrix", new Matrix3f().identity()); - setUniformFloatInternal(program, "uCameraZ", camera.getZPosition()); - RenderSystem.checkGLError("setupNonDefaultShaders_" + shader.getShaderName()); - } catch (Exception e) { - logger.warn("Failed to setup non-default shader: {}", shader.getShaderName(), e); - } - } - } finally { - // 恢复之前绑定的着色器程序 - if (currentProgram != 0) { - GL20.glUseProgram(currentProgram); - } - } - } - - private static void renderLightPositions(Model2D model) { - if (!renderLightPositions) return; - // 设置灯泡颜色为光源的颜色 - for (LightSource light : model.getLights()) { - if (!light.isEnabled()) continue; - - // 使用光源的颜色来绘制灯泡 - Vector4f lightColor = new Vector4f(light.getColor().x, light.getColor().y, light.getColor().z, 1.0f); - setUniformVec4Internal(defaultProgram, "uColor", lightColor); - - // 绘制灯泡形状 - drawLightBulb(light.getPosition(), light.getIntensity()); - - if (light.isAmbient()) { - drawCrossMark(light.getPosition(), light.getIntensity()); - } - } - // 恢复原始颜色 - setUniformVec4Internal(defaultProgram, "uColor", new Vector4f(1, 1, 1, 1)); - } - - /** - * 绘制简洁的灯泡形状 - * - * @param position 灯泡位置 - * @param intensity 光源强度,用于控制灯泡大小 - */ - private static void drawLightBulb(Vector2f position, float intensity) { - Tesselator tesselator = Tesselator.getInstance(); - BufferBuilder builder = tesselator.getBuilder(); - float bulbSize = 3.0f + (intensity / 10.0f); - int segments = 16; - builder.begin(RenderSystem.DRAW_TRIANGLE_FAN, segments + 2); - - builder.vertex(position.x, position.y, 0.5f, 0.5f); - - for (int i = 0; i <= segments; i++) { - double angle = 2.0 * Math.PI * i / segments; - float x = position.x + bulbSize * (float) Math.cos(angle); - float y = position.y + bulbSize * (float) Math.sin(angle); - builder.vertex(x, y, 0.5f, 0.5f); - } - tesselator.end(); - } - - /** - * 绘制十字标记(用于环境光) - */ - private static void drawCrossMark(Vector2f position, float size) { - Tesselator tesselator = Tesselator.getInstance(); - BufferBuilder builder = tesselator.getBuilder(); - - float crossSize = size * 0.8f; - - // 绘制水平线 - builder.begin(RenderSystem.DRAW_LINES, 2); - builder.vertex(position.x - crossSize, position.y, 0.5f, 0.5f); - builder.vertex(position.x + crossSize, position.y, 0.5f, 0.5f); - tesselator.end(); - - // 绘制垂直线 - builder.begin(RenderSystem.DRAW_LINES, 2); - builder.vertex(position.x, position.y - crossSize, 0.5f, 0.5f); - builder.vertex(position.x, position.y + crossSize, 0.5f, 0.5f); - tesselator.end(); - } - - /** - * 关键修改点:在渲染前确保更新 part 的 worldTransform, - * 然后直接使用 part.getWorldTransform() 作为 uModelMatrix 传入 shader。 - */ - private static void renderPartRecursive(ModelPart part, Matrix3f parentMat) { - // 确保 part 的 local/world 矩阵被计算(会更新 transformDirty) - part.updateWorldTransform(parentMat, false); - - // 直接使用已经计算好的 worldTransform - Matrix3f world = part.getWorldTransform(); - - // 先设置部件相关的 uniform(opacity / blendMode / color 等) - setPartUniforms(defaultProgram, part); - - // 把 world 矩阵传给 shader(uModelMatrix) - setUniformMatrix3(defaultProgram, "uModelMatrix", world); - - // 绘制本节点的所有 mesh(将 world 传入 renderMesh) - for (Mesh2D mesh : part.getMeshes()) { - renderMesh(mesh, world); - } - - // 递归渲染子节点,继续传入当前 world 作为子节点的 parent - for (ModelPart child : part.getChildren()) { - renderPartRecursive(child, world); - } - } - - private static void renderMesh(Mesh2D mesh, Matrix3f modelMatrix) { - if (!mesh.isVisible()) return; - - // 如果 mesh 已经被烘焙到世界坐标,则传 identity 矩阵给 shader(防止重复变换) - Matrix3f matToUse = mesh.isBakedToWorld() ? new Matrix3f().identity() : new Matrix3f(modelMatrix); - - // 手动应用摄像机偏移 - Vector2f offset = getCameraOffset(); - matToUse.m20(matToUse.m20() - offset.x); - matToUse.m21(matToUse.m21() - offset.y); - // 设置纹理相关的uniform - if (mesh.getTexture() != null) { - mesh.getTexture().bind(0); // 绑定到纹理单元0 - setUniformIntInternal(defaultProgram, "uTexture", 0); - } else { - // 使用默认白色纹理 - RenderSystem.bindTexture(defaultTextureId); - setUniformIntInternal(defaultProgram, "uTexture", 0); - } - - // 将模型矩阵设置为当前 mesh 使用的矩阵(shader 内名为 uModelMatrix) - setUniformMatrix3(defaultProgram, "uModelMatrix", matToUse); - - // 调用 Mesh2D 的 draw 方法,传入当前使用的着色器程序和变换矩阵 - mesh.draw(defaultProgram.programId, matToUse); - - RenderSystem.checkGLError("renderMesh"); - } - - - // ================== 渲染碰撞箱相关实现 ================== - - private static void renderPhysicsColliders(PhysicsSystem physics) { - if (physics == null) { - logger.warn("renderPhysicsColliders: physics system is null"); - return; - } - - // 设置渲染状态 - RenderSystem.checkGLError("before_set_line_width"); - RenderSystem.lineWidth(colliderLineWidth); - RenderSystem.checkGLError("after_set_line_width"); - - RenderSystem.activeTexture(RenderSystem.GL_TEXTURE0); - RenderSystem.bindTexture(defaultTextureId); - RenderSystem.checkGLError("after_bind_texture"); - - setUniformIntInternal(defaultProgram, "uTexture", 0); - setUniformVec4Internal(defaultProgram, "uColor", colliderColor); - setUniformFloatInternal(defaultProgram, "uOpacity", 1.0f); - setUniformIntInternal(defaultProgram, "uBlendMode", 0); - setUniformIntInternal(defaultProgram, "uDebugMode", 0); - RenderSystem.checkGLError("after_set_uniforms"); - - // 使用单位矩阵作为 model(碰撞体顶点按世界坐标提供) - setUniformMatrix3(defaultProgram, "uModelMatrix", new Matrix3f().identity()); - RenderSystem.checkGLError("after_set_model_matrix"); - - List colliders = physics.getColliders(); - if (colliders == null || colliders.isEmpty()) { - logger.debug("No colliders to render"); - return; - } - - int enabledColliders = 0; - for (PhysicsSystem.PhysicsCollider collider : colliders) { - if (collider == null || !collider.isEnabled()) continue; - - RenderSystem.checkGLError("before_render_collider_" + enabledColliders); - - if (collider instanceof PhysicsSystem.CircleCollider c) { - if (c.getCenter() != null && c.getRadius() > 0) { - drawCircleColliderWire(c.getCenter(), c.getRadius()); - enabledColliders++; - } else { - logger.warn("Invalid CircleCollider: center={}, radius={}", c.getCenter(), c.getRadius()); - } - } else if (collider instanceof PhysicsSystem.RectangleCollider r) { - if (r.getCenter() != null && r.getWidth() > 0 && r.getHeight() > 0) { - drawRectangleColliderWire(r.getCenter(), r.getWidth(), r.getHeight()); - enabledColliders++; - } else { - logger.warn("Invalid RectangleCollider: center={}, width={}, height={}", - r.getCenter(), r.getWidth(), r.getHeight()); - } - } else { - logger.warn("Unknown collider type: {}", collider.getClass().getSimpleName()); - } - - RenderSystem.checkGLError("after_render_collider_" + enabledColliders); - } - - logger.debug("Rendered {} enabled colliders", enabledColliders); - - // 恢复默认线宽 - RenderSystem.lineWidth(1.0f); - RenderSystem.checkGLError("after_reset_line_width"); - } - - - /** - * 绘制圆形碰撞框(线框) - * 使用临时 VAO/VBO,每帧创建并删除(简单实现) - */ - private static void drawCircleColliderWire(Vector2f center, float radius) { - int segments = Math.max(8, CIRCLE_SEGMENTS); - - Tesselator tesselator = Tesselator.getInstance(); - BufferBuilder builder = tesselator.getBuilder(); - - builder.begin(RenderSystem.DRAW_LINE_LOOP, segments); - for (int i = 0; i < segments; i++) { - double ang = 2.0 * Math.PI * i / segments; - float x = center.x + radius * (float) Math.cos(ang); - float y = center.y + radius * (float) Math.sin(ang); - builder.vertex(x, y, 0.5f, 0.5f); - } - tesselator.end(); - } - - /** - * 绘制矩形碰撞框(线框) - */ - private static void drawRectangleColliderWire(Vector2f center, float width, float height) { - float halfW = width / 2.0f; - float halfH = height / 2.0f; - - Tesselator tesselator = Tesselator.getInstance(); - BufferBuilder builder = tesselator.getBuilder(); - - builder.begin(RenderSystem.DRAW_LINE_LOOP, 4); - builder.vertex(center.x - halfW, center.y - halfH, 0.5f, 0.5f); - builder.vertex(center.x + halfW, center.y - halfH, 0.5f, 0.5f); - builder.vertex(center.x + halfW, center.y + halfH, 0.5f, 0.5f); - builder.vertex(center.x - halfW, center.y + halfH, 0.5f, 0.5f); - tesselator.end(); - } - - // ================== uniform 设置辅助(内部使用,确保 program 已绑定) ================== - private static void setUniformIntInternal(ShaderProgram sp, String name, int value) { - int loc = sp.getUniformLocation(name); - if (loc != -1) RenderSystem.uniform1i(loc, value); - } - - private static void setUniformVec3Internal(ShaderProgram sp, String name, org.joml.Vector3f vec) { - int loc = sp.getUniformLocation(name); - if (loc != -1) RenderSystem.uniform3f(loc, vec); - } - - private static void setUniformVec2Internal(ShaderProgram sp, String name, org.joml.Vector2f vec) { - int loc = sp.getUniformLocation(name); - if (loc != -1) RenderSystem.uniform2f(loc, vec); - } - - private static void setUniformFloatInternal(ShaderProgram sp, String name, float value) { - int loc = sp.getUniformLocation(name); - if (loc != -1) RenderSystem.uniform1f(loc, value); - } - - private static void setUniformVec4Internal(ShaderProgram sp, String name, org.joml.Vector4f vec) { - int loc = sp.getUniformLocation(name); - if (loc != -1) RenderSystem.uniform4f(loc, vec); - } - - private static void setUniformMatrix3(ShaderProgram sp, String name, org.joml.Matrix3f m) { - int loc = sp.getUniformLocation(name); - if (loc == -1) return; - RenderSystem.uniformMatrix3(loc, m); - } - - // ================== 部件属性 ================== - private static void setPartUniforms(ShaderProgram sp, ModelPart part) { - setUniformFloatInternal(sp, "uOpacity", part.getOpacity()); - int blend = 0; - ModelPart.BlendMode bm = part.getBlendMode(); - if (bm != null) { - switch (bm) { - case ADDITIVE: - blend = 1; - break; - case MULTIPLY: - blend = 2; - break; - case SCREEN: - blend = 3; - break; - case NORMAL: - default: - blend = 0; - } - } else { - blend = 0; - } - setUniformIntInternal(sp, "uBlendMode", blend); - // 这里保留为白色,若需要部件 tint 请替换为 part 的 color 属性 - setUniformVec4Internal(sp, "uColor", new Vector4f(1, 1, 1, 1)); - } - - public static TextRenderer getTextRenderer() { - return defaultTextRenderer; - } - - // ================== 工具 ================== - private static Matrix3f buildOrthoProjection(int width, int height) { - Matrix3f m = new Matrix3f(); - // 这个投影把屏幕像素坐标(x in [0,width], y in [0,height])映射到 NDC [-1,1]x[1,-1] - m.set( - 2.0f / width, 0.0f, -1.0f, - 0.0f, -2.0f / height, 1.0f, - 0.0f, 0.0f, 1.0f - ); - return m; - } - - - /** - * 渲染文字 - * - * @param text 文字内容 - * @param x 世界坐标 X - * @param y 世界坐标 Y ,反转的 - * @param color RGBA 颜色 - */ - public static void renderText(String text, float x, float y, Vector4f color) { - if (!initialized || defaultTextRenderer == null) return; - RenderSystem.assertOnRenderThread(); - Vector2f offset = getCameraOffset(); - float px = x - offset.x; - float py = y - offset.y; - defaultTextRenderer.renderText(text, px, py, color); - } - - public static void renderText(String text, float x, float y, Vector4f color, float scale) { - if (!initialized || defaultTextRenderer == null) return; - RenderSystem.assertOnRenderThread(); - Vector2f offset = getCameraOffset(); - float px = x - offset.x; - float py = y - offset.y; - defaultTextRenderer.renderText(text, px, py, color, scale); - } - - /** - * 获取默认摄像机与当前摄像机之间的偏移量 - * - * @return Vector2f 偏移向量 (dx, dy) - */ - public static Vector2f getCameraOffset() { - float width = viewportWidth; - float height = viewportHeight; - float zoom = camera.getZoom(); - Vector2f pos = camera.getPosition(); - float tx = -1.0f - (2.0f * zoom * pos.x / width); - float ty = 1.0f + (2.0f * zoom * pos.y / height); - float tx0 = -1.0f; - float ty0 = 1.0f; - float offsetX = tx - tx0; - float offsetY = ty - ty0; - offsetX = -offsetX * width / 2.0f / zoom; - offsetY = offsetY * height / 2.0f / zoom; - return new Vector2f(offsetX, offsetY); - } - - public static void setViewport(int width, int height) { - viewportWidth = Math.max(1, width); - viewportHeight = Math.max(1, height); - RenderSystem.viewport(0, 0, viewportWidth, viewportHeight); - } - - // ================== 辅助:外部获取状态 ================== - public static boolean isInitialized() { - return initialized; - } - - public static int getLoadedMeshCount() { - return meshResources.size(); - } - -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java b/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java deleted file mode 100644 index ba417e2..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java +++ /dev/null @@ -1,240 +0,0 @@ -package com.chuangzhou.vivid2D.render; - -import com.chuangzhou.vivid2D.render.model.util.BoundingBox; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; -import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; -import org.joml.Vector2f; -import org.joml.Vector4f; -import org.lwjgl.opengl.GL11; - -/** - * MultiSelectionBoxRenderer — 美观版完整实现 (已适配动态缩放 - 边框与手柄) - * - * 特性: - * - 边框、手柄、中心点的大小都会根据视图缩放动态调整,确保在任何缩放级别下都清晰可见。 - * - 所有元素尽可能在一次 GL_TRIANGLES draw call 中完成,以提高效率。 - */ -public class MultiSelectionBoxRenderer { - - // -------------------- 配置常量 (世界单位) -------------------- - public static final float DEFAULT_DASH_LENGTH = 10.0f; - public static final float DEFAULT_GAP_LENGTH = 6.0f; - - // -------------------- 配置常量 (屏幕像素单位) -------------------- - // 这些值定义了元素在屏幕上看起来应该有多大 - private static final float PIXEL_MAIN_BORDER_THICKNESS = 2.0f; // <-- 新增:主边框的像素厚度 - private static final float PIXEL_HANDLE_CORNER_SIZE = 8.0f; - private static final float PIXEL_HANDLE_MID_SIZE = 6.0f; - private static final float PIXEL_CENTER_LINE_THICKNESS = 1.5f; - private static final float PIXEL_CENTER_CROSS_RADIUS = 7.0f; - - // 颜色 - public static final Vector4f DASHED_BORDER_COLOR = new Vector4f(1.0f, 0.85f, 0.0f, 1.0f); - public static final Vector4f SOLID_BORDER_COLOR_MAIN = new Vector4f(0.0f, 0.92f, 0.94f, 1.0f); - public static final Vector4f HANDLE_COLOR = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); - public static final Vector4f MULTI_SELECTION_HANDLE_COLOR = new Vector4f(1.0f, 0.9f, 0.0f, 1.0f); - public static final Vector4f CENTER_POINT_COLOR = new Vector4f(1.0f, 0.2f, 0.2f, 1.0f); - - /** - * 绘制单选的选择框(主入口) - * - * @param bounds 包围盒(世界坐标) - * @param pivot 旋转中心 / 中心点(世界坐标) - * @param zoom 当前摄像机的缩放值 (e.g., from ModelRender.getCamera().getZoom()) - */ - public static void drawSelectBox(BoundingBox bounds, Vector2f pivot, float zoom) { - if (bounds == null || !bounds.isValid() || zoom <= 1e-6f) return; - - // 根据 zoom 计算所有元素在世界坐标下的实际尺寸 - float worldBorderThickness = PIXEL_MAIN_BORDER_THICKNESS / zoom; - float worldCornerSize = PIXEL_HANDLE_CORNER_SIZE / zoom; - float worldMidSize = PIXEL_HANDLE_MID_SIZE / zoom; - float worldCenterLineThickness = PIXEL_CENTER_LINE_THICKNESS / zoom; - float worldCenterCrossRadius = PIXEL_CENTER_CROSS_RADIUS / zoom; - - float minX = bounds.getMinX(); - float minY = bounds.getMinY(); - float maxX = bounds.getMaxX(); - float maxY = bounds.getMaxY(); - - Tesselator tesselator = Tesselator.getInstance(); - BufferBuilder bb = tesselator.getBuilder(); - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - - // 将所有绘制合并到一次 TRIANGLES 调用中 - // 预估顶点数:4条边*6 + 8个手柄*6 + 中心十字*6*2 = 24 + 48 + 12 = 84 - bb.begin(RenderSystem.GL_TRIANGLES, 96); - - // 1. 绘制有厚度的边框 - bb.setColor(SOLID_BORDER_COLOR_MAIN); - addQuadLine(bb, minX, minY, maxX, minY, worldBorderThickness); // 上边 - addQuadLine(bb, maxX, minY, maxX, maxY, worldBorderThickness); // 右边 - addQuadLine(bb, maxX, maxY, minX, maxY, worldBorderThickness); // 下边 - addQuadLine(bb, minX, maxY, minX, minY, worldBorderThickness); // 左边 - - // 2. 绘制手柄 - bb.setColor(HANDLE_COLOR); - addHandleQuad(bb, minX, minY, worldCornerSize); - addHandleQuad(bb, maxX, minY, worldCornerSize); - addHandleQuad(bb, minX, maxY, worldCornerSize); - addHandleQuad(bb, maxX, maxY, worldCornerSize); - addHandleQuad(bb, (minX + maxX) * 0.5f, minY, worldMidSize); - addHandleQuad(bb, (minX + maxX) * 0.5f, maxY, worldMidSize); - addHandleQuad(bb, minX, (minY + maxY) * 0.5f, worldMidSize); - addHandleQuad(bb, maxX, (minY + maxY) * 0.5f, worldMidSize); - - // 3. 绘制中心点 - bb.setColor(CENTER_POINT_COLOR); - addQuadLine(bb, pivot.x - worldCenterCrossRadius, pivot.y, pivot.x + worldCenterCrossRadius, pivot.y, worldCenterLineThickness); - addQuadLine(bb, pivot.x, pivot.y - worldCenterCrossRadius, pivot.x, pivot.y + worldCenterCrossRadius, worldCenterLineThickness); - - tesselator.end(); - } - - /** - * 绘制多选框(虚线 + 手柄) - * - * @param multiBounds 多选包围盒 - * @param zoom 当前摄像机的缩放值 - */ - public static void drawMultiSelectionBox(BoundingBox multiBounds, float zoom) { - if (multiBounds == null || !multiBounds.isValid() || zoom <= 1e-6f) return; - - // 根据 zoom 计算所有元素在世界坐标下的实际尺寸 - float worldBorderThickness = PIXEL_MAIN_BORDER_THICKNESS / zoom; - float worldCornerSize = PIXEL_HANDLE_CORNER_SIZE / zoom; - float worldMidSize = PIXEL_HANDLE_MID_SIZE / zoom; - float worldCenterLineThickness = PIXEL_CENTER_LINE_THICKNESS / zoom; - float worldCenterCrossRadius = PIXEL_CENTER_CROSS_RADIUS / zoom; - - float minX = multiBounds.getMinX(); - float minY = multiBounds.getMinY(); - float maxX = multiBounds.getMaxX(); - float maxY = multiBounds.getMaxY(); - - Tesselator tesselator = Tesselator.getInstance(); - BufferBuilder bb = tesselator.getBuilder(); - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - - // 合并所有绘制 - int estimatedSegments = Math.max(4, (int) Math.ceil((2f * (multiBounds.getWidth() + multiBounds.getHeight())) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH))); - bb.begin(RenderSystem.GL_TRIANGLES, estimatedSegments * 6 * 4 + 96); // 넉넉하게 할당 - - // 1. 绘制有厚度的虚线边框 - bb.setColor(DASHED_BORDER_COLOR); - addThickDashedLine(bb, minX, minY, maxX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH, worldBorderThickness); - addThickDashedLine(bb, maxX, minY, maxX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH, worldBorderThickness); - addThickDashedLine(bb, maxX, maxY, minX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH, worldBorderThickness); - addThickDashedLine(bb, minX, maxY, minX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH, worldBorderThickness); - - // 2. 绘制手柄 - bb.setColor(MULTI_SELECTION_HANDLE_COLOR); - addHandleQuad(bb, minX, minY, worldCornerSize); - addHandleQuad(bb, maxX, minY, worldCornerSize); - addHandleQuad(bb, minX, maxY, worldCornerSize); - addHandleQuad(bb, maxX, maxY, worldCornerSize); - addHandleQuad(bb, (minX + maxX) * 0.5f, minY, worldMidSize); - addHandleQuad(bb, (minX + maxX) * 0.5f, maxY, worldMidSize); - addHandleQuad(bb, minX, (minY + maxY) * 0.5f, worldMidSize); - addHandleQuad(bb, maxX, (minY + maxY) * 0.5f, worldMidSize); - - // 3. 绘制中心点 - Vector2f center = multiBounds.getCenter(); - bb.setColor(CENTER_POINT_COLOR); - addQuadLine(bb, center.x - worldCenterCrossRadius, center.y, center.x + worldCenterCrossRadius, center.y, worldCenterLineThickness); - addQuadLine(bb, center.x, center.y - worldCenterCrossRadius, center.x, center.y + worldCenterCrossRadius, worldCenterLineThickness); - - tesselator.end(); - } - - - // -------------------- 辅助绘图方法 -------------------- - - /** - * (新增) 在两点之间生成有厚度的虚线段 (使用 GL_TRIANGLES) - * - * @param dashLen 虚线长度(世界坐标) - * @param gapLen 间隙长度(世界坐标) - * @param thickness 虚线的厚度(世界坐标) - */ - private static void addThickDashedLine(BufferBuilder bb, float startX, float startY, float endX, float endY, - float dashLen, float gapLen, float thickness) { - float dx = endX - startX; - float dy = endY - startY; - float len = (float) Math.sqrt(dx * dx + dy * dy); - if (len < 1e-6f) return; - - float dirX = dx / len, dirY = dy / len; - float segment = dashLen + gapLen; - int count = Math.max(1, (int) Math.ceil(len / segment)); - - for (int i = 0; i < count; i++) { - float s = i * segment; - if (s >= len) break; - float e = Math.min(s + dashLen, len); - - float sx = startX + dirX * s; - float sy = startY + dirY * s; - float ex = startX + dirX * e; - float ey = startY + dirY * e; - - // 为每一小段虚线绘制一个有厚度的四边形 - addQuadLine(bb, sx, sy, ex, ey, thickness); - } - } - - /** - * 手柄:以中心点绘制正方形手柄(填充) - */ - private static void addHandleQuad(BufferBuilder bb, float cx, float cy, float size) { - float half = size * 0.5f; - addFilledQuad(bb, cx - half, cy - half, cx + half, cy + half); - } - - /** - * 绘制一条由四边形模拟的线段(厚度可控) - */ - public static void addQuadLine(BufferBuilder bb, float x0, float y0, float x1, float y1, float thickness) { - float dx = x1 - x0; - float dy = y1 - y0; - float len = (float) Math.sqrt(dx * dx + dy * dy); - if (len < 1e-6f) return; - - float halfThick = thickness * 0.5f; - // 计算线段的法线向量 - float nx = -dy / len * halfThick; - float ny = dx / len * halfThick; - - // 计算四边形的四个顶点 - float v0x = x0 + nx; float v0y = y0 + ny; - float v1x = x1 + nx; float v1y = y1 + ny; - float v2x = x1 - nx; float v2y = y1 - ny; - float v3x = x0 - nx; float v3y = y0 - ny; - - addQuadVertices(bb, v0x, v0y, v1x, v1y, v2x, v2y, v3x, v3y); - } - - /** - * 添加一个填充四边形(用两个三角形表示) - */ - public static void addFilledQuad(BufferBuilder bb, float x0, float y0, float x1, float y1) { - addQuadVertices(bb, x0, y0, x1, y0, x1, y1, x0, y1); - } - - /** - * 辅助方法:添加构成四边形的6个顶点 - */ - private static void addQuadVertices(BufferBuilder bb, float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3) { - // tri 1 - bb.vertex(x0, y0, 0.0f, 0.0f); - bb.vertex(x1, y1, 0.0f, 0.0f); - bb.vertex(x2, y2, 0.0f, 0.0f); - // tri 2 - bb.vertex(x2, y2, 0.0f, 0.0f); - bb.vertex(x3, y3, 0.0f, 0.0f); - bb.vertex(x0, y0, 0.0f, 0.0f); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java b/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java deleted file mode 100644 index e2e5590..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java +++ /dev/null @@ -1,336 +0,0 @@ -package com.chuangzhou.vivid2D.render; - -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; -import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; -import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement; -import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram; -import org.joml.Vector4f; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; -import org.lwjgl.opengl.GL30; -import org.lwjgl.opengl.GL33; -import org.lwjgl.stb.STBTTAlignedQuad; -import org.lwjgl.stb.STBTTBakedChar; -import org.lwjgl.system.MemoryStack; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.ByteBuffer; - -import static org.lwjgl.stb.STBTruetype.stbtt_BakeFontBitmap; -import static org.lwjgl.stb.STBTruetype.stbtt_GetBakedQuad; - -/** - * 支持 ASCII + 中文的 OpenGL 文本渲染器 - * - * @author tzdwindows 7 - */ -public final class TextRenderer { - private static final Logger logger = LoggerFactory.getLogger(TextRenderer.class); - - private final int bitmapWidth; - private final int bitmapHeight; - private final int firstChar; - private final int charCount; - - private STBTTBakedChar.Buffer asciiCharData; - private STBTTBakedChar.Buffer chineseCharData; - private int asciiTextureId; - private int chineseTextureId; - - private boolean initialized = false; - - // 中文字符起始编码(选择一个不冲突的范围) - private static final int CHINESE_FIRST_CHAR = 0x4E00; // CJK Unified Ideographs 常用汉字起始范围 - private static final int CHINESE_CHAR_COUNT = 20000; - - public TextRenderer(int bitmapWidth, int bitmapHeight, int firstChar, int charCount) { - this.bitmapWidth = bitmapWidth; - this.bitmapHeight = bitmapHeight; - this.firstChar = firstChar; - this.charCount = charCount; - } - - /** - * 初始化字体渲染器 - */ - public void initialize(ByteBuffer fontData, float fontHeight) { - if (initialized) { - logger.warn("TextRenderer already initialized"); - return; - } - if (fontData == null || fontData.capacity() == 0 || fontHeight <= 0) { - logger.error("Invalid font data or font height"); - return; - } - - ShaderProgram shader = ShaderManagement.getShaderProgram("TextShader"); - if (shader == null) { - logger.error("TextShader not found"); - return; - } - - shader.use(); - - try { - asciiCharData = STBTTBakedChar.malloc(charCount); - ByteBuffer asciiBitmap = ByteBuffer.allocateDirect(bitmapWidth * bitmapHeight); - int asciiRes = stbtt_BakeFontBitmap(fontData, fontHeight, asciiBitmap, - bitmapWidth, bitmapHeight, firstChar, asciiCharData); - if (asciiRes <= 0) { - logger.error("ASCII font bake failed, result: {}", asciiRes); - cleanup(); - return; - } - asciiTextureId = createTextureFromBitmap(bitmapWidth, bitmapHeight, asciiBitmap); - if (asciiTextureId == 0) { - logger.error("Failed to create ASCII texture"); - cleanup(); - return; - } - - // 烘焙中文 - 使用更大的纹理和正确的字符范围 - int chineseTexSize = 4096; // 中文字符需要更大的纹理 - // 分配足够的空间来存储 CHINESE_CHAR_COUNT 个字符的数据 - chineseCharData = STBTTBakedChar.malloc(CHINESE_CHAR_COUNT); - ByteBuffer chineseBitmap = ByteBuffer.allocateDirect(chineseTexSize * chineseTexSize); - - // 关键:烘焙从 CHINESE_FIRST_CHAR 开始的 CHINESE_CHAR_COUNT 个连续字符 - int chineseRes = stbtt_BakeFontBitmap(fontData, fontHeight, chineseBitmap, - chineseTexSize, chineseTexSize, CHINESE_FIRST_CHAR, chineseCharData); - if (chineseRes <= 0) { - logger.error("Chinese font bake failed, result: {}", chineseRes); - cleanup(); - return; - } - chineseTextureId = createTextureFromBitmap(chineseTexSize, chineseTexSize, chineseBitmap); - if (chineseTextureId == 0) { - logger.error("Failed to create Chinese texture"); - cleanup(); - return; - } - - initialized = true; - logger.debug("TextRenderer initialized, ASCII tex={}, Chinese tex={}", asciiTextureId, chineseTextureId); - - } catch (Exception e) { - logger.error("Exception during TextRenderer init: {}", e.getMessage(), e); - cleanup(); - } finally { - shader.stop(); - } - } - - /** - * 渲染文字 - */ - public void renderText(String text, float x, float y, Vector4f color) { - renderText(text, x, y, color, 1.0f); - } - - /** - * 获取一行文字的宽度(单位:像素) - */ - public float getTextWidth(String text) { - return getTextWidth(text, 1.0f); - } - - /** - * 获取一行文字的宽度(带缩放) - */ - public float getTextWidth(String text, float scale) { - if (!initialized || text == null || text.isEmpty()) return 0f; - - float width = 0f; - - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (c >= firstChar && c < firstChar + charCount) { - STBTTBakedChar bakedChar = asciiCharData.get(c - firstChar); - width += bakedChar.xadvance() * scale; - } else { - // 修复中文索引逻辑:检查字符是否在烘焙的连续范围内 - if (c >= CHINESE_FIRST_CHAR && c < CHINESE_FIRST_CHAR + CHINESE_CHAR_COUNT) { - int idx = c - CHINESE_FIRST_CHAR; // 关键:使用 Unicode 差值作为索引 - STBTTBakedChar bakedChar = chineseCharData.get(idx); - width += bakedChar.xadvance() * scale; - } else { - // 对于未找到的字符,使用空格宽度 - width += 0.5f * scale; // 估计值 - } - } - } - - return width; - } - - public void renderText(String text, float x, float y, Vector4f color, float scale) { - if (!initialized || text == null || text.isEmpty()) return; - if (scale <= 0f) scale = 1.0f; - - RenderSystem.assertOnRenderThread(); - RenderSystem.pushState(); - try { - ShaderProgram shader = ShaderManagement.getShaderProgram("TextShader"); - if (shader == null) { - logger.error("TextShader not found"); - return; - } - shader.use(); - ShaderManagement.setUniformVec4(shader, "uColor", color); - ShaderManagement.setUniformInt(shader, "uTexture", 0); - - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - RenderSystem.disableDepthTest(); - - try (MemoryStack stack = MemoryStack.stackPush()) { - STBTTAlignedQuad q = STBTTAlignedQuad.malloc(stack); - float[] xpos = {x}; - float[] ypos = {y}; - - Tesselator t = Tesselator.getInstance(); - BufferBuilder builder = t.getBuilder(); - - // 按字符类型分组渲染以减少纹理切换 - int currentTexture = -1; - boolean batchStarted = false; - builder.setColor(color); - - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - int targetTexture; - STBTTBakedChar.Buffer charBuffer; - int texWidth, texHeight; - - if (c >= firstChar && c < firstChar + charCount) { - targetTexture = asciiTextureId; - charBuffer = asciiCharData; - texWidth = bitmapWidth; - texHeight = bitmapHeight; - stbtt_GetBakedQuad(charBuffer, texWidth, texHeight, c - firstChar, xpos, ypos, q, true); - } else { - // 修复中文索引逻辑:检查字符是否在烘焙的连续范围内 - if (c >= CHINESE_FIRST_CHAR && c < CHINESE_FIRST_CHAR + CHINESE_CHAR_COUNT) { - targetTexture = chineseTextureId; - charBuffer = chineseCharData; - texWidth = 4096; - texHeight = 4096; - // 关键修复:索引是字符的 Unicode 减去起始 Unicode - int idx = c - CHINESE_FIRST_CHAR; - stbtt_GetBakedQuad(charBuffer, texWidth, texHeight, idx, xpos, ypos, q, true); - } else { - continue; // 跳过不支持的字符 - } - } - - // 如果纹理改变,结束当前批次 - if (targetTexture != currentTexture) { - if (batchStarted) { - t.end(); - batchStarted = false; - } - RenderSystem.bindTexture(targetTexture); - currentTexture = targetTexture; - } - - // 开始新批次(如果需要) - if (!batchStarted) { - builder.begin(RenderSystem.DRAW_TRIANGLES, (text.length() - i) * 6); - batchStarted = true; - } - - // 应用缩放并计算顶点 - float sx0 = x + (q.x0() - x) * scale; - float sx1 = x + (q.x1() - x) * scale; - float sy0 = y + (q.y0() - y) * scale; - float sy1 = y + (q.y1() - y) * scale; - - builder.vertex(sx0, sy0, q.s0(), q.t0()); - builder.vertex(sx1, sy0, q.s1(), q.t0()); - builder.vertex(sx0, sy1, q.s0(), q.t1()); - - builder.vertex(sx1, sy0, q.s1(), q.t0()); - builder.vertex(sx1, sy1, q.s1(), q.t1()); - builder.vertex(sx0, sy1, q.s0(), q.t1()); - } - - // 结束最后一个批次 - if (batchStarted) { - t.end(); - } - } - - } catch (Exception e) { - logger.error("Error rendering text: {}", e.getMessage(), e); - } finally { - RenderSystem.popState(); - } - } - - private int createTextureFromBitmap(int width, int height, ByteBuffer pixels) { - RenderSystem.assertOnRenderThread(); - try { - int textureId = RenderSystem.genTextures(); - RenderSystem.bindTexture(textureId); - - RenderSystem.pixelStore(GL11.GL_UNPACK_ALIGNMENT, 1); - RenderSystem.texImage2D(GL11.GL_TEXTURE_2D, 0, GL30.GL_R8, width, height, 0, - GL11.GL_RED, GL11.GL_UNSIGNED_BYTE, pixels); - - RenderSystem.setTextureMinFilter(GL11.GL_LINEAR); - RenderSystem.setTextureMagFilter(GL11.GL_LINEAR); - RenderSystem.setTextureWrapS(GL12.GL_CLAMP_TO_EDGE); - RenderSystem.setTextureWrapT(GL12.GL_CLAMP_TO_EDGE); - - // 设置纹理swizzle以便单通道纹理在着色器中显示为白色 - RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_R, GL11.GL_RED); - RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_G, GL11.GL_RED); - RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_B, GL11.GL_RED); - RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_A, GL11.GL_RED); - - RenderSystem.pixelStore(GL11.GL_UNPACK_ALIGNMENT, 4); - RenderSystem.bindTexture(0); - - return textureId; - } catch (Exception e) { - logger.error("Failed to create texture from bitmap: {}", e.getMessage(), e); - return 0; - } - } - - public void cleanup() { - RenderSystem.assertOnRenderThread(); - if (asciiTextureId != 0) { - RenderSystem.deleteTextures(asciiTextureId); - asciiTextureId = 0; - } - if (chineseTextureId != 0) { - RenderSystem.deleteTextures(chineseTextureId); - chineseTextureId = 0; - } - if (asciiCharData != null) { - asciiCharData.free(); - asciiCharData = null; - } - if (chineseCharData != null) { - chineseCharData.free(); - chineseCharData = null; - } - initialized = false; - logger.debug("TextRenderer cleaned up"); - } - - public boolean isInitialized() { - return initialized; - } - - public int getAsciiTextureId() { - return asciiTextureId; - } - - public int getChineseTextureId() { - return chineseTextureId; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/EventPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/EventPanel.java deleted file mode 100644 index 9218326..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/EventPanel.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -import javax.swing.*; - -public class EventPanel extends JPanel { -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeDetailsDialog.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeDetailsDialog.java deleted file mode 100644 index 4034fec..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeDetailsDialog.java +++ /dev/null @@ -1,685 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement; -import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement.Parameter; -import com.chuangzhou.vivid2D.render.model.AnimationParameter; -import com.chuangzhou.vivid2D.render.model.ModelPart; - -import javax.swing.*; -import javax.swing.border.Border; -import javax.swing.border.EmptyBorder; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.TableCellEditor; -import javax.swing.table.TableCellRenderer; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.SortedSet; -import java.util.stream.Collectors; - -/** - * 用于编辑单个关键帧值并显示同一时间点其他参数信息的对话框。 - */ -public class KeyframeDetailsDialog extends JDialog { - - private static final Color COLOR_BACKGROUND = new Color(50, 50, 50); - private static final Color COLOR_FOREGROUND = new Color(220, 220, 220); - private static final Color COLOR_HEADER = new Color(70, 70, 70); - private static final Color COLOR_ACCENT_1 = new Color(230, 80, 80); // 用于删除按钮 - private static final Color COLOR_ACCENT_2 = new Color(80, 150, 230); - private static final Color COLOR_GRID = new Color(60, 60, 60); - private static final Border DIALOG_PADDING = new EmptyBorder(10, 10, 10, 10); - private static final float FLOAT_TOLERANCE = 1e-6f; // 浮点数比较容差 - - private final AnimationParameter parameter; - private final ParametersManagement parametersManagement; - private final ModelPart modelPart; - private final SortedSet keyframesSet; - private final Float originalValue; - private Float confirmedValue = null; - - private final JTextField valueField = new JTextField(15); - private final JSlider valueSlider = new JSlider(0, 1000); // 使用 0-1000 归一化 - - // [新增] 搜索字段 - private final JTextField searchField = new JTextField(15); - - // 内部类,用于存储和显示同一时间点的其他参数信息 - private final RelatedParametersTableModel relatedTableModel; - private final JTable relatedTable; - - // [新增] 用于存储所有相关参数的完整列表(过滤前) - private List allRelatedParameters = new ArrayList<>(); - - - public KeyframeDetailsDialog(Window owner, AnimationParameter parameter, Float value, SortedSet keyframesSet, - ParametersManagement parametersManagement, ModelPart modelPart) { - super(owner, "编辑关键帧: " + parameter.getId(), ModalityType.APPLICATION_MODAL); - this.parameter = parameter; - this.originalValue = value; - this.keyframesSet = keyframesSet; - this.parametersManagement = parametersManagement; - this.modelPart = modelPart; - - // 字段初始化 - // [修改] 传递中文 ID 映射 - this.relatedTableModel = new RelatedParametersTableModel(modelPart, parametersManagement, this::refreshTableData); - this.relatedTable = new JTable(relatedTableModel); - - initUI(); - loadData(value); - fetchRelatedParameters(); // 查询并加载所有相关参数,并初始化表格 - } - - // 已修改的 record:新增 recordIndex,用于精确指向 ModelPart 完整记录中的条目 - private record RelatedParameterInfo(String paramId, Object value, int recordIndex) {} - - // ---------------------------------------------------------------------------------- - // 内部类:RelatedParametersTableModel (处理表格数据和删除逻辑) - // ---------------------------------------------------------------------------------- - private class RelatedParametersTableModel extends AbstractTableModel { - private final String[] columnNames = {"参数 ID", "值", "操作"}; - private final List data = new ArrayList<>(); - private final ModelPart modelPart; - private final ParametersManagement management; - private final Runnable refreshCallback; - - // [新增] 参数ID 中文映射 - private final Map paramIdMap; - - - public RelatedParametersTableModel(ModelPart modelPart, ParametersManagement management, Runnable refreshCallback) { - this.modelPart = modelPart; - this.management = management; - this.refreshCallback = refreshCallback; - // 初始化中文映射 - this.paramIdMap = new HashMap<>(); - this.paramIdMap.put("position", "位置"); - this.paramIdMap.put("rotate", "旋转"); - //this.paramIdMap.put("secondaryVertex", "二级顶点变形器(顶点位置)"); - this.paramIdMap.put("meshVertices", "变形器(顶点位置)"); - this.paramIdMap.put("scale", "缩放"); - } - - public void setData(List list) { - data.clear(); - data.addAll(list); - fireTableDataChanged(); - } - - @Override - public int getRowCount() { return data.size(); } - @Override - public int getColumnCount() { return columnNames.length; } - @Override - public String getColumnName(int column) { return columnNames[column]; } - - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - RelatedParameterInfo info = data.get(rowIndex); - return switch (columnIndex) { - case 0 -> getDisplayParamId(info.paramId()); // [修改] 使用显示 ID - case 1 -> info.value(); - case 2 -> "删除"; // Button text - default -> null; - }; - } - - /** - * [新增] 获取用于显示的参数 ID (中文映射) - */ - private String getDisplayParamId(String paramId) { - return paramIdMap.getOrDefault(paramId, paramId); - } - - @Override - public boolean isCellEditable(int rowIndex, int columnIndex) { - // "值"列 (1) 不可编辑,"操作"列 (2) 可点击 (仅允许删除) - return columnIndex == 2; - } - - // 移除 setValueAt 方法,禁用修改功能。 - - /** - * 处理单行删除操作 (用于按钮点击)。 - */ - public void deleteRow(int rowIndex) { - if (rowIndex >= 0 && rowIndex < data.size()) { - RelatedParameterInfo info = data.get(rowIndex); - String paramId = info.paramId(); - int recordIndex = info.recordIndex(); - - int confirm = JOptionPane.showConfirmDialog(KeyframeDetailsDialog.this, - String.format("确定要删除参数 '%s' 的此关键帧记录吗?", getDisplayParamId(paramId)), - "确认删除", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); - - if (confirm == JOptionPane.YES_OPTION) { - // 调用 ParametersManagement 的新 API 进行精确删除 - management.removeParameterAt(modelPart, recordIndex); - - // [修改] 不直接调用 fetchRelatedParameters,而是调用 refreshCallback,它会重新查询并刷新 UI - refreshCallback.run(); - } - } - } - - /** - * 处理多行删除操作 (用于快捷键)。 - * @param modelRows 模型索引数组。 - */ - public void deleteRows(int[] modelRows) { - if (modelRows.length == 0) return; - - // 提取要删除的 ModelPart 记录的原始索引,并按降序排序。 - List recordIndices = Arrays.stream(modelRows) - .mapToObj(data::get) - .map(RelatedParameterInfo::recordIndex) - .sorted(Collections.reverseOrder()) - .collect(Collectors.toList()); - - // 确认对话框 - int confirm = JOptionPane.showConfirmDialog(KeyframeDetailsDialog.this, - String.format("确定要删除选中的 %d 个关键帧参数记录吗?", recordIndices.size()), - "确认批量删除", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); - - if (confirm == JOptionPane.YES_OPTION) { - for (int index : recordIndices) { - // 调用 ParametersManagement 的新 API 进行精确删除 - management.removeParameterAt(modelPart, index); - } - - // [修改] 重新获取数据以刷新 UI (只需要一次) - refreshCallback.run(); - } - } - } - - // ---------------------------------------------------------------------------------- - // 内部类:ButtonRenderer & ButtonEditor (处理删除按钮) - // ---------------------------------------------------------------------------------- - private class ButtonRenderer extends JButton implements TableCellRenderer { - public ButtonRenderer() { - setOpaque(true); - setBackground(COLOR_ACCENT_1); - setForeground(Color.WHITE); - setFocusPainted(false); - setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(COLOR_GRID), - new EmptyBorder(0, 0, 0, 0) - )); - } - - @Override - public Component getTableCellRendererComponent(JTable table, Object value, - boolean isSelected, boolean hasFocus, int row, int column) { - setText((value == null) ? "" : value.toString()); - return this; - } - } - - private class ButtonEditor extends AbstractCellEditor implements TableCellEditor { - private final JButton button; - private int currentRow; - - public ButtonEditor() { - button = new JButton(); - button.setOpaque(true); - button.setBackground(COLOR_ACCENT_1.darker()); - button.setForeground(Color.WHITE); - button.setFocusPainted(false); - button.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(COLOR_GRID), - new EmptyBorder(0, 0, 0, 0) - )); - - button.addActionListener((ActionEvent e) -> { - // 在 Event Dispatch Thread 中执行删除逻辑 - SwingUtilities.invokeLater(() -> { - fireEditingStopped(); - relatedTableModel.deleteRow(currentRow); // 调用单行删除 - }); - }); - } - - @Override - public Component getTableCellEditorComponent(JTable table, Object value, - boolean isSelected, int row, int column) { - currentRow = row; - button.setText((value == null) ? "" : value.toString()); - return button; - } - - @Override - public Object getCellEditorValue() { - return button.getText(); - } - } - - - /** - * [修改] 查询在当前关键帧值处,ModelPart 中所有已记录的参数变化,并存储在 allRelatedParameters 中。 - */ - private void fetchRelatedParameters() { - // 获取该 ModelPart 的所有历史记录 - Parameter fullRecord = parametersManagement.getModelPartParameters(modelPart); - if (fullRecord == null) { - allRelatedParameters = new ArrayList<>(); - relatedTableModel.setData(allRelatedParameters); // 刷新表格 - return; - } - - List anims = fullRecord.animationParameter(); - List keyframes = fullRecord.keyframe(); - List paramIds = fullRecord.paramId(); - List values = fullRecord.value(); - - int size = Math.min(keyframes.size(), Math.min(anims.size(), Math.min(paramIds.size(), values.size()))); - - List related = new ArrayList<>(); - - for (int i = 0; i < size; i++) { - Float currentKeyframe = keyframes.get(i); - AnimationParameter recordAnimParam = anims.get(i); - - // 检查 1 (参数): 使用 equals 判断 AnimationParameter 是否与当前编辑的参数相等 - boolean isSameAnimationParameter = recordAnimParam != null && recordAnimParam.equals(parameter); - - // 检查 2 (时间点): 使用 Objects.equals 判断 keyframe 是否与 originalValue 相等 (处理 null) - boolean isSameKeyframe = Objects.equals(currentKeyframe, originalValue); - - if (isSameAnimationParameter && isSameKeyframe) { - // [修改] 不再排除当前正在编辑的参数本身 - related.add(new RelatedParameterInfo( - paramIds.get(i), - values.get(i), - i // <-- 记录此条目在 ModelPart 完整记录中的原始索引 i - )); - } - } - allRelatedParameters = related; // 存储完整列表 - applyFilter(); // 应用过滤器刷新表格 - } - - /** - * [新增] 根据搜索框内容过滤 allRelatedParameters 并更新表格。 - */ - private void applyFilter() { - String searchText = searchField.getText().trim().toLowerCase(); - - if (searchText.isEmpty()) { - relatedTableModel.setData(allRelatedParameters); - return; - } - - List filteredList = allRelatedParameters.stream() - .filter(info -> { - // 过滤逻辑:匹配原始 ID 或中文显示 ID - String paramId = info.paramId().toLowerCase(); - String displayId = relatedTableModel.getDisplayParamId(info.paramId()).toLowerCase(); - - return paramId.contains(searchText) || displayId.contains(searchText); - }) - .collect(Collectors.toList()); - - relatedTableModel.setData(filteredList); - } - - /** - * [新增] 重新查询所有数据并应用当前过滤器。用于删除操作后的刷新。 - */ - private void refreshTableData() { - fetchRelatedParameters(); - } - - - // 处理表格选中的多行删除 - private void deleteSelectedRelatedRows() { - int[] selectedViewRows = relatedTable.getSelectedRows(); - if (selectedViewRows.length == 0) { - return; - } - - // 转换视图索引到模型索引 - int[] modelRowsToDelete = new int[selectedViewRows.length]; - for(int i = 0; i < selectedViewRows.length; i++) { - modelRowsToDelete[i] = relatedTable.convertRowIndexToModel(selectedViewRows[i]); - } - - // 调用批量删除方法。确认框已移至 model 内部。 - relatedTableModel.deleteRows(modelRowsToDelete); - } - - - private void initUI() { - setSize(550, 450); - setMinimumSize(new Dimension(500, 350)); - setLocationRelativeTo(getOwner()); - getContentPane().setBackground(COLOR_BACKGROUND); - setLayout(new BorderLayout(10, 10)); - ((JPanel) getContentPane()).setBorder(DIALOG_PADDING); - - // --- 顶部信息面板 (ID, Range, Default) --- - JPanel topPanel = new JPanel(new GridLayout(4, 2, 5, 5)); - topPanel.setBackground(COLOR_BACKGROUND); - topPanel.add(createLabel("参数 ID:")); - // [修改] 显示参数 ID 的中文映射 - topPanel.add(createValueLabel(relatedTableModel.getDisplayParamId(parameter.getId()))); - topPanel.add(createLabel("Model Part:")); - topPanel.add(createValueLabel(modelPart != null ? modelPart.getName() : "N/A")); - topPanel.add(createLabel("值域:")); - String range = String.format("[%.3f, %.3f]", parameter.getMinValue(), parameter.getMaxValue()); - topPanel.add(createValueLabel(range)); - topPanel.add(createLabel("默认值:")); - topPanel.add(createValueLabel(String.format("%.3f", parameter.getDefaultValue()))); - - add(topPanel, BorderLayout.NORTH); - - // --- 中部主编辑/信息区 --- - JSplitPane centerSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT); - centerSplit.setOpaque(false); - centerSplit.setDividerLocation(150); - centerSplit.setDividerSize(5); - centerSplit.setBorder(null); - - - // 1. 关键帧值编辑面板 - JPanel editPanel = createEditPanel(); - centerSplit.setTopComponent(editPanel); - - // 2. 相关参数列表面板 - JPanel listPanel = createRelatedParametersListPanel(); - centerSplit.setBottomComponent(listPanel); - - add(centerSplit, BorderLayout.CENTER); - - - // --- 底部操作栏 --- - add(createBottomPanel(), BorderLayout.SOUTH); - - // Esc 键关闭 = Cancel - getRootPane().registerKeyboardAction(e -> onCancel(), - KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), - JComponent.WHEN_IN_FOCUSED_WINDOW); - } - - private JPanel createEditPanel() { - JPanel panel = new JPanel(new GridBagLayout()); - panel.setBackground(COLOR_BACKGROUND); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(COLOR_GRID), "关键帧值编辑", - 0, 0, getFont(), COLOR_ACCENT_2 - )); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(5, 5, 5, 5); - gbc.fill = GridBagConstraints.HORIZONTAL; - - // 标签 - gbc.gridx = 0; - gbc.gridy = 0; - gbc.weightx = 0.0; - panel.add(createLabel("关键帧值:"), gbc); - - // 文本字段 - styleTextField(valueField); - valueField.addActionListener(e -> updateSliderFromField()); - valueField.addFocusListener(new java.awt.event.FocusAdapter() { - @Override - public void focusLost(java.awt.event.FocusEvent e) { - updateSliderFromField(); - } - }); - gbc.gridx = 1; - gbc.gridy = 0; - gbc.weightx = 1.0; - panel.add(valueField, gbc); - - // 滑块 - valueSlider.setBackground(COLOR_BACKGROUND); - valueSlider.setForeground(COLOR_FOREGROUND); - valueSlider.setPaintTicks(false); - valueSlider.setPaintLabels(false); - valueSlider.addChangeListener(e -> updateFieldFromSlider()); - gbc.gridx = 0; - gbc.gridy = 1; - gbc.gridwidth = 2; - gbc.weightx = 1.0; - panel.add(valueSlider, gbc); - - // 底部留白 - gbc.gridx = 0; - gbc.gridy = 2; - gbc.gridwidth = 2; - gbc.weighty = 1.0; - panel.add(new JPanel() {{ setOpaque(false); }}, gbc); - - return panel; - } - - private JPanel createRelatedParametersListPanel() { - JPanel panel = new JPanel(new BorderLayout(5, 5)); // [修改] 增加边距 - panel.setBackground(COLOR_BACKGROUND); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(COLOR_GRID), "在该关键帧值上所有 ModelPart 参数值", - 0, 0, getFont(), COLOR_FOREGROUND - )); - - // --- 搜索面板 --- - JPanel searchPanel = new JPanel(new BorderLayout(5, 0)); - searchPanel.setBackground(COLOR_BACKGROUND); - searchPanel.add(createLabel("搜索参数 ID:"), BorderLayout.WEST); - styleTextField(searchField); - searchPanel.add(searchField, BorderLayout.CENTER); - - // [新增] 搜索框事件监听 - searchField.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { applyFilter(); } - @Override - public void removeUpdate(DocumentEvent e) { applyFilter(); } - @Override - public void changedUpdate(DocumentEvent e) { applyFilter(); } - }); - - panel.add(searchPanel, BorderLayout.NORTH); - - - relatedTable.setBackground(COLOR_BACKGROUND); - relatedTable.setForeground(COLOR_FOREGROUND); - relatedTable.setGridColor(COLOR_GRID); - relatedTable.getTableHeader().setBackground(COLOR_HEADER); - relatedTable.getTableHeader().setForeground(COLOR_FOREGROUND); - relatedTable.setFont(getFont().deriveFont(12f)); - relatedTable.setRowHeight(20); - - // 允许批量选择 - relatedTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - - // 值列右对齐 (columnIndex == 1) - DefaultTableCellRenderer rightRenderer = new DefaultTableCellRenderer(); - rightRenderer.setHorizontalAlignment(JLabel.RIGHT); - rightRenderer.setBackground(COLOR_BACKGROUND); - rightRenderer.setForeground(COLOR_FOREGROUND); - relatedTable.getColumnModel().getColumn(1).setCellRenderer(rightRenderer); - - // 设置 "操作" 列的 Renderer 和 Editor (columnIndex == 2) - relatedTable.getColumnModel().getColumn(2).setCellRenderer(new ButtonRenderer()); - relatedTable.getColumnModel().getColumn(2).setCellEditor(new ButtonEditor()); - relatedTable.getColumnModel().getColumn(2).setMaxWidth(60); // 限制按钮列宽度 - relatedTable.getColumnModel().getColumn(2).setMinWidth(60); - - // 添加 DELETE 和 BACK_SPACE 快捷键绑定 - InputMap inputMap = relatedTable.getInputMap(JComponent.WHEN_FOCUSED); - ActionMap actionMap = relatedTable.getActionMap(); - - inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "deleteSelected"); - inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "deleteSelected"); - - actionMap.put("deleteSelected", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - deleteSelectedRelatedRows(); - } - }); - - - JScrollPane scroll = new JScrollPane(relatedTable); - scroll.setBackground(COLOR_BACKGROUND); - scroll.getViewport().setBackground(COLOR_BACKGROUND); - scroll.setBorder(BorderFactory.createLineBorder(COLOR_GRID)); - - panel.add(scroll, BorderLayout.CENTER); - return panel; - } - - private JPanel createBottomPanel() { - JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); - panel.setBackground(COLOR_BACKGROUND); - - JButton okButton = new JButton("确定"); - JButton cancelButton = new JButton("取消"); - styleButton(okButton); - styleButton(cancelButton); - - panel.add(okButton); - panel.add(cancelButton); - - okButton.addActionListener(e -> onOK()); - cancelButton.addActionListener(e -> onCancel()); - - return panel; - } - - private JLabel createLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(COLOR_FOREGROUND.darker()); - return label; - } - - private JLabel createValueLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(COLOR_FOREGROUND); - return label; - } - - private void loadData(Float value) { - // 设置滑块和文本字段的初始值 - if (value != null) { - valueField.setText(String.format("%.6f", value)); - - float range = parameter.getMaxValue() - parameter.getMinValue(); - if (range > 0) { - int sliderValue = (int) (((value - parameter.getMinValue()) / range) * 1000f); - valueSlider.setValue(sliderValue); - } else { - valueSlider.setValue(0); - } - } - } - - private void updateFieldFromSlider() { - float normalized = valueSlider.getValue() / 1000f; - float value = parameter.getMinValue() + normalized * (parameter.getMaxValue() - parameter.getMinValue()); - valueField.setText(String.format("%.6f", value)); - } - - private void updateSliderFromField() { - try { - float val = Float.parseFloat(valueField.getText().trim()); - // 钳位 - val = Math.max(parameter.getMinValue(), Math.min(parameter.getMaxValue(), val)); - valueField.setText(String.format("%.6f", val)); // 格式化并显示钳位后的值 - - float range = parameter.getMaxValue() - parameter.getMinValue(); - if (range > 0) { - int sliderValue = (int) (((val - parameter.getMinValue()) / range) * 1000f); - valueSlider.setValue(sliderValue); - } else { - valueSlider.setValue(0); - } - } catch (NumberFormatException e) { - JOptionPane.showMessageDialog(this, "无效的数值", "格式错误", JOptionPane.ERROR_MESSAGE); - } - } - - private void onOK() { - try { - float newValue = Float.parseFloat(valueField.getText().trim()); - // 钳位 - newValue = Math.max(parameter.getMinValue(), Math.min(parameter.getMaxValue(), newValue)); - - if (originalValue != null && Math.abs(originalValue - newValue) > FLOAT_TOLERANCE) { - // 只有值发生变化时才移除旧值 - keyframesSet.remove(originalValue); - } - - // 检查新值是否已存在 - if (keyframesSet.contains(newValue)) { - // 如果是原来的值或已存在的值,直接确认 - this.confirmedValue = newValue; - } else { - // 添加新值 - keyframesSet.add(newValue); - this.confirmedValue = newValue; - } - - dispose(); - } catch (NumberFormatException e) { - JOptionPane.showMessageDialog(this, "请输入有效的浮点数", "格式错误", JOptionPane.ERROR_MESSAGE); - } - } - - private void onCancel() { - this.confirmedValue = null; - dispose(); - } - - private void styleButton(JButton button) { - button.setBackground(COLOR_HEADER); - button.setForeground(COLOR_FOREGROUND); - button.setFocusPainted(false); - button.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(COLOR_GRID), - new EmptyBorder(5, 10, 5, 10) - )); - } - - private void styleTextField(JTextField field) { - field.setBackground(COLOR_HEADER); - field.setForeground(COLOR_FOREGROUND); - field.setCaretColor(COLOR_FOREGROUND); - field.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(COLOR_GRID), - new EmptyBorder(4, 4, 4, 4) - )); - } - - - /** - * 显示对话框。 - * @param owner 父窗口 - * @param parameter 动画参数 - * @param currentValue 要编辑的关键帧值 - * @param keyframesSet 关键帧集合 (用于添加/移除操作) - * @param parametersManagement 参数管理实例 - * @param modelPart 模型部件 - * @return 如果用户点击确定,返回新的关键帧值;否则返回 null。 - */ - public static Float showEditor(Window owner, AnimationParameter parameter, Float currentValue, SortedSet keyframesSet, - ParametersManagement parametersManagement, ModelPart modelPart) { - if (parameter == null || currentValue == null || parametersManagement == null || modelPart == null) return null; - KeyframeDetailsDialog dialog = new KeyframeDetailsDialog(owner, parameter, currentValue, keyframesSet, parametersManagement, modelPart); - dialog.setVisible(true); - return dialog.confirmedValue; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeEditorDialog.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeEditorDialog.java deleted file mode 100644 index ccf52ef..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeEditorDialog.java +++ /dev/null @@ -1,710 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement; -import com.chuangzhou.vivid2D.render.model.AnimationParameter; -import com.chuangzhou.vivid2D.render.model.ModelPart; - -import javax.swing.*; -import javax.swing.border.Border; -import javax.swing.border.EmptyBorder; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.TableCellRenderer; -import java.awt.*; -import java.awt.event.*; -import java.util.ArrayList; -import java.util.Collections; -import java.util.SortedSet; -import java.util.TreeSet; - -public class KeyframeEditorDialog extends JDialog { - - // --- 现代UI颜色定义 --- - private static final Color COLOR_BACKGROUND = new Color(50, 50, 50); - private static final Color COLOR_FOREGROUND = new Color(220, 220, 220); - private static final Color COLOR_HEADER = new Color(70, 70, 70); - private static final Color COLOR_ACCENT_1 = new Color(230, 80, 80); // 关键帧 (红色) - private static final Color COLOR_ACCENT_2 = new Color(80, 150, 230); // 当前值 (蓝色) - private static final Color COLOR_GRID = new Color(60, 60, 60); - private static final Border DIALOG_PADDING = new EmptyBorder(10, 10, 10, 10); - - private final AnimationParameter parameter; - private final EditorRuler ruler; - private final KeyframeTableModel tableModel; - private final JTable keyframeTable; - private final JTextField addField = new JTextField(8); - - /** - * 临时存储编辑,直到用户点击 "OK" - */ - private final TreeSet tempKeyframes; - private final ParametersManagement parametersManagement; - private final ModelPart modelPart; - - // 用于跟踪 OK/Cancel 状态 - private boolean confirmed = false; - - // 内部类,用于显示和编辑的标尺 - private class EditorRuler extends JComponent { - private static final int RULER_HEIGHT = 25; - private static final int MARKER_SIZE = 8; - private static final int TICK_HEIGHT = 5; - private static final int PADDING = 15; // 左右内边距 - private static final int LABEL_VMARGIN = 3; // 标签垂直边距 - - // --- 用于跟踪鼠标悬浮 --- - private int mouseHoverX = -1; - private float mouseHoverValue = 0.0f; - // -------------------------- - - EditorRuler() { - setPreferredSize(new Dimension(100, RULER_HEIGHT + 35)); - setBackground(COLOR_BACKGROUND); - setForeground(COLOR_FOREGROUND); - setOpaque(true); - - addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - float value = xToValue(e.getX()); - float range = parameter.getMaxValue() - parameter.getMinValue(); - float snapThresholdPx = 4; // 4 像素 - float snapThreshold = (range > 0) ? (xToValue(getPadding() + (int)snapThresholdPx) - xToValue(getPadding())) : 0; - - Float nearest = getNearestTempKeyframe(value, snapThreshold); - - if (e.isShiftDown() || SwingUtilities.isRightMouseButton(e)) { // 按住 Shift 或右键删除 - if (nearest != null) { - tempKeyframes.remove(nearest); - } - } else if (SwingUtilities.isLeftMouseButton(e)) { - if (nearest != null) { - // 选中已有的 - int row = tableModel.getRowForValue(nearest); - if (row != -1) { - keyframeTable.setRowSelectionInterval(row, row); - keyframeTable.scrollRectToVisible(keyframeTable.getCellRect(row, 0, true)); - } - } else { - // 添加新的 (钳位) - float clampedValue = Math.max(parameter.getMinValue(), Math.min(parameter.getMaxValue(), value)); - tempKeyframes.add(clampedValue); - } - } - updateAllUI(); - } - - // --- [修复] mouseExited 移到这里 (MouseAdapter) --- - @Override - public void mouseExited(MouseEvent e) { - mouseHoverX = -1; // 鼠标离开,清除悬浮位置 - repaint(); // 触发重绘以隐藏提示 - } - // ---------------------------------------------------- - }); - - // --- 鼠标移动监听器 --- - addMouseMotionListener(new MouseMotionAdapter() { - @Override - public void mouseMoved(MouseEvent e) { - mouseHoverX = e.getX(); - mouseHoverValue = xToValue(mouseHoverX); - repaint(); // 触发重绘以显示悬浮提示 - } - // 注意:这里不再需要 @Override public void mouseExited(MouseEvent e) - }); - // ---------------------------------- - } - - private int getPadding() { - return PADDING; - } - - private float xToValue(int x) { - int padding = getPadding(); - int trackWidth = getWidth() - padding * 2; - if (trackWidth <= 0) return parameter.getMinValue(); - - float percent = Math.max(0f, Math.min(1f, (float) (x - padding) / trackWidth)); - return parameter.getMinValue() + percent * (parameter.getMaxValue() - parameter.getMinValue()); - } - - private int valueToX(float value) { - int padding = getPadding(); - int trackWidth = getWidth() - padding * 2; - float range = parameter.getMaxValue() - parameter.getMinValue(); - float percent = 0; - if (range > 0) { - // [修复] 确保钳位 - percent = (Math.max(parameter.getMinValue(), Math.min(parameter.getMaxValue(), value)) - parameter.getMinValue()) / range; - } - return padding + (int) (percent * trackWidth); - } - - private Float getNearestTempKeyframe(float value, float snapThreshold) { - if (snapThreshold <= 0) return null; - - Float lower = tempKeyframes.floor(value); - Float higher = tempKeyframes.ceiling(value); - - float distToLower = (lower != null) ? Math.abs(value - lower) : Float.MAX_VALUE; - float distToHigher = (higher != null) ? Math.abs(value - higher) : Float.MAX_VALUE; - - if (distToLower < snapThreshold && distToLower <= distToHigher) { - return lower; - } - if (distToHigher < snapThreshold && distToHigher < distToLower) { - return higher; - } - return null; - } - - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); // 绘制不透明背景 - Graphics2D g2 = (Graphics2D) g.create(); // 使用 g.create() 防止 g 被修改 - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // [修复] 强制清除背景,防止渲染残留 - g2.setColor(getBackground()); - g2.fillRect(0, 0, getWidth(), getHeight()); - - int padding = getPadding(); - int w = getWidth() - padding * 2; - int h = getHeight(); - - int topOffset = 15; - int trackY = topOffset + (h - topOffset) / 2; // 垂直居中于剩余空间 - - // 1. 轨道 - g2.setColor(COLOR_GRID); - g2.setStroke(new BasicStroke(2)); - g2.drawLine(padding, trackY, padding + w, trackY); - - // 2. 绘制刻度和标签 (min, max, mid) - float min = parameter.getMinValue(); - float max = parameter.getMaxValue(); - - drawTick(g2, min, trackY, true); // 强制绘制 min - drawTick(g2, max, trackY, true); // 强制绘制 max - - // 仅在 min 和 max 不太近时绘制 mid - if (Math.abs(max-min) > 0.1) { - float mid = min + (max - min) / 2; - drawTick(g2, mid, trackY, false); // 不强制绘制 mid - } - - // 3. 绘制关键帧 (来自 tempKeyframes) - g2.setColor(COLOR_ACCENT_1); - for (float f : tempKeyframes) { - int x = valueToX(f); - g2.fillOval(x - MARKER_SIZE / 2, trackY - MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE); - } - - // --- 4. 绘制鼠标悬浮值 (Hover Value) --- - if (mouseHoverX != -1) { - if (mouseHoverX >= padding && mouseHoverX <= padding + w) { - g2.setColor(COLOR_ACCENT_1); - g2.drawLine(mouseHoverX, trackY - TICK_HEIGHT - 2, mouseHoverX, trackY + TICK_HEIGHT + 2); - String hoverLabel = String.format("%.2f", mouseHoverValue); - FontMetrics fm = g2.getFontMetrics(); - int labelWidth = fm.stringWidth(hoverLabel); - int labelY = topOffset + (RULER_HEIGHT / 2) - fm.getAscent() / 2; - int labelX = mouseHoverX - labelWidth / 2; - labelX = Math.max(padding, Math.min(labelX, getWidth() - padding - labelWidth)); - g2.drawString(hoverLabel, labelX, labelY); - } - } - // --------------------------------------------- - - // 5. 绘制当前值 (来自 parameter) - g2.setColor(COLOR_ACCENT_2); - int x = valueToX(parameter.getValue()); - g2.fillOval(x - MARKER_SIZE / 2, trackY - MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE); - - g2.dispose(); // 释放 g.create() 创建的 Graphics - } - - private void drawTick(Graphics2D g2, float value, int y, boolean forceLabel) { - int x = valueToX(value); - g2.setColor(COLOR_GRID.brighter()); - g2.drawLine(x, y - TICK_HEIGHT, x, y + TICK_HEIGHT); - - // [修复] 仅在空间足够或被强制时绘制标签 - FontMetrics fm = g2.getFontMetrics(); - // [修复] 格式化为 2 位小数 - String label = String.format("%.2f", value); - int labelWidth = fm.stringWidth(label); - - // 简单的防重叠 - boolean fits = (labelWidth < (getWidth() - getPadding()*2) / 5); - - if (forceLabel || fits) { - g2.setColor(COLOR_FOREGROUND); - g2.drawString(label, x - labelWidth / 2, y + TICK_HEIGHT + fm.getAscent() + LABEL_VMARGIN); - } - } - } - - /** - * 自定义 TableModel 以显示 "No." 和 "Value" - */ - private class KeyframeTableModel extends AbstractTableModel { - private final String[] columnNames = {"序", "值"}; - private java.util.List keyframes = new ArrayList<>(); - - public void setData(SortedSet data) { - this.keyframes.clear(); - this.keyframes.addAll(data); - fireTableDataChanged(); - } - - public int getRowForValue(float value) { - int index = Collections.binarySearch(keyframes, value); - return (index < 0) ? -1 : index; - } - - public Float getValueAtRow(int row) { - if (row >= 0 && row < keyframes.size()) { - return keyframes.get(row); - } - return null; - } - - @Override - public int getRowCount() { - return keyframes.size(); - } - - @Override - public int getColumnCount() { - return columnNames.length; - } - - @Override - public String getColumnName(int column) { - return columnNames[column]; - } - - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - if (columnIndex == 0) { - return rowIndex + 1; // "No" (序号) - } - if (columnIndex == 1) { - return keyframes.get(rowIndex); // "Value" (值) - [修复] 返回 Float 对象 - } - return null; - } - - // --- 允许 "值" 列被编辑 --- - @Override - public boolean isCellEditable(int rowIndex, int columnIndex) { - return columnIndex == 1; // 只有 "值" 列可以编辑 - } - - // --- 处理单元格编辑后的值 --- - @Override - public void setValueAt(Object aValue, int rowIndex, int columnIndex) { - if (columnIndex != 1) return; - - float newValue; - try { - newValue = Float.parseFloat(aValue.toString()); - } catch (NumberFormatException e) { - JOptionPane.showMessageDialog(KeyframeEditorDialog.this, - "请输入有效的浮点数", "格式错误", JOptionPane.ERROR_MESSAGE); - return; - } - - // 钳位 - newValue = Math.max(parameter.getMinValue(), Math.min(parameter.getMaxValue(), newValue)); - - // 获取旧值 - Float oldValue = getValueAtRow(rowIndex); - if (oldValue == null) return; - - // 更新临时的 Set - tempKeyframes.remove(oldValue); - tempKeyframes.add(newValue); - - // 彻底刷新UI (因为 Set 排序可能已改变) - updateAllUI(); - - // 刷新后,重新定位并选中新值的行 - int newRow = tableModel.getRowForValue(newValue); - if (newRow != -1) { - keyframeTable.setRowSelectionInterval(newRow, newRow); - } - } - // ------------------------------------ - - @Override - public Class getColumnClass(int columnIndex) { - if (columnIndex == 0) return Integer.class; - if (columnIndex == 1) return Float.class; - return Object.class; - } - } - - - public KeyframeEditorDialog(Window owner, AnimationParameter parameter, ParametersManagement parametersManagement, ModelPart modelPart) { - super(owner, "关键帧编辑器: " + parameter.getId(), ModalityType.APPLICATION_MODAL); - this.parameter = parameter; - this.modelPart = modelPart; - - this.tempKeyframes = new TreeSet<>(parameter.getKeyframes()); - - this.ruler = new EditorRuler(); - this.tableModel = new KeyframeTableModel(); - this.keyframeTable = new JTable(tableModel); - this.parametersManagement = parametersManagement; - - initUI(); - updateAllUI(); - } - - private void initUI() { - setSize(500, 400); - setMinimumSize(new Dimension(450, 350)); - setResizable(true); - - setLocationRelativeTo(getOwner()); - getContentPane().setBackground(COLOR_BACKGROUND); - setLayout(new BorderLayout(5, 5)); - ((JPanel) getContentPane()).setBorder(DIALOG_PADDING); - - // 1. 顶部标尺 - ruler.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(COLOR_GRID), "标尺 (点击添加, Shift/右键 删除)", - 0, 0, getFont(), COLOR_FOREGROUND - )); - add(ruler, BorderLayout.NORTH); - - // 2. 中间列表 - configureTableAppearance(); - JScrollPane scroll = new JScrollPane(keyframeTable); - configureScrollPaneAppearance(scroll); - - JPanel centerPanel = new JPanel(new BorderLayout(5, 5)); - centerPanel.setBackground(COLOR_BACKGROUND); - centerPanel.add(scroll, BorderLayout.CENTER); - centerPanel.add(createListActionsPanel(), BorderLayout.EAST); - - add(centerPanel, BorderLayout.CENTER); - - // 3. 底部操作栏 (OK/Cancel) - add(createBottomPanel(), BorderLayout.SOUTH); - - // --- 为 JTable 添加双击监听器 --- - keyframeTable.addMouseListener(new MouseAdapter() { - public void mousePressed(MouseEvent e) { - if (e.getClickCount() == 2) { - int row = keyframeTable.rowAtPoint(e.getPoint()); - //int col = keyframeTable.columnAtPoint(e.getPoint()); // 不需要列判断 - - if (row >= 0) { - Float selectedValue = tableModel.getValueAtRow(row); - if (selectedValue != null) { - showKeyframeDetailsDialog(selectedValue, row); - } - } - } - } - }); - // ------------------------------------ - } - - /** - * 显示关键帧详细信息对话框 - */ - private void showKeyframeDetailsDialog(Float currentValue, int currentRow) { - // KeyframeDetailsDialog 将负责处理值的更新和在 tempKeyframes 中的替换 - Float newValue = KeyframeDetailsDialog.showEditor( - this, - parameter, - currentValue, - tempKeyframes, // 将 Set 传递给子对话框进行修改 - parametersManagement, // 传递 ParametersManagement - modelPart // 传递 ModelPart - ); - - if (newValue != null) { - // 对话框已更新 tempKeyframes - - // 彻底刷新UI (因为 Set 排序可能已改变) - updateAllUI(); - - // 刷新后,重新定位并选中新值的行 - int newRow = tableModel.getRowForValue(newValue); - if (newRow != -1) { - keyframeTable.setRowSelectionInterval(newRow, newRow); - keyframeTable.scrollRectToVisible(keyframeTable.getCellRect(newRow, 0, true)); - } else { - keyframeTable.clearSelection(); - } - } - } - - private JPanel createListActionsPanel() { - JPanel actionsPanel = new JPanel(); - actionsPanel.setBackground(COLOR_BACKGROUND); - actionsPanel.setLayout(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(0, 5, 0, 5); - gbc.anchor = GridBagConstraints.NORTH; - gbc.weighty = 1.0; - - JButton delButton = new JButton("删除"); - styleButton(delButton); - delButton.addActionListener(e -> removeSelectedKeyframe()); - - actionsPanel.add(delButton, gbc); - - return actionsPanel; - } - - private JPanel createBottomPanel() { - JPanel bottomPanel = new JPanel(new BorderLayout(5, 5)); - bottomPanel.setBackground(COLOR_BACKGROUND); - - // 左侧:添加新帧 - JPanel addPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - addPanel.setBackground(COLOR_BACKGROUND); - - JLabel addLabel = new JLabel("添加值:"); - addLabel.setForeground(COLOR_FOREGROUND); - styleTextField(addField); - - JButton addButton = new JButton("添加"); - styleButton(addButton); - - addPanel.add(addLabel); - addPanel.add(addField); - addPanel.add(addButton); - - // 右侧:OK / Cancel - JPanel okCancelPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); - okCancelPanel.setBackground(COLOR_BACKGROUND); - - JButton okButton = new JButton("确定"); - JButton cancelButton = new JButton("取消"); - styleButton(okButton); - styleButton(cancelButton); - - okCancelPanel.add(okButton); - okCancelPanel.add(cancelButton); - - bottomPanel.add(addPanel, BorderLayout.CENTER); - bottomPanel.add(okCancelPanel, BorderLayout.EAST); - - // 事件绑定 - addButton.addActionListener(e -> addKeyframeFromField()); - addField.addActionListener(e -> addKeyframeFromField()); - - okButton.addActionListener(e -> onOK()); - cancelButton.addActionListener(e -> onCancel()); - - // Esc 键关闭 = Cancel - getRootPane().registerKeyboardAction(e -> onCancel(), - KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), - JComponent.WHEN_IN_FOCUSED_WINDOW); - - return bottomPanel; - } - - /** - * 确认更改,应用到原始 parameter - * [修复] 恢复为 private 访问权限,因为它不覆盖任何父类方法。 - */ - private void onOK() { - // 停止任何可能的单元格编辑 - if (keyframeTable.isEditing()) { - keyframeTable.getCellEditor().stopCellEditing(); - } - - parameter.clearKeyframes(); - for (Float f : tempKeyframes) { - parameter.addKeyframe(f); - } - this.confirmed = true; // 标记为已确认 - dispose(); - } - - /** - * 取消更改 - * [修复] 恢复为 private 访问权限,因为它不覆盖任何父类方法。 - */ - private void onCancel() { - // 停止任何可能的单元格编辑,但丢弃结果 - if (keyframeTable.isEditing()) { - keyframeTable.getCellEditor().cancelCellEditing(); - } - - this.confirmed = false; // 标记为未确认 - dispose(); - } - - private void addKeyframeFromField() { - try { - float val = Float.parseFloat(addField.getText().trim()); - // 钳位 - val = Math.max(parameter.getMinValue(), Math.min(parameter.getMaxValue(), val)); - - tempKeyframes.add(val); - updateAllUI(); - - // 添加后自动选中 - int row = tableModel.getRowForValue(val); - if (row != -1) { - keyframeTable.setRowSelectionInterval(row, row); - keyframeTable.scrollRectToVisible(keyframeTable.getCellRect(row, 0, true)); - } - - addField.setText(""); - } catch (NumberFormatException e) { - JOptionPane.showMessageDialog(this, "无效的数值", "错误", JOptionPane.ERROR_MESSAGE); - } - } - - private void removeSelectedKeyframe() { - int selectedRow = keyframeTable.getSelectedRow(); - if (selectedRow != -1) { - Float val = tableModel.getValueAtRow(selectedRow); - if (val != null) { - tempKeyframes.remove(val); - updateAllUI(); - - // 重新选中删除后的下一行 - if (tableModel.getRowCount() > 0) { - int newSel = Math.min(selectedRow, tableModel.getRowCount() - 1); - keyframeTable.setRowSelectionInterval(newSel, newSel); - } - } - } - } - - private void updateAllUI() { - // 更新列表 - tableModel.setData(tempKeyframes); - // 重绘标尺 - ruler.repaint(); - } - - // --- 辅助方法:设置UI风格 --- - - private void configureTableAppearance() { - keyframeTable.setBackground(COLOR_BACKGROUND); - keyframeTable.setForeground(COLOR_FOREGROUND); - keyframeTable.setGridColor(COLOR_GRID); - keyframeTable.setSelectionBackground(COLOR_ACCENT_2); - keyframeTable.setSelectionForeground(Color.WHITE); - keyframeTable.getTableHeader().setBackground(COLOR_HEADER); - keyframeTable.getTableHeader().setForeground(COLOR_FOREGROUND); - keyframeTable.setFont(getFont().deriveFont(14f)); - keyframeTable.setRowHeight(20); - - // 居中 "No" 列 - DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer(); - centerRenderer.setHorizontalAlignment(JLabel.CENTER); - centerRenderer.setBackground(COLOR_BACKGROUND); - centerRenderer.setForeground(COLOR_FOREGROUND); - keyframeTable.getColumnModel().getColumn(0).setMaxWidth(60); - keyframeTable.getColumnModel().getColumn(0).setCellRenderer(centerRenderer); - - // "Value" 列,格式化浮点数 - TableCellRenderer floatRenderer = new DefaultTableCellRenderer() { - { // Instance initializer - setHorizontalAlignment(JLabel.RIGHT); // 数字右对齐 - setBorder(new EmptyBorder(0, 5, 0, 5)); // 增加内边距 - } - - @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { - super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - - if (value instanceof Float) { - setText(String.format("%.6f", (Float) value)); - } - - if (!isSelected) { - setBackground(COLOR_BACKGROUND); - setForeground(COLOR_FOREGROUND); - } - return this; - } - }; - keyframeTable.getColumnModel().getColumn(1).setCellRenderer(floatRenderer); - - // 为 "值" 列设置一个暗黑风格的编辑器 - JTextField editorTextField = new JTextField(); - styleTextField(editorTextField); // 复用暗黑风格 - editorTextField.setBorder(BorderFactory.createLineBorder(COLOR_ACCENT_2)); // 编辑时高亮 - keyframeTable.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(editorTextField)); - } - - private void configureScrollPaneAppearance(JScrollPane scroll) { - scroll.setBackground(COLOR_BACKGROUND); - scroll.getViewport().setBackground(COLOR_BACKGROUND); - scroll.setBorder(BorderFactory.createLineBorder(COLOR_GRID)); - scroll.getVerticalScrollBar().setUI(new javax.swing.plaf.basic.BasicScrollBarUI() { - @Override - protected void configureScrollBarColors() { - this.thumbColor = COLOR_HEADER; - this.trackColor = COLOR_BACKGROUND; - } - - @Override - protected JButton createDecreaseButton(int orientation) { - return createZeroButton(); - } - - @Override - protected JButton createIncreaseButton(int orientation) { - return createZeroButton(); - } - - private JButton createZeroButton() { - JButton b = new JButton(); - b.setPreferredSize(new Dimension(0, 0)); - return b; - } - }); - } - - private void styleButton(JButton button) { - button.setBackground(COLOR_HEADER); - button.setForeground(COLOR_FOREGROUND); - button.setFocusPainted(false); - button.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(COLOR_GRID), - new EmptyBorder(5, 10, 5, 10) - )); - } - - private void styleTextField(JTextField field) { - field.setBackground(COLOR_HEADER); - field.setForeground(COLOR_FOREGROUND); - field.setCaretColor(COLOR_FOREGROUND); - field.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(COLOR_GRID), - new EmptyBorder(4, 4, 4, 4) - )); - } - - - public boolean isConfirmed() { - return confirmed; - } - - /** - * 显示对话框。 - */ - public static boolean showEditor(Window owner, AnimationParameter parameter,ParametersManagement parametersManagement, ModelPart modelPart) { - if (parameter == null) return false; - KeyframeEditorDialog dialog = new KeyframeEditorDialog(owner, parameter,parametersManagement,modelPart); - dialog.setVisible(true); - return dialog.isConfirmed(); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeSlider.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeSlider.java deleted file mode 100644 index 96e96b8..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeSlider.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -import com.chuangzhou.vivid2D.render.model.AnimationParameter; - -import javax.swing.*; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import java.awt.*; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseMotionAdapter; -import java.util.ArrayList; -import java.util.List; - -/** - * 一个自定义的滑块控件,用于显示和操作 AnimationParameter。 - * 它可以显示关键帧标记,并实现拖拽时的吸附功能。 - */ -public class KeyframeSlider extends JComponent { - - private static final int TRACK_HEIGHT = 6; - private static final int THUMB_SIZE = 14; - private static final int KEYFRAME_MARKER_SIZE = 8; - private static final int PADDING = 8; - - private AnimationParameter parameter; - private boolean isDragging = false; - private final List listeners = new ArrayList<>(); - - // 吸附阈值(占总宽度的百分比) - private final float snapThresholdPercent = 0.02f; // 2% - - public KeyframeSlider() { - setPreferredSize(new Dimension(100, THUMB_SIZE + PADDING * 2)); - setMinimumSize(new Dimension(50, THUMB_SIZE + PADDING * 2)); - - addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - if (parameter == null) return; - isDragging = true; - updateValueFromMouse(e.getX()); - } - - @Override - public void mouseReleased(MouseEvent e) { - isDragging = false; - } - }); - - addMouseMotionListener(new MouseMotionAdapter() { - @Override - public void mouseDragged(MouseEvent e) { - if (isDragging && parameter != null) { - updateValueFromMouse(e.getX()); - } - } - }); - } - - /** - * 设置此滑块绑定的参数。 - */ - public void setParameter(AnimationParameter p) { - this.parameter = p; - repaint(); - } - - public AnimationParameter getParameter() { - return parameter; - } - - private void updateValueFromMouse(int mouseX) { - if (parameter == null) return; - - int trackStart = PADDING; - int trackWidth = getWidth() - PADDING * 2; - float percent = Math.max(0f, Math.min(1f, (float) (mouseX - trackStart) / trackWidth)); - - float min = parameter.getMinValue(); - float max = parameter.getMaxValue(); - float range = max - min; - float newValue = min + percent * range; - - // --- 吸附逻辑 --- - float snapThreshold = range * snapThresholdPercent; - Float nearestKeyframe = parameter.getNearestKeyframe(newValue, snapThreshold); - if (nearestKeyframe != null) { - newValue = nearestKeyframe; - } - // ---------------- - - if (parameter.getValue() != newValue) { - parameter.setValue(newValue); // setValue 会自动钳位 - fireStateChanged(); - repaint(); - } - } - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - Graphics2D g2 = (Graphics2D) g; - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - if (parameter == null) { - paintDisabled(g2); - return; - } - - int trackY = (getHeight() - TRACK_HEIGHT) / 2; - int trackStart = PADDING; - int trackWidth = getWidth() - PADDING * 2; - - // 1. 绘制轨道 - g2.setColor(getBackground().darker()); - g2.fillRoundRect(trackStart, trackY, trackWidth, TRACK_HEIGHT, TRACK_HEIGHT, TRACK_HEIGHT); - - // 2. 绘制关键帧标记 - g2.setColor(Color.CYAN.darker()); - int markerY = (getHeight() - KEYFRAME_MARKER_SIZE) / 2; - for (float keyframeValue : parameter.getKeyframes()) { - float percent = (keyframeValue - parameter.getMinValue()) / (parameter.getMaxValue() - parameter.getMinValue()); - int markerX = trackStart + (int) (percent * trackWidth) - KEYFRAME_MARKER_SIZE / 2; - // 绘制菱形 - Polygon diamond = new Polygon(); - diamond.addPoint(markerX + KEYFRAME_MARKER_SIZE / 2, markerY); - diamond.addPoint(markerX + KEYFRAME_MARKER_SIZE, markerY + KEYFRAME_MARKER_SIZE / 2); - diamond.addPoint(markerX + KEYFRAME_MARKER_SIZE / 2, markerY + KEYFRAME_MARKER_SIZE); - diamond.addPoint(markerX, markerY + KEYFRAME_MARKER_SIZE / 2); - g2.fill(diamond); - } - - // 3. 绘制滑块 (Thumb) - float currentPercent = parameter.getNormalizedValue(); - int thumbX = trackStart + (int) (currentPercent * trackWidth) - THUMB_SIZE / 2; - int thumbY = (getHeight() - THUMB_SIZE) / 2; - - g2.setColor(isEnabled() ? getForeground() : Color.GRAY); - g2.fillOval(thumbX, thumbY, THUMB_SIZE, THUMB_SIZE); - g2.setColor(getBackground()); - g2.drawOval(thumbX, thumbY, THUMB_SIZE, THUMB_SIZE); - } - - private void paintDisabled(Graphics2D g2) { - int trackY = (getHeight() - TRACK_HEIGHT) / 2; - int trackStart = PADDING; - int trackWidth = getWidth() - PADDING * 2; - g2.setColor(Color.GRAY.brighter()); - g2.fillRoundRect(trackStart, trackY, trackWidth, TRACK_HEIGHT, TRACK_HEIGHT, TRACK_HEIGHT); - } - - // --- ChangeEvent 支持 (与 JSlider 保持一致) --- - public void addChangeListener(ChangeListener l) { - listeners.add(l); - } - - public void removeChangeListener(ChangeListener l) { - listeners.remove(l); - } - - protected void fireStateChanged() { - ChangeEvent e = new ChangeEvent(this); - for (ChangeListener l : new ArrayList<>(listeners)) { - l.stateChanged(e); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelAIPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelAIPanel.java deleted file mode 100644 index 17b3c82..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelAIPanel.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -public class ModelAIPanel { -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java deleted file mode 100644 index bdb5a9d..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -import com.chuangzhou.vivid2D.render.model.Mesh2D; - -/** - * 模型点击事件监听器接口 - * - * @author tzdwindows 7 - */ -public interface ModelClickListener { - /** - * 当点击模型时触发 - * - * @param mesh 被点击的网格,如果点击在空白处则为 null - * @param modelX 模型坐标系中的 X 坐标 - * @param modelY 模型坐标系中的 Y 坐标 - * @param screenX 屏幕坐标系中的 X 坐标 - * @param screenY 屏幕坐标系中的 Y 坐标 - */ - void onModelClicked(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY); - - /** - * 当鼠标在模型上移动时触发 - * - * @param mesh 鼠标下方的网格,如果不在任何网格上则为 null - * @param modelX 模型坐标系中的 X 坐标 - * @param modelY 模型坐标系中的 Y 坐标 - * @param screenX 屏幕坐标系中的 X 坐标 - * @param screenY 屏幕坐标系中的 Y 坐标 - */ - default void onModelHover(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) { - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java deleted file mode 100644 index 461e0eb..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java +++ /dev/null @@ -1,1413 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -import com.chuangzhou.vivid2D.render.awt.manager.LayerOperationManager; -import com.chuangzhou.vivid2D.render.awt.manager.ThumbnailManager; -import com.chuangzhou.vivid2D.render.awt.manager.data.LayerOperationManagerData; -import com.chuangzhou.vivid2D.render.awt.util.*; -import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerCellRenderer; -// 确保 LayerReorderTransferHandler 被正确导入,它处理内部重排 -import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerReorderTransferHandler; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.Texture; -import com.chuangzhou.vivid2D.window.MainWindow; -import org.joml.Vector2f; - -import javax.imageio.ImageIO; -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import javax.swing.plaf.basic.BasicSliderUI; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.geom.RoundRectangle2D; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -// 引入 JnaFileChooser -import jnafilechooser.api.JnaFileChooser; -// 引入拖放和快捷键相关的类 -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.Transferable; -import javax.swing.TransferHandler; -import javax.swing.AbstractAction; -import javax.swing.Action; -import javax.swing.KeyStroke; - - -public class ModelLayerPanel extends JPanel { - private Model2D model; - private ModelRenderPanel renderPanel; - - private DefaultListModel listModel; - private JList layerList; - - private ModernButton addButton; - private ModernButton removeButton; - private ModernButton upButton; - private ModernButton downButton; - private ModernButton bindTextureButton; - - private JSlider opacitySlider; - private JLabel opacityValueLabel; - private boolean isDragging = false; - private ModelPart draggedPart = null; - private Vector2f dragStartPosition = null; - - private volatile boolean ignoreSliderEvents = false; - - private ThumbnailManager thumbnailManager; - private PSDImporter psdImporter; - private LayerOperationManager operationManager; - - private static Color themeColor(String key, Color fallback) { - try { - Color c = UIManager.getColor(key); - if (c != null) return c; - Object o = UIManager.get(key); - if (o instanceof Color) return (Color) o; - } catch (Exception ignored) {} - return fallback; - } - - private static final Color BACKGROUND_COLOR = themeColor("Panel.background", new Color(37, 37, 38)); - private static final Color SURFACE_COLOR = themeColor("List.background", new Color(45, 45, 48)); - private static final Color ACCENT_COLOR = themeColor("nimbusSelectedText", new Color(10, 132, 255)); - private static final Color TEXT_COLOR = themeColor("Label.foreground", new Color(220, 220, 220)); - private static final Color BORDER_COLOR = themeColor("Separator.foreground", new Color(60, 60, 60)); - - // 按钮/控件状态颜色(从主题读取,仍保留合理回退) - private static final Color ACCENT_HOVER_COLOR = themeColor("nimbusSelection", new Color(50, 152, 255)); - private static final Color ACCENT_PRESSED_COLOR = themeColor("nimbusBase", new Color(0, 110, 235)); - private static final Color BUTTON_HOVER_COLOR = themeColor("Button.background", new Color(70, 70, 73)); - - // 固定面板推荐最大宽度(避免过宽) - private static final int PANEL_MAX_WIDTH = 300; - - public ModelLayerPanel(ModelRenderPanel renderPanel) { - this.renderPanel = renderPanel; - this.model = renderPanel.getModel(); - // 限制面板宽度,保持简约不占用过多空间 - setPreferredSize(new Dimension(PANEL_MAX_WIDTH, 600)); - setMaximumSize(new Dimension(PANEL_MAX_WIDTH, Integer.MAX_VALUE)); - - setupModernLookAndFeel(); - this.thumbnailManager = new ThumbnailManager(renderPanel); - - // FIX: 移除在 this 上的 TransferHandler,因为 JList 将会覆盖它 - // this.setTransferHandler(new FileDropTransferHandler()); - - if (this.model != null) { - this.psdImporter = new PSDImporter(model, renderPanel, this); - this.operationManager = new LayerOperationManager(model); - initComponents(); - reloadFromModel(); - generateAllThumbnails(); - } else { - initComponents(); - renderPanel.getGlContextManager().waitForModel().thenAccept(m -> { - if (m == null) return; - SwingUtilities.invokeLater(() -> { - this.model = m; - this.psdImporter = new PSDImporter(model, renderPanel, ModelLayerPanel.this); - this.operationManager = new LayerOperationManager(model); - loadMetadata(); - reloadFromModel(); - generateAllThumbnails(); - }); - }); - } - } - - private void setupWindowFileDropHandler() { - SwingUtilities.invokeLater(() -> { - Window window = SwingUtilities.getWindowAncestor(this); - // 确保找到的 window 是 RootPaneContainer (如 JFrame, JDialog) - if (window instanceof MainWindow mainWindow) { - JComponent contentPane = (JComponent) mainWindow.getContentPane(); - if (contentPane != null) { - contentPane.setTransferHandler(new FileDropOnlyTransferHandler()); - } - } - }); - } - - public void loadMetadata() { - String modelDataPath = renderPanel.getGlContextManager().getModelPath() + ".data"; - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(modelDataPath))) { - Object obj = ois.readObject(); - if (obj instanceof LayerOperationManagerData layerData) { - operationManager.loadMetadata(layerData.layerMetadata); - renderPanel.getGlContextManager().executeInGLContext(() -> operationManager.loadMetadata(layerData.layerMetadata)); - } - } catch (IOException | ClassNotFoundException | RuntimeException ex) { - // 常见于文件不存在或文件损坏 - System.out.println("No layer metadata file found or failed to load: " + ex.getMessage()); - } - } - - private void setupModernLookAndFeel() { - setBackground(BACKGROUND_COLOR); - setBorder(new EmptyBorder(8, 8, 8, 8)); // 更小的内边距 - } - - // ============== 缩略图相关方法 ============== - private void generateAllThumbnails() { - if (model == null) return; - - thumbnailManager.clearCache(); - for (int i = 0; i < listModel.getSize(); i++) { - ModelPart part = listModel.get(i); - thumbnailManager.generateThumbnail(part); - } - layerList.repaint(); - } - - // 修正:支持多选,刷新第一个选中项的缩略图 - private void refreshSelectedThumbnail() { - List selected = layerList.getSelectedValuesList(); - if (!selected.isEmpty()) { - thumbnailManager.generateThumbnail(selected.get(0)); - layerList.repaint(); - } - } - - private void initComponents() { - setLayout(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - gbc.insets = new Insets(0, 0, 6, 0); // 更小的垂直间距 - - JPanel headerPanel = createHeaderPanel(); - gbc.gridy = 0; - gbc.weighty = 0.0; - add(headerPanel, gbc); - - listModel = new DefaultListModel<>(); - layerList = createModernList(); - JScrollPane centerScrollPane = createCenterPanel(); - gbc.gridy = 1; - gbc.weighty = 1.0; - gbc.fill = GridBagConstraints.BOTH; - add(centerScrollPane, gbc); - - setupWindowFileDropHandler(); - // 绑定快捷键 - setupKeyBindings(layerList); - - JPanel controlPanel = createControlPanel(); - gbc.gridy = 2; - gbc.weighty = 0.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(6, 0, 0, 0); - add(controlPanel, gbc); - } - - /** - * 设置快捷键绑定。 - * @param list JList 组件 - */ - private void setupKeyBindings(JList list) { - // 关键更改:从 JList 获取 InputMap,但使用 WHEN_IN_FOCUSED_WINDOW 模式 - // 这样,只要包含这个 ModelLayerPanel 的窗口处于焦点状态,快捷键就会生效。 - InputMap inputMap = list.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); - - // 获取组件的 ActionMap - ActionMap actionMap = list.getActionMap(); - - // 绑定 Delete/Backspace 键到删除操作 - KeyStroke deleteKey = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0); - KeyStroke backspaceKey = KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0); - - inputMap.put(deleteKey, "deleteLayer"); - inputMap.put(backspaceKey, "deleteLayer"); - Action deleteAction = new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - onRemoveLayer(); - } - }; - - actionMap.put("deleteLayer", deleteAction); - } - - private JList createModernList() { - JList list = new JList<>(listModel); - // 启用多选 - list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - list.setBackground(SURFACE_COLOR); - list.setForeground(TEXT_COLOR); - list.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); - list.setFixedCellHeight(46); // 更紧凑 - LayerCellRenderer cellRenderer = new LayerCellRenderer(this, thumbnailManager); - cellRenderer.attachMouseListener(list, listModel); - list.setCellRenderer(cellRenderer); - list.setDragEnabled(true); - - // FIX: 使用新的复合 TransferHandler 统一处理文件拖放和内部重排 - list.setTransferHandler(new CompositeLayerTransferHandler(this)); - - list.setDropMode(DropMode.INSERT); - list.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - int idx = list.locationToIndex(e.getPoint()); - if (idx >= 0) { - showRenameDialog(listModel.get(idx)); - } - } - } - }); - list.addListSelectionListener(e -> updateUIState()); - return list; - } - - private JPanel createHeaderPanel() { - JPanel headerPanel = new JPanel(new BorderLayout()); - headerPanel.setBackground(BACKGROUND_COLOR); - headerPanel.setBorder(BorderFactory.createEmptyBorder(2, 4, 6, 4)); - - JLabel titleLabel = new JLabel("图层"); - titleLabel.setForeground(TEXT_COLOR); - titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 14f)); - - headerPanel.add(titleLabel, BorderLayout.WEST); - return headerPanel; - } - - - private JScrollPane createCenterPanel() { - JScrollPane scrollPane = new JScrollPane(layerList); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); // 极简 - scrollPane.getViewport().setBackground(SURFACE_COLOR); - scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); - return scrollPane; - } - - private JPanel createControlPanel() { - JPanel controlPanel = new JPanel(); - controlPanel.setLayout(new BoxLayout(controlPanel, BoxLayout.Y_AXIS)); - controlPanel.setBackground(BACKGROUND_COLOR); - - JPanel buttonPanel = createButtonPanel(); - buttonPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - controlPanel.add(buttonPanel); - - JPanel settingsPanel = createSettingsPanel(); - settingsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - controlPanel.add(Box.createVerticalStrut(6)); - controlPanel.add(settingsPanel); - - return controlPanel; - } - - private JPanel createButtonPanel() { - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); - buttonPanel.setBackground(BACKGROUND_COLOR); - // 无多余标题边框 - - addButton = createIconButton("\uFF0B", "添加", this::showAddMenu); // 更细小的加号 - removeButton = createIconButton("\u2013", "删除", this::onRemoveLayer); // 细长减号 - upButton = createIconButton("\u25B2", "上移", this::moveSelectedUp); - downButton = createIconButton("\u25BC", "下移", this::moveSelectedDown); - bindTextureButton = createIconButton("\uD83D\uDDBC", "绑定", this::bindTextureToSelectedPart); - - // 只显示常用按钮,保持干净 - addButton.setPreferredSize(new Dimension(36, 28)); - removeButton.setPreferredSize(new Dimension(36, 28)); - upButton.setPreferredSize(new Dimension(36, 28)); - downButton.setPreferredSize(new Dimension(36, 28)); - bindTextureButton.setPreferredSize(new Dimension(36, 28)); - - removeButton.setEnabled(false); - upButton.setEnabled(false); - downButton.setEnabled(false); - bindTextureButton.setEnabled(false); - - buttonPanel.add(addButton); - buttonPanel.add(removeButton); - buttonPanel.add(upButton); - buttonPanel.add(downButton); - buttonPanel.add(bindTextureButton); - - return buttonPanel; - } - - private JPanel createSettingsPanel() { - JPanel settingsPanel = new JPanel(new BorderLayout(8, 0)); - settingsPanel.setBackground(BACKGROUND_COLOR); - settingsPanel.setBorder(BorderFactory.createEmptyBorder(4, 0, 4, 0)); - - JLabel opacityLabel = new JLabel("不透明度"); - opacityLabel.setForeground(TEXT_COLOR); - opacityLabel.setFont(opacityLabel.getFont().deriveFont(12f)); - opacityLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 6)); - - opacitySlider = createModernSlider(); - opacityValueLabel = new JLabel("100%"); - opacityValueLabel.setForeground(TEXT_COLOR); - opacityValueLabel.setFont(opacityValueLabel.getFont().deriveFont(12f)); - opacityValueLabel.setPreferredSize(new Dimension(40, 20)); - - opacitySlider.addChangeListener(e -> { - if (ignoreSliderEvents) return; - onOpacityChanged(); - }); - - JPanel left = new JPanel(new BorderLayout()); - left.setBackground(BACKGROUND_COLOR); - left.add(opacityLabel, BorderLayout.WEST); - left.add(opacitySlider, BorderLayout.CENTER); - - settingsPanel.add(left, BorderLayout.CENTER); - settingsPanel.add(opacityValueLabel, BorderLayout.EAST); - - return settingsPanel; - } - - private JSlider createModernSlider() { - JSlider slider = new JSlider(0, 100, 100); - slider.setBackground(BACKGROUND_COLOR); - slider.setForeground(ACCENT_COLOR); // 用于已填充部分 - // 应用自定义的扁平UI - slider.setUI(new ModernSliderUI(slider)); - return slider; - } - - private ModernButton createIconButton(String icon, String tooltip, Runnable action) { - ModernButton button = new ModernButton(icon); - button.setToolTipText(tooltip); - button.addActionListener(e -> action.run()); - // 增大图标字体,使其看起来更像图标 - button.setFont(button.getFont().deriveFont(14f)); - return button; - } - - private void showAddMenu() { - JPopupMenu addMenu = new ModernPopupMenu(); - - String[] menuItems = { - "📄 创建空图层", - "🖼️ 从文件创建图层", - "🎨 创建透明图层", - "---", - "📂 从PSD文件导入" - }; - - Runnable[] actions = { - this::createEmptyPart, - this::createPartWithTextureFromFile, - this::createPartWithTransparentTexture, - null, - this::importPSDFile - }; - - for (int i = 0; i < menuItems.length; i++) { - if (menuItems[i].equals("---")) { - // 使用现代化的分隔符 - JSeparator separator = new JSeparator(); - separator.setBackground(BORDER_COLOR); - separator.setForeground(BORDER_COLOR); - addMenu.add(separator); - } else { - JMenuItem item = new ModernMenuItem(menuItems[i]); - if (actions[i] != null) { - int finalI = i; - item.addActionListener(e -> actions[finalI].run()); - } - addMenu.add(item); - } - } - - addMenu.show(addButton, 0, addButton.getHeight()); - } - - public void reloadFromModel() { - // 修正:记录所有选中项 - List selectedParts = layerList.getSelectedValuesList(); - - listModel.clear(); - if (model == null) return; - try { - List parts = model.getParts(); - if (parts != null) { - // 图层是反向显示的(顶部是索引0) - for (int i = parts.size() - 1; i >= 0; i--) { - listModel.addElement(parts.get(i)); - } - } - } catch (Exception ex) { - ex.printStackTrace(); - } - - // 修正:重新选中之前选中的图层块 - setSelectedLayers(selectedParts); - } - - /** - * 【新增方法】执行多选拖拽后的图层块重排序操作。 - * 供 CompositeLayerTransferHandler (原 LayerReorderTransferHandler) 调用。 - * @param srcIndices 列表中的视觉源索引数组(从上到下,已排序)。 - * @param dropIndex 列表中的视觉目标插入索引。 - */ - public void performBlockReorder(int[] srcIndices, int dropIndex) { - if (model == null || srcIndices.length == 0) return; - - // 1. 获取当前的视觉图层列表 - List visualList = new ArrayList<>(listModel.size()); - for (int i = 0; i < listModel.size(); i++) visualList.add(listModel.get(i)); - - // 2. 识别并提取要移动的 ModelPart 块 - List partsToMove = new ArrayList<>(srcIndices.length); - for (int index : srcIndices) { - partsToMove.add(listModel.getElementAt(index)); - } - - // 3. 从列表中移除要移动的块 - visualList.removeAll(partsToMove); - - // 4. 计算实际插入点 (新的列表大小) - int newDropIndex = dropIndex; - - // newDropIndex 不超过新的列表大小 - newDropIndex = Math.min(newDropIndex, visualList.size()); - - // 5. 将块插入到新的位置 - visualList.addAll(newDropIndex, partsToMove); - - // 6. 更新模型和UI - updateModelAndUIFromVisualList(visualList, partsToMove); - } - - private void updateUIState() { - // 修正:支持多选 - List selected = layerList.getSelectedValuesList(); - boolean hasSelection = !selected.isEmpty(); - boolean singleSelection = selected.size() == 1; - - if (singleSelection) { - updateOpacitySlider(selected.get(0)); - } else { - // 多选或未选中时,重置不透明度滑块UI - ignoreSliderEvents = true; - opacitySlider.setValue(100); - opacityValueLabel.setText("---"); - ignoreSliderEvents = false; - } - - removeButton.setEnabled(hasSelection); - // 多选时启用上下移动按钮 - upButton.setEnabled(hasSelection); - downButton.setEnabled(hasSelection); - // 绑定贴图仍然只在单选时有意义 - bindTextureButton.setEnabled(singleSelection); - } - - /** - * 【新增辅助方法】更新模型和UI,并重新选中块。 - */ - private void updateModelAndUIFromVisualList(List visualList, List selectedParts) { - // 刷新模型:这一步是关键,它更新了 model.getParts() 的内部顺序 - operationManager.moveLayer(visualList); - - // 刷新列表模型 (UI) - ignoreSliderEvents = true; - listModel.clear(); - for (ModelPart p : visualList) { - listModel.addElement(p); - } - ignoreSliderEvents = false; - - // 重新选中块 - setSelectedLayers(selectedParts); - - // 刷新缩略图 - if (!selectedParts.isEmpty()) { - refreshSelectedThumbnail(); - } - } - - /** - * 将指定的图层块作为整体重新选中。 - * 供外部调用,用于在模型操作后设置当前选中的多图层。 - * @param parts 要选中的 ModelPart 列表。 - */ - public void setSelectedLayers(List parts) { - if (!SwingUtilities.isEventDispatchThread()) { - SwingUtilities.invokeLater(() -> setSelectedLayers(parts)); - return; - } - if (parts.isEmpty()) { - layerList.clearSelection(); - return; - } - List indicesList = new ArrayList<>(parts.size()); - for (int i = 0; i < listModel.getSize(); i++) { - if (parts.contains(listModel.getElementAt(i))) { - indicesList.add(i); - } - } - if (!indicesList.isEmpty()) { - int[] indices = indicesList.stream().mapToInt(i->i).toArray(); - int[] currentIndices = layerList.getSelectedIndices(); - if (Arrays.equals(currentIndices, indices)) { - return; - } - layerList.setIgnoreRepaint(true); - try { - ListSelectionModel selectionModel = layerList.getSelectionModel(); - selectionModel.setValueIsAdjusting(true); - try { - selectionModel.clearSelection(); - for (int index : indices) { - selectionModel.addSelectionInterval(index, index); - } - } finally { - selectionModel.setValueIsAdjusting(false); - } - layerList.ensureIndexIsVisible(indices[0]); - } finally { - layerList.setIgnoreRepaint(false); - layerList.repaint(); - } - } - } - - - private void updateOpacitySlider(ModelPart part) { - float opacity = extractOpacity(part); - int value = Math.round(opacity * 100); - - ignoreSliderEvents = true; - try { - opacitySlider.setValue(value); - opacityValueLabel.setText(value + "%"); - } finally { - ignoreSliderEvents = false; - } - } - - private float extractOpacity(ModelPart part) { - try { - Method method = part.getClass().getMethod("getOpacity"); - Object value = method.invoke(part); - if (value instanceof Float) return (Float) value; - } catch (Exception e) { - try { - Field field = part.getClass().getDeclaredField("opacity"); - field.setAccessible(true); - Object value = field.get(part); - if (value instanceof Float) return (Float) value; - } catch (Exception ignored) {} - } - return 1.0f; - } - - private void onOpacityChanged() { - ModelPart sel = layerList.getSelectedValue(); - if (sel == null) return; - - int value = opacitySlider.getValue(); - opacityValueLabel.setText(value + "%"); - - setPartOpacity(sel, value / 100.0f); - - if (model != null) model.markNeedsUpdate(); - layerList.repaint(); - refreshSelectedThumbnail(); - } - - private void createEmptyPart() { - String name = JOptionPane.showInputDialog(SwingUtilities.getWindowAncestor(this), "新图层名称:", "新图层"); - if (name == null || name.trim().isEmpty()) return; - - // 传入名称 - operationManager.addLayer(name); - reloadFromModel(); - - ModelPart newPart = findPartByName(name); - if (newPart != null) { - selectPart(newPart); - thumbnailManager.generateThumbnail(newPart); - } - } - - private ModelPart findPartByName(String name) { - if (model == null) return null; - Map partMap = model.getPartMap(); - return partMap != null ? partMap.get(name) : null; - } - - public Map getModelPartMap() { - if (model == null) return null; - return model.getPartMap(); - } - - private void showRenameDialog(ModelPart part) { - String newName = (String) JOptionPane.showInputDialog( - SwingUtilities.getWindowAncestor(this), - "输入新名称:", - "重命名图层", - JOptionPane.PLAIN_MESSAGE, - null, - null, - part.getName() - ); - - if (newName != null && !newName.trim().isEmpty()) { - renamePart(part, newName); - reloadFromModel(); - refreshSelectedThumbnail(); - } - } - - public void setModel(Model2D model) { - this.model = model; - this.psdImporter = new PSDImporter(model, renderPanel, this); - this.operationManager = new LayerOperationManager(model); - reloadFromModel(); - generateAllThumbnails(); - } - - public void setRenderPanel(ModelRenderPanel panel) { - this.renderPanel = panel; - this.thumbnailManager = new ThumbnailManager(panel); - this.psdImporter = new PSDImporter(model, panel, this); - } - - /** - * 【JnaFileChooser 替换】从文件选择器导入 PSD 文件。 - */ - private void importPSDFile() { - JnaFileChooser chooser = new JnaFileChooser(); - chooser.setTitle("选择 PSD 文件 (*.psd)"); - chooser.addFilter("PSD文件 (*.psd)", "psd"); - chooser.setMultiSelectionEnabled(false); - chooser.setMode(JnaFileChooser.Mode.Files); - - if (chooser.showOpenDialog(SwingUtilities.getWindowAncestor(this))) { - psdImporter.importPSDFile(chooser.getSelectedFile()); - } - } - - private void setPartOpacity(ModelPart part, float opacity) { - try { - Method method = part.getClass().getMethod("setOpacity", float.class); - method.invoke(part, opacity); - } catch (Exception e) { - try { - Field field = part.getClass().getDeclaredField("opacity"); - field.setAccessible(true); - field.setFloat(part, opacity); - } catch (Exception ignored) {} - } - } - - private TitledBorder createModernBorder(String title) { - TitledBorder border = BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(BORDER_COLOR, 1), - title - ); - border.setTitleColor(TEXT_COLOR); - border.setTitleFont(border.getTitleFont().deriveFont(Font.PLAIN, 12f)); - return border; - } - - public Texture createTextureFromBufferedImage(BufferedImage img, String texName) { - BufferedImage flippedImage = flipImageVertically(img); - return Texture.createFromBufferedImage(texName, flippedImage); - } - - private BufferedImage flipImageVertically(BufferedImage img) { - int width = img.getWidth(); - int height = img.getHeight(); - BufferedImage flipped = new BufferedImage(width, height, img.getType()); - Graphics2D g = flipped.createGraphics(); - g.drawImage(img, 0, height, width, -height, null); - g.dispose(); - return flipped; - } - - public void refreshCurrentThumbnail() { - refreshSelectedThumbnail(); - } - - private void selectPart(ModelPart part) { - if (part == null) return; - for (int i = 0; i < listModel.getSize(); i++) { - if (listModel.get(i) == part) { - layerList.setSelectedIndex(i); - layerList.ensureIndexIsVisible(i); - return; - } - } - } - - private void renamePart(ModelPart part, String newName) { - if (part == null) return; - part.setName(newName); - } - - public Model2D getModel() { - return model; - } - - /** - * 【JnaFileChooser 替换】打开文件选择器,绑定贴图到选中部件。 - */ - private void bindTextureToSelectedPart() { - ModelPart sel = layerList.getSelectedValue(); - if (sel == null) return; - - JnaFileChooser chooser = new JnaFileChooser(); - chooser.setTitle("选择贴图文件"); - chooser.addFilter("图片文件 (*.png, *.jpg, *.jpeg)", "png", "jpg", "jpeg"); - chooser.setMode(JnaFileChooser.Mode.Files); - - if (!chooser.showOpenDialog(SwingUtilities.getWindowAncestor(this))) return; - - File f = chooser.getSelectedFile(); - - try { - BufferedImage img = null; - try { - img = ImageIO.read(f); - } catch (Exception ignored) { - } - - Mesh2D targetMesh = null; - try { - List list = sel.getMeshes(); - if (!list.isEmpty() && list.get(0) != null) { - targetMesh = list.get(0); - } - } catch (Exception ignored) { - } - - if (targetMesh == null) { - if (img == null) { - img = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB); - } - targetMesh = MeshTextureUtil.createQuadForImage(img, sel.getName() + "_mesh"); - try { - sel.addMesh(targetMesh); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - final Mesh2D meshToBind = targetMesh; - final String filePath = f.getAbsolutePath(); - final String texName = sel.getName() + "_tex"; - - if (renderPanel != null) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - Texture texture = Texture.createFromFile(texName, filePath); - List partMeshes = sel.getMeshes(); - Mesh2D actualMesh = null; - if (partMeshes != null && !partMeshes.isEmpty()) { - actualMesh = partMeshes.get(partMeshes.size() - 1); - } - - if (actualMesh != null) { - actualMesh.setTexture(texture); - } else { - // 修复:将无法解析的 'mesh' 替换为正确的局部变量 'meshToBind' - meshToBind.setTexture(texture); - } - - model.addTexture(texture); - model.markNeedsUpdate(); - } catch (Throwable ex) { - ex.printStackTrace(); - } - }); - } else { - if (img == null) img = ImageIO.read(f); - Texture mem = MeshTextureUtil.tryCreateTextureFromImageMemory(img, texName); - if (mem != null) { - meshToBind.setTexture(mem); - model.addTexture(mem); - model.markNeedsUpdate(); - } else { - System.err.println("无法在无 GL 上下文中创建纹理: " + filePath); - } - } - - reloadFromModel(); - selectPart(sel); - refreshSelectedThumbnail(); - } catch (Exception ex) { - JOptionPane.showMessageDialog(this, "绑定贴图失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - ex.printStackTrace(); - } - } - - private void onRemoveLayer() { - List selectedParts = layerList.getSelectedValuesList(); - if (selectedParts.isEmpty()) return; - - String names = selectedParts.stream().map(ModelPart::getName).collect(java.util.stream.Collectors.joining("、")); - int r = JOptionPane.showConfirmDialog(SwingUtilities.getWindowAncestor(this), "确认删除图层:" + names + " ?", "确认删除", JOptionPane.YES_NO_OPTION); - if (r != JOptionPane.YES_OPTION) return; - - try { - for(ModelPart part : selectedParts) { - operationManager.removeLayer(part); - thumbnailManager.removeThumbnail(part); - if (part == selectedParts.get(0)) { - if (renderPanel != null && renderPanel.getParametersManagement() != null) { - renderPanel.getParametersManagement().removeParameter(part, "all"); - renderPanel.getGlContextManager().executeInGLContext(() -> { - if (renderPanel != null && renderPanel.getParametersManagement() != null) { - renderPanel.getParametersManagement().removeParameter(part, "all"); - } - }); - } - } - } - reloadFromModel(); - } catch (Exception ex) { - // 修复:将父组件改为顶层窗口 - JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(this), "删除失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - } - } - - private void moveSelectedUp() { - moveSelectedBlock(-1); - } - - private void moveSelectedDown() { - moveSelectedBlock(1); - } - - /** - * 【新增方法】将选中的图层块作为一个整体上移/下移一位。 - * @param direction -1 (上移) 或 1 (下移) - */ - private void moveSelectedBlock(int direction) { - List selectedParts = layerList.getSelectedValuesList(); - if (selectedParts.isEmpty()) return; - - int minIndex = layerList.getMinSelectionIndex(); - int maxIndex = layerList.getMaxSelectionIndex(); - - if (direction == -1) { // 向上移动 - if (minIndex <= 0) return; - // 目标位置是 minIndex - 1 - performBlockReorder(layerList.getSelectedIndices(), minIndex - 1); - } else { // 向下移动 - if (maxIndex >= listModel.getSize() - 1) return; - // 目标位置是 maxIndex + 1 (即在 maxIndex 所在的块后插入) - performBlockReorder(layerList.getSelectedIndices(), maxIndex + 1); - } - } - - - /** - * 【JnaFileChooser 替换】打开文件选择器,从文件创建图层。 - */ - private void createPartWithTextureFromFile() { - JnaFileChooser chooser = new JnaFileChooser(); - chooser.setTitle("选择图片文件创建图层"); - chooser.addFilter("图片文件 (*.png, *.jpg, *.jpeg)", "png", "jpg", "jpeg"); - chooser.setMode(JnaFileChooser.Mode.Files); - - if (!chooser.showOpenDialog(SwingUtilities.getWindowAncestor(this))) return; - File f = chooser.getSelectedFile(); - - createPartWithTextureFromFile(f); - } - - /** - * 【重构核心逻辑】从指定文件创建图层的核心逻辑。供文件选择器和拖放使用。 - * @param f 图片文件 - */ - private void createPartWithTextureFromFile(File f) { - try { - BufferedImage img = ImageIO.read(f); - if (img == null) throw new IOException("无法读取图片:" + f.getAbsolutePath()); - - // 1. 获取用户输入的名称(或默认文件名) - String name = JOptionPane.showInputDialog(SwingUtilities.getWindowAncestor(this), "新图层名称:", f.getName()); - if (name == null || name.trim().isEmpty()) name = f.getName(); - - // --- 修复:确保图层名称唯一性 start --- - // 解决 "Part already exists" 错误,确保 ModelPart 名称唯一 - Map partMap = getModelPartMap(); - if (partMap != null) { - String uniqueName = name; - int counter = 1; - - // 分离文件名和扩展名 - String nameWithoutExt = name; - String extension = ""; - int dotIndex = name.lastIndexOf('.'); - if (dotIndex > 0) { // 确保点不在开头 - nameWithoutExt = name.substring(0, dotIndex); - extension = name.substring(dotIndex); - } - - // 剥离已有的 (数字) 部分,从干净的基础名开始计数 - String baseNameForCounter = nameWithoutExt; - int bracketStart = baseNameForCounter.lastIndexOf('('); - int bracketEnd = baseNameForCounter.lastIndexOf(')'); - - if (bracketStart > 0 && bracketEnd == baseNameForCounter.length() - 1) { - try { - // 检查括号中的内容是否为数字 - Integer.parseInt(baseNameForCounter.substring(bracketStart + 1, bracketEnd).trim()); - baseNameForCounter = baseNameForCounter.substring(0, bracketStart).trim(); - } catch (NumberFormatException ignored) { - // 不是数字,则保持不变 - } - } - - // 检查并生成唯一名称 - while (partMap.containsKey(uniqueName)) { - uniqueName = baseNameForCounter + " (" + counter + ")" + extension; - counter++; - if (counter > 100) { // 避免无限循环 - throw new IllegalStateException("无法生成唯一图层名称。"); - } - } - name = uniqueName; // 使用最终的唯一名称 - } - // --- 修复:确保图层名称唯一性 end --- - - ModelPart part = model.createPart(name); - // 修复上一个问题中 GL Context lambda 无法解析 'mesh' 的编译错误 - final Mesh2D mesh = MeshTextureUtil.createQuadForImage(img, name + "_mesh"); - part.addMesh(mesh); - - if (renderPanel != null) { - final String texName = name + "_tex"; - final String filePath = f.getAbsolutePath(); - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - Texture texture = Texture.createFromFile(texName, filePath); - List partMeshes = part.getMeshes(); - Mesh2D actualMesh = null; - if (partMeshes != null && !partMeshes.isEmpty()) { - actualMesh = partMeshes.get(partMeshes.size() - 1); - } - - if (actualMesh != null) { - actualMesh.setTexture(texture); - } else { - mesh.setTexture(texture); - } - - model.addTexture(texture); - model.markNeedsUpdate(); - } catch (Throwable ex) { - ex.printStackTrace(); - } - }); - } else { - Texture memTex = MeshTextureUtil.tryCreateTextureFromImageMemory(img, name + "_tex"); - if (memTex != null) { - mesh.setTexture(memTex); - model.addTexture(memTex); - model.markNeedsUpdate(); - } - } - - reloadFromModel(); - selectPart(part); - thumbnailManager.generateThumbnail(part); - } catch (Exception ex) { - JOptionPane.showMessageDialog(this, "创建带贴图图层失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - } - } - - - public void endDragOperation() { - if (isDragging && draggedPart != null && dragStartPosition != null) { - Vector2f endPosition = draggedPart.getPosition(); - if (!endPosition.equals(dragStartPosition)) { - recordDragOperation(draggedPart, dragStartPosition, endPosition); - } - isDragging = false; - draggedPart = null; - dragStartPosition = null; - } - } - - private void recordDragOperation(ModelPart part, Vector2f startPos, Vector2f endPos) { - OperationHistoryManager manager = OperationHistoryManager.getInstance(); - if (manager != null) { - manager.recordOperation("DRAG_PART", part, startPos, endPos); - } - } - - private void createPartWithTransparentTexture() { - String name = JOptionPane.showInputDialog(SwingUtilities.getWindowAncestor(this), "新图层名称(透明):", "透明图层"); - if (name == null || name.trim().isEmpty()) return; - int w = 128, h = 128; - try { - String wh = JOptionPane.showInputDialog(SwingUtilities.getWindowAncestor(this), "输入尺寸(宽x高,例如 128x128)或留空使用 128x128:", "128x128"); - if (wh != null && wh.contains("x")) { - String[] sp = wh.split("x"); - w = Math.max(1, Integer.parseInt(sp[0].trim())); - h = Math.max(1, Integer.parseInt(sp[1].trim())); - } - } catch (Exception ignored) { - } - BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - - ModelPart part = model.createPart(name); - Mesh2D mesh = MeshTextureUtil.createQuadForImage(img, name + "_mesh"); - part.addMesh(mesh); - - Texture memTex = MeshTextureUtil.tryCreateTextureFromImageMemory(img, name + "_tex"); - if (memTex != null) { - mesh.setTexture(memTex); - model.addTexture(memTex); - } - - model.markNeedsUpdate(); - // 传入名称 - operationManager.addLayer(part.getName()); - reloadFromModel(); - selectPart(part); - thumbnailManager.generateThumbnail(part); - } - - public LayerOperationManager getLayerOperationManager() { - return operationManager; - } - - - // ==================================================================== - // 现代化的内部UI类 - // ==================================================================== - - /** - * 【新类】复合拖放处理器:统一处理外部文件拖放和内部图层重排。 - * 将其设置给 JList (layerList),以确保它优先于 JScrollPane 捕获事件。 - */ - private class CompositeLayerTransferHandler extends TransferHandler { - private final LayerReorderTransferHandler internalReorderHandler; - private final List IMAGE_EXTENSIONS = List.of("png", "jpg", "jpeg"); - private static final String PSD_EXTENSION = "psd"; - - public CompositeLayerTransferHandler(ModelLayerPanel panel) { - // 内部图层重排处理器实例 - this.internalReorderHandler = new LayerReorderTransferHandler(panel); - } - - @Override - public int getSourceActions(JComponent c) { - // 委托给内部处理器处理拖出操作 (内部重排) - return internalReorderHandler.getSourceActions(c); - } - - @Override - public Transferable createTransferable(JComponent c) { - // 委托给内部处理器处理拖出数据 (内部重排) - return internalReorderHandler.createTransferable(c); - } - - @Override - public boolean canImport(TransferSupport support) { - // 1. 检查是否为外部文件拖放 (文件列表 DataFlavor) - 优先处理 - if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { - return true; - } - - // 2. 否则,委托给内部处理器处理 (图层重排) - return internalReorderHandler.canImport(support); - } - - @Override - public boolean importData(TransferSupport support) { - // 1. 处理外部文件拖放 - if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { - try { - @SuppressWarnings("unchecked") - Transferable t = support.getTransferable(); - List files = (List) t.getTransferData(DataFlavor.javaFileListFlavor); - - if (files.size() != 1) { - JOptionPane.showMessageDialog(ModelLayerPanel.this, "仅支持拖放单个文件。", "导入失败", JOptionPane.WARNING_MESSAGE); - return false; - } - - File file = files.get(0); - String fileName = file.getName().toLowerCase(); - String extension = ""; - int dotIndex = fileName.lastIndexOf('.'); - if (dotIndex != -1) { - extension = fileName.substring(dotIndex + 1); - } - - if (PSD_EXTENSION.equals(extension)) { - psdImporter.importPSDFile(file); - return true; - } else if (IMAGE_EXTENSIONS.contains(extension)) { - createPartWithTextureFromFile(file); - return true; - } else { - JOptionPane.showMessageDialog(ModelLayerPanel.this, "不支持的文件类型: ." + extension, "导入失败", JOptionPane.WARNING_MESSAGE); - return false; - } - - } catch (Exception ex) { - JOptionPane.showMessageDialog(ModelLayerPanel.this, "文件导入失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - ex.printStackTrace(); - return false; - } - } - - // 2. 否则,委托给内部处理器处理 (图层重排) - return internalReorderHandler.importData(support); - } - } - - - /** - * 现代化的圆角按钮 - */ - private static class ModernButton extends JButton { - private boolean hovered = false; - private boolean pressed = false; - - public ModernButton(String text) { - super(text); - setContentAreaFilled(false); - setFocusPainted(false); - setBorderPainted(false); - setForeground(TEXT_COLOR); - setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8)); - setOpaque(false); - setFont(getFont().deriveFont(Font.PLAIN, 13f)); - addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - pressed = true; - repaint(); - } - @Override - public void mouseReleased(MouseEvent e) { - pressed = false; - repaint(); - } - @Override - public void mouseEntered(MouseEvent e) { - hovered = true; - repaint(); - } - @Override - public void mouseExited(MouseEvent e) { - hovered = false; - pressed = false; - repaint(); - } - }); - } - - @Override - protected void paintComponent(Graphics g) { - Graphics2D g2 = (Graphics2D) g.create(); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // 扁平样式:只有 hover/pressed 时才有轻微背景 - if (!isEnabled()) { - // 半透明效果 - g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f)); - } - - if (pressed) { - g2.setColor(ACCENT_PRESSED_COLOR); - g2.fillRect(0, 0, getWidth(), getHeight()); - } else if (hovered) { - g2.setColor(BUTTON_HOVER_COLOR); - g2.fillRect(0, 0, getWidth(), getHeight()); - } // 默认不绘制背景以保持极简 - - // 文本居中 - g2.setColor(isEnabled() ? TEXT_COLOR : BORDER_COLOR.darker()); - FontMetrics fm = g2.getFontMetrics(); - int stringWidth = fm.stringWidth(getText()); - int stringHeight = fm.getAscent(); - int x = (getWidth() - stringWidth) / 2; - int y = (getHeight() + stringHeight) / 2 - 2; - g2.setFont(getFont()); - g2.drawString(getText(), x, y); - - g2.dispose(); - } - } - - /** - * 现代化的菜单项 - */ - private static class ModernMenuItem extends JMenuItem { - public ModernMenuItem(String text) { - super(text); - setBackground(SURFACE_COLOR); - setForeground(TEXT_COLOR); - setBorder(BorderFactory.createEmptyBorder(6, 10, 6, 10)); - // 使用 UIManager 设置的选中颜色 - setOpaque(true); - } - } - - /** - * 现代化的弹出菜单 - */ - private static class ModernPopupMenu extends JPopupMenu { - public ModernPopupMenu() { - setBackground(SURFACE_COLOR); - setBorder(BorderFactory.createLineBorder(BORDER_COLOR)); - } - } - - /** - * 现代化的扁平滑块UI - */ - private static class ModernSliderUI extends BasicSliderUI { - private final RoundRectangle2D.Float trackShape = new RoundRectangle2D.Float(); - private final RoundRectangle2D.Float thumbShape = new RoundRectangle2D.Float(); - - public ModernSliderUI(JSlider b) { - super(b); - } - - @Override - public void paintTrack(Graphics g) { - Graphics2D g2 = (Graphics2D) g.create(); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // 轨道背景 - g2.setColor(BORDER_COLOR); - int trackY = (trackRect.height / 2) - 3 + trackRect.y; - trackShape.setRoundRect(trackRect.x, trackY, trackRect.width, 6, 6, 6); - g2.fill(trackShape); - - // 轨道前景 (已填充部分) - g2.setColor(ACCENT_COLOR); - int thumbPos = thumbRect.x + (thumbRect.width / 2); - trackShape.setRoundRect(trackRect.x, trackY, thumbPos - trackRect.x, 6, 6, 6); - g2.fill(trackShape); - - g2.dispose(); - } - - @Override - public void paintThumb(Graphics g) { - Graphics2D g2 = (Graphics2D) g.create(); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // 绘制滑块 - thumbShape.setRoundRect(thumbRect.x, thumbRect.y, thumbRect.width, thumbRect.height, 14, 14); - - // 滑块颜色 - g2.setColor(isDragging() ? ACCENT_PRESSED_COLOR : ACCENT_HOVER_COLOR); - g2.fill(thumbShape); - - // 滑块内部的小点 - g2.setColor(Color.WHITE); - g2.fillOval(thumbRect.x + (thumbRect.width/2) - 2, thumbRect.y + (thumbRect.height/2) - 2, 4, 4); - - g2.dispose(); - } - - @Override - protected Dimension getThumbSize() { - // 定义滑块大小 - return new Dimension(14, 14); - } - } - - /** - * 【只处理外部文件拖放】的处理器。 - * 将其设置给顶层窗口的内容面板。 - */ - private class FileDropOnlyTransferHandler extends TransferHandler { - private final List IMAGE_EXTENSIONS = List.of("png", "jpg", "jpeg"); - private static final String PSD_EXTENSION = "psd"; - private static final String MODEL_EXTENSION = "model"; - - @Override - public boolean canImport(TransferSupport support) { - return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor); - } - - @Override - public boolean importData(TransferSupport support) { - if (!canImport(support)) { - return false; - } - try { - Transferable t = support.getTransferable(); - @SuppressWarnings("unchecked") - List files = (List) t.getTransferData(DataFlavor.javaFileListFlavor); - if (files.size() != 1) { - JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(ModelLayerPanel.this), "仅支持拖放单个文件。", "导入失败", JOptionPane.WARNING_MESSAGE); - return false; - } - final File file = files.get(0); - String fileName = file.getName().toLowerCase(); - String extension = ""; - int dotIndex = fileName.lastIndexOf('.'); - if (dotIndex != -1) { - extension = fileName.substring(dotIndex + 1); - } - final String finalExtension = extension; - SwingUtilities.invokeLater(() -> { - if (MODEL_EXTENSION.equals(finalExtension)) { - Window window = SwingUtilities.getWindowAncestor(ModelLayerPanel.this); - if (window instanceof MainWindow mainWindow) { - if (mainWindow.shouldAskUserToSave()) { - int confirm = JOptionPane.showConfirmDialog( - mainWindow, - "当前模型已修改。加载新模型 " + file.getName() + " 前是否保存更改?", - "加载模型确认", - JOptionPane.YES_NO_CANCEL_OPTION - ); - if (confirm == JOptionPane.CANCEL_OPTION) { - return; - } - if (confirm == JOptionPane.YES_OPTION) { - mainWindow.saveData(false); - } - } - mainWindow.loadModel(file.getAbsolutePath()); - } else { - JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(ModelLayerPanel.this), "无法获取主窗口引用,无法加载模型文件。", "导入失败", JOptionPane.ERROR_MESSAGE); - } - } else if (PSD_EXTENSION.equals(finalExtension)) { - psdImporter.importPSDFile(file); - } else if (IMAGE_EXTENSIONS.contains(finalExtension)) { - createPartWithTextureFromFile(file); - } else { - JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(ModelLayerPanel.this), "不支持的文件类型: ." + finalExtension, "导入失败", JOptionPane.WARNING_MESSAGE); - } - }); - return true; - } catch (Exception ex) { - JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(ModelLayerPanel.this), "文件导入失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - ex.printStackTrace(); - return false; - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelPartInfoPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelPartInfoPanel.java deleted file mode 100644 index 819ad11..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelPartInfoPanel.java +++ /dev/null @@ -1,289 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.model.ModelEvent; // 引入 ModelEvent 接口 -import com.chuangzhou.vivid2D.render.model.ModelPart; -import org.joml.Vector2f; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.*; -import java.awt.*; -import java.util.Objects; - -/** - * 用于显示选中 ModelPart 部件属性的面板。 - */ -public class ModelPartInfoPanel extends JPanel implements ModelEvent { // 实现 ModelEvent 接口 - private static final Logger logger = LoggerFactory.getLogger(ModelPartInfoPanel.class); - private final ModelRenderPanel renderPanel; - - // 【新增】当前正在监控的 ModelPart - private ModelPart monitoredPart = null; - - // UI 字段,用于显示 ModelPart 的属性值 - private final JLabel nameValueLabel = new JLabel("无选中"); - private final JLabel positionXLabel = new JLabel("0.00"); // 统一格式 - private final JLabel positionYLabel = new JLabel("0.00"); // 统一格式 - private final JLabel rotationLabel = new JLabel("0.00°"); // 统一格式 - private final JLabel scaleXLabel = new JLabel("1.00"); // 统一格式 - private final JLabel scaleYLabel = new JLabel("1.00"); // 统一格式 - private final JLabel visibleLabel = new JLabel("false"); - private final JLabel opacityLabel = new JLabel("100%"); - private final JLabel blendModeLabel = new JLabel("NORMAL"); - private final JLabel meshCountLabel = new JLabel("0"); - private final JLabel childCountLabel = new JLabel("0"); - - /** - * 构造器 - * @param renderPanel 渲染面板实例,用于上下文或将来获取选中信息 - */ - public ModelPartInfoPanel(ModelRenderPanel renderPanel) { - super(new BorderLayout()); - this.renderPanel = Objects.requireNonNull(renderPanel, "ModelRenderPanel 不能为空"); - - setBorder(BorderFactory.createTitledBorder("部件属性")); - - // 使用一个内嵌的 JPanel 来放置属性列表,并将其放入 JScrollPane - JPanel propertiesPanel = new JPanel(new GridBagLayout()); - propertiesPanel.setBackground(UIManager.getColor("Panel.background")); - JScrollPane scrollPane = new JScrollPane(propertiesPanel); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); - - add(scrollPane, BorderLayout.CENTER); - - // 设置 GridBagLayout 约束 - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(2, 5, 2, 5); // 边距 - - // 第一列:属性名 (右对齐) - gbc.gridx = 0; - gbc.anchor = GridBagConstraints.EAST; - gbc.weightx = 0.0; - - // 第二列:属性值 (左对齐,占用剩余空间) - GridBagConstraints gbcValue = new GridBagConstraints(); - gbcValue.insets = new Insets(2, 5, 2, 5); - gbcValue.gridx = 1; - gbcValue.anchor = GridBagConstraints.WEST; - gbcValue.fill = GridBagConstraints.HORIZONTAL; - gbcValue.weightx = 1.0; - - int row = 0; - - // 辅助方法:添加属性行 - row = addPropertyRow(propertiesPanel, "名称:", nameValueLabel, gbc, gbcValue, row); - row = addSeparator(propertiesPanel, gbc, gbcValue, row); - - row = addPropertyRow(propertiesPanel, "位置 (X):", positionXLabel, gbc, gbcValue, row); - row = addPropertyRow(propertiesPanel, "位置 (Y):", positionYLabel, gbc, gbcValue, row); - row = addPropertyRow(propertiesPanel, "旋转 (deg):", rotationLabel, gbc, gbcValue, row); - row = addSeparator(propertiesPanel, gbc, gbcValue, row); - - row = addPropertyRow(propertiesPanel, "缩放 (X):", scaleXLabel, gbc, gbcValue, row); - row = addPropertyRow(propertiesPanel, "缩放 (Y):", scaleYLabel, gbc, gbcValue, row); - row = addSeparator(propertiesPanel, gbc, gbcValue, row); - - row = addPropertyRow(propertiesPanel, "可见:", visibleLabel, gbc, gbcValue, row); - row = addPropertyRow(propertiesPanel, "不透明度:", opacityLabel, gbc, gbcValue, row); - row = addPropertyRow(propertiesPanel, "混合模式:", blendModeLabel, gbc, gbcValue, row); - row = addSeparator(propertiesPanel, gbc, gbcValue, row); - - row = addPropertyRow(propertiesPanel, "网格数量:", meshCountLabel, gbc, gbcValue, row); - row = addPropertyRow(propertiesPanel, "子部件数量:", childCountLabel, gbc, gbcValue, row); - - // 占位符:确保内容靠上 - gbc.gridy = row; - gbc.weighty = 1.0; - propertiesPanel.add(Box.createVerticalGlue(), gbc); - } - - /** - * 辅助方法:添加一行属性(标签 + 值) - */ - private int addPropertyRow(JPanel panel, String labelText, JLabel valueLabel, - GridBagConstraints gbcLabel, GridBagConstraints gbcValue, int row) { - - // 属性名 - JLabel label = new JLabel(labelText); - gbcLabel.gridy = row; - panel.add(label, gbcLabel); - - // 属性值 - gbcValue.gridy = row; - panel.add(valueLabel, gbcValue); - - return row + 1; - } - - /** - * 辅助方法:添加分隔线 - */ - private int addSeparator(JPanel panel, GridBagConstraints gbcLabel, GridBagConstraints gbcValue, int row) { - JSeparator separator = new JSeparator(SwingConstants.HORIZONTAL); - gbcLabel.gridy = row; - gbcLabel.gridx = 0; - gbcLabel.gridwidth = 2; // 跨越两列 - gbcLabel.fill = GridBagConstraints.HORIZONTAL; - gbcLabel.insets = new Insets(5, 0, 5, 0); - panel.add(separator, gbcLabel); - - // 恢复默认的 insets 和 gridwidth - gbcLabel.insets = new Insets(2, 5, 2, 5); - gbcLabel.gridwidth = 1; - gbcValue.insets = new Insets(2, 5, 2, 5); - - return row + 1; - } - - /** - * 将角度标准化到0-360度范围内 - */ - private float normalizeAngle(float degrees) { - degrees = degrees % 360; - if (degrees < 0) { - degrees += 360; - } - return degrees; - } - - /** - * 核心逻辑:从 ModelPart 更新所有显示值。 - */ - private void updateDisplay(ModelPart part) { - if (part == null) { - // 清空显示 - nameValueLabel.setText("无选中"); - positionXLabel.setText("0.00"); - positionYLabel.setText("0.00"); - rotationLabel.setText("0.00°"); - scaleXLabel.setText("1.00"); - scaleYLabel.setText("1.00"); - visibleLabel.setText("false"); - opacityLabel.setText("100%"); - blendModeLabel.setText("NORMAL"); - meshCountLabel.setText("0"); - childCountLabel.setText("0"); - logger.debug("ModelPartInfoPanel: 清空选中部件信息"); - } else { - // 设置新的值 - Vector2f position = part.getPosition(); - Vector2f scale = part.getScale(); - float rotationDeg = (float) Math.toDegrees(part.getRotation()); - rotationDeg = normalizeAngle(rotationDeg); - - nameValueLabel.setText(part.getName()); - positionXLabel.setText(String.format("%.2f", position.x)); - positionYLabel.setText(String.format("%.2f", position.y)); - rotationLabel.setText(String.format("%.2f°", rotationDeg)); - scaleXLabel.setText(String.format("%.2f", scale.x)); - scaleYLabel.setText(String.format("%.2f", scale.y)); - visibleLabel.setText(String.valueOf(part.isVisible())); - opacityLabel.setText(String.format("%d%%", (int)(part.getOpacity() * 100))); - blendModeLabel.setText(part.getBlendMode().name()); - meshCountLabel.setText(String.valueOf(part.getMeshes().size())); - childCountLabel.setText(String.valueOf(part.getChildren().size())); - - logger.debug("ModelPartInfoPanel: 更新选中部件信息 - {}", part.getName()); - } - } - - /** - * 【修改】更新面板以显示指定 ModelPart 的属性,并管理事件监听器。 - */ - public void updatePanel(ModelPart part) { - // 1. 移除旧部件的事件监听 - if (this.monitoredPart != null) { - this.monitoredPart.removeEvent(this); - } - - // 2. 更新正在监控的部件 - this.monitoredPart = part; - - // 3. 添加新部件的事件监听 - if (this.monitoredPart != null) { - this.monitoredPart.addEvent(this); - } - - // 4. 立即更新显示 - updateDisplay(part); - - // 确保 UI 在 EDT 上更新 - revalidate(); - repaint(); - } - - /** - * 【新增】实现 ModelEvent 接口,监听部件属性的变化。 - */ - @Override - public void trigger(String eventName, Object source) { - if (source != this.monitoredPart) return; // 只处理当前监控的部件 - - // 确保 UI 更新在 Swing EDT 上进行 - SwingUtilities.invokeLater(() -> { - ModelPart part = (ModelPart) source; - try { - // 根据事件名只更新变化的属性,提高效率 - switch (eventName) { - case "name": - nameValueLabel.setText(part.getName()); - break; - case "position": - Vector2f position = part.getPosition(); - positionXLabel.setText(String.format("%.2f", position.x)); - positionYLabel.setText(String.format("%.2f", position.y)); - break; - case "rotation": - float rotationDeg = (float) Math.toDegrees(part.getRotation()); - rotationDeg = normalizeAngle(rotationDeg); - rotationLabel.setText(String.format("%.2f°", rotationDeg)); - break; - case "scale": - Vector2f scale = part.getScale(); - scaleXLabel.setText(String.format("%.2f", scale.x)); - scaleYLabel.setText(String.format("%.2f", scale.y)); - break; - case "visible": - visibleLabel.setText(String.valueOf(part.isVisible())); - break; - case "opacity": - opacityLabel.setText(String.format("%d%%", (int)(part.getOpacity() * 100))); - break; - case "blendMode": - blendModeLabel.setText(part.getBlendMode().name()); - break; - case "children": - childCountLabel.setText(String.valueOf(part.getChildren().size())); - break; - case "meshes": - meshCountLabel.setText(String.valueOf(part.getMeshes().size())); - break; - default: - updateDisplay(part); - break; - } - - // 强制刷新 - revalidate(); - repaint(); - } catch (Exception e) { - logger.error("Error updating ModelPartInfoPanel for event {}: {}", eventName, e.getMessage()); - } - }); - } - - /** - * 【新增】清理监听器资源 - */ - @Override - public void removeNotify() { - super.removeNotify(); - // 移除正在监控的部件的事件监听 - if (this.monitoredPart != null) { - this.monitoredPart.removeEvent(this); - this.monitoredPart = null; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java deleted file mode 100644 index 9cd1d1c..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java +++ /dev/null @@ -1,770 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.awt.manager.*; -import com.chuangzhou.vivid2D.render.awt.tools.SelectionTool; -import com.chuangzhou.vivid2D.render.awt.tools.Tool; -import com.chuangzhou.vivid2D.render.awt.tools.VertexDeformationTool; -import com.chuangzhou.vivid2D.render.awt.util.FrameInterpolator; -import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal; -import com.chuangzhou.vivid2D.render.model.AnimationParameter; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.manager.RanderToolsManager; -import com.chuangzhou.vivid2D.render.model.util.tools.VertexDeformationRander; -import com.chuangzhou.vivid2D.render.systems.Camera; -import com.chuangzhou.vivid2D.test.TestModelGLPanel; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.Timer; -import javax.swing.*; -import java.awt.*; -import java.awt.event.*; -import java.awt.image.BufferedImage; -import java.awt.image.VolatileImage; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicReference; - -/** - * vivid2D 模型的 Java 渲染面板 - * - *

该类提供了 vivid2D 模型在 Java 环境下的图形渲染功能, - * 包含基本的 2D 图形绘制、模型显示和交互操作。

- * - * @author tzdwindows 7 - * @version 1.1 - * @see TestModelGLPanel - * @since 2025-10-13 - */ -public class ModelRenderPanel extends JPanel { - private static final Logger logger = LoggerFactory.getLogger(ModelRenderPanel.class); - private final GLContextManager glContextManager; - private final MouseManagement mouseManagement; - private final CameraManagement cameraManagement; - private final WorldManagement worldManagement; - private final AtomicReference modelRef = new AtomicReference<>(); - private final KeyboardManager keyboardManager; - private final CopyOnWriteArrayList clickListeners = new CopyOnWriteArrayList<>(); - private final StatusRecordManagement statusRecordManagement; - private final RanderToolsManager randerToolsManager = RanderToolsManager.getInstance(); - private final AtomicReference parametersManagement = new AtomicReference<>(); - public static final float BORDER_THICKNESS = 6.0f; - public static final float CORNER_SIZE = 12.0f; - private final Timer doubleClickTimer; - private volatile long lastClickTime = 0; - private static final int DOUBLE_CLICK_INTERVAL = 300; - private final ToolManagement toolManagement; - private VolatileImage vImage = null; - - /** - * 获取摄像机实例 - */ - public Camera getCamera() { - return ModelRender.getCamera(); - } - - /** - * 重置摄像机 - */ - public void resetCamera() { - glContextManager.executeInGLContext(ModelRender::resetCamera); - } - - /** - * 构造函数:使用模型路径 - */ - public ModelRenderPanel(String modelPath, int width, int height) { - this.glContextManager = new GLContextManager(modelPath, width, height); - this.statusRecordManagement = new StatusRecordManagement(this, OperationHistoryGlobal.getInstance()); - this.keyboardManager = new KeyboardManager(this); - this.worldManagement = new WorldManagement(this, glContextManager); - this.cameraManagement = new CameraManagement(this, glContextManager, worldManagement); - this.mouseManagement = new MouseManagement(this, glContextManager, cameraManagement, keyboardManager); - this.toolManagement = new ToolManagement(this, randerToolsManager); - - // 注册所有工具 - toolManagement.registerTool(new VertexDeformationTool(this), new VertexDeformationRander()); - initialize(); - - keyboardManager.initKeyboardShortcuts(); - - doubleClickTimer = new Timer(DOUBLE_CLICK_INTERVAL, e -> { - handleSingleClick(); - }); - doubleClickTimer.setRepeats(false); - - modelsUpdate(getModel()); - - ParametersPanel.ParameterEventBroadcaster.getInstance().addListener(new ParametersPanel.ParameterEventListener() { - @Override - public void onParameterUpdated(AnimationParameter p) { - ParametersManagement pm = getParametersManagement(); - if (pm == null) { - logger.warn("ParametersManagement 未初始化,无法应用参数更新。"); - return; - } - final List selectedParts = getSelectedParts(); - if (selectedParts.isEmpty()) { - logger.debug("没有选中的模型部件,跳过应用参数。"); - return; - } - glContextManager.executeInGLContext(() -> { - try { - FrameInterpolator.applyFrameInterpolations(pm, selectedParts, p, logger); - for (ModelPart selectedPart : selectedParts) { - selectedPart.updateMeshVertices(); - } - } catch (Exception ex) { - logger.error("在GL上下文线程中应用关键字动画参数时出错", ex); - } - }); - } - }); - } - - /** - * 构造函数:使用已加载模型 - */ - public ModelRenderPanel(Model2D model, int width, int height) { - this.glContextManager = new GLContextManager(model, width, height); - this.modelRef.set(model); - this.statusRecordManagement = new StatusRecordManagement(this, OperationHistoryGlobal.getInstance()); - this.keyboardManager = new KeyboardManager(this); - this.worldManagement = new WorldManagement(this, glContextManager); - this.cameraManagement = new CameraManagement(this, glContextManager, worldManagement); - this.mouseManagement = new MouseManagement(this, glContextManager, cameraManagement, keyboardManager); - this.toolManagement = new ToolManagement(this, randerToolsManager); - toolManagement.registerTool(new VertexDeformationTool(this),new VertexDeformationRander()); - initialize(); - keyboardManager.initKeyboardShortcuts(); - doubleClickTimer = new Timer(DOUBLE_CLICK_INTERVAL, e -> { - handleSingleClick(); - }); - doubleClickTimer.setRepeats(false); - } - - /** - * 处理双击事件 - */ - private void handleDoubleClick(MouseEvent e) { - float[] modelCoords = worldManagement.screenToModelCoordinates(e.getX(), e.getY()); - if (toolManagement.hasActiveTool() && modelCoords != null) { - glContextManager.executeInGLContext(() -> { - toolManagement.handleMouseDoubleClicked(e, modelCoords[0], modelCoords[1]); - }); - return; - } - } - - /** - * 处理单单击事件 - */ - private void handleSingleClick() {} - - /** - * 添加模型点击监听器 - */ - public void addModelClickListener(ModelClickListener listener) { - clickListeners.add(listener); - } - - /** - * 移除模型点击监听器 - */ - public void removeModelClickListener(ModelClickListener listener) { - clickListeners.remove(listener); - } - - /** - * 获取当前选中的模型部件 - * @return 选中的模型部件列表 - */ - public List getSelectedParts() { - Tool currentTool = toolManagement.getCurrentTool(); - if (currentTool instanceof SelectionTool) { - return ((SelectionTool) currentTool).getSelectedParts(); - } - return java.util.Collections.emptyList(); - } - - /** - * 获取当前选中的网格 - */ - public Mesh2D getSelectedMesh() { - Tool currentTool = toolManagement.getCurrentTool(); - if (currentTool instanceof SelectionTool) { - return ((SelectionTool) currentTool).getSelectedMesh(); - } else if (toolManagement.getDefaultTool() instanceof SelectionTool selectedMesh) { - return selectedMesh.getSelectedMesh(); - } - return null; - } - - /** - * 获取当前选中的所有网格 - */ - public java.util.Set getSelectedMeshes() { - Tool currentTool = toolManagement.getCurrentTool(); - if (currentTool instanceof SelectionTool) { - return ((SelectionTool) currentTool).getSelectedMeshes(); - } else if (toolManagement.getDefaultTool() instanceof SelectionTool selectedMeshes) { - return selectedMeshes.getSelectedMeshes(); - } - return java.util.Collections.emptySet(); - } - - /** - * 清空所有选中的网格 - */ - public void clearSelectedMeshes() { - glContextManager.executeInGLContext(() -> { - // 委托给工具管理系统的当前工具 - Tool currentTool = toolManagement.getCurrentTool(); - if (currentTool instanceof SelectionTool) { - ((SelectionTool) currentTool).clearSelectedMeshes(); - } else { - toolManagement.switchToDefaultTool(); - } - - logger.debug("清空所有选中网格"); - }); - } - - /** - * 全选所有网格 - */ - public void selectAllMeshes() { - glContextManager.executeInGLContext(() -> { - // 委托给工具管理系统的当前工具 - Tool currentTool = toolManagement.getCurrentTool(); - if (currentTool instanceof SelectionTool) { - ((SelectionTool) currentTool).selectAllMeshes(); - logger.info("已全选网格"); - } - }); - } - - /** - * 获取当前选中的部件 - */ - public ModelPart getSelectedPart() { - Mesh2D selectedMesh = getSelectedMesh(); - return selectedMesh != null ? findPartByMesh(selectedMesh) : null; - } - - /** - * 获取鼠标悬停的网格 - */ - public Mesh2D getHoveredMesh() { - // 委托给工具管理系统的当前工具 - Tool currentTool = toolManagement.getCurrentTool(); - if (currentTool instanceof SelectionTool) { - return ((SelectionTool) currentTool).getHoveredMesh(); - } - return null; - } - - private void initialize() { - setLayout(new BorderLayout()); - setPreferredSize(new Dimension(glContextManager.getWidth(), glContextManager.getHeight())); - - // 添加鼠标监听器 - mouseManagement.addMouseListeners(); - - // 创建渲染线程 - glContextManager.startRendering(); - - this.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - int w = getWidth(); - int h = getHeight(); - if (w <= 0 || h <= 0) return; - if (w == glContextManager.getWidth() && h == glContextManager.getHeight()) return; - resize(w, h); - } - }); - - glContextManager.setRepaintCallback(this::repaint); - } - - /** - * 处理鼠标按下事件 - */ - public void handleMousePressed(MouseEvent e) { - if (!glContextManager.isContextInitialized()) return; - final int screenX = e.getX(); - final int screenY = e.getY(); - requestFocusInWindow(); - - // 首先处理中键拖拽(摄像机控制),在任何模式下都可用 - if (SwingUtilities.isMiddleMouseButton(e)) { - glContextManager.setCameraDragging(true); - cameraManagement.setLastCameraDragX(screenX); - cameraManagement.setLastCameraDragY(screenY); - setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); - return; - } - - float[] modelCoords = worldManagement.screenToModelCoordinates(screenX, screenY); - - // 如果有激活的工具,优先交给工具处理 - if (toolManagement.hasActiveTool() && modelCoords != null) { - glContextManager.executeInGLContext(() -> toolManagement.handleMousePressed(e, modelCoords[0], modelCoords[1])); - } - } - - public ToolManagement getToolManagement() { - return toolManagement; - } - - public void switchTool(String toolName) { - glContextManager.executeInGLContext(() -> toolManagement.switchTool(toolName)); - } - - public void switchToDefaultTool() { - glContextManager.executeInGLContext(toolManagement::switchToDefaultTool); - } - - /** - * 切换到液化工具 - */ - public void switchToLiquifyTool() { - switchTool("液化工具"); - } - - public Tool getCurrentTool() { - return toolManagement.getCurrentTool(); - } - - /** - * 处理鼠标拖拽事件 - */ - public void handleMouseDragged(MouseEvent e) { - if (glContextManager.isCameraDragging()) { - final int screenX = e.getX(); - final int screenY = e.getY(); - // 计算鼠标移动距离 - final int deltaX = screenX - cameraManagement.getLastCameraDragX(); - final int deltaY = screenY - cameraManagement.getLastCameraDragY(); - - // 更新最后拖拽位置 - cameraManagement.setLastCameraDragX(screenX); - cameraManagement.setLastCameraDragY(screenY); - - // 确保在 GL 上下文线程中执行摄像机移动 - glContextManager.executeInGLContext(() -> { - try { - Camera camera = ModelRender.getCamera(); - float zoom = camera.getZoom(); - - float worldDeltaX = -deltaX / zoom; - float worldDeltaY = -deltaY / zoom; - - // 应用摄像机移动 - camera.move(worldDeltaX, worldDeltaY); - } catch (Exception ex) { - logger.error("处理摄像机拖拽时出错", ex); - } - }); - return; - } - - final float[][] modelCoords = {worldManagement.screenToModelCoordinates(e.getX(), e.getY())}; - - float modelX = modelCoords[0][0]; - float modelY = modelCoords[0][1]; - for (ModelClickListener listener : clickListeners) { - try { - listener.onModelHover(getSelectedMesh(), modelX, modelY, e.getX(), e.getY()); - } catch (Exception ex) { - logger.error("点击事件监听器执行出错", ex); - } - } - - // 如果有激活的工具,优先交给工具处理 - if (toolManagement.hasActiveTool() && modelCoords[0] != null) { - glContextManager.executeInGLContext(() -> toolManagement.handleMouseDragged(e, modelCoords[0][0], modelCoords[0][1])); - return; - } - } - - /** - * 处理鼠标释放事件 - */ - public void handleMouseReleased(MouseEvent e) { - // 首先处理摄像机拖拽释放 - if (glContextManager.isCameraDragging() && SwingUtilities.isMiddleMouseButton(e)) { - glContextManager.setCameraDragging(false); - // 恢复悬停状态的光标 - updateCursorForHoverState(); - return; - } - - float[] modelCoords = worldManagement.screenToModelCoordinates(e.getX(), e.getY()); - - // 如果有激活的工具,优先交给工具处理 - if (toolManagement.hasActiveTool() && modelCoords != null) { - toolManagement.handleMouseReleased(e, modelCoords[0], modelCoords[1]); - } - } - - /** - * 处理鼠标点击事件 - */ - public void handleMouseClick(MouseEvent e) { - if (!glContextManager.isContextInitialized()) return; - - final int screenX = e.getX(); - final int screenY = e.getY(); - - long currentTime = System.currentTimeMillis(); - boolean isDoubleClick = (currentTime - lastClickTime) < DOUBLE_CLICK_INTERVAL; - lastClickTime = currentTime; - - if (isDoubleClick) { - // 取消单单击计时器 - doubleClickTimer.stop(); - handleDoubleClick(e); - } else { - float[] modelCoords = worldManagement.screenToModelCoordinates(screenX, screenY); - - glContextManager.executeInGLContext(() -> { - try { - if (modelCoords == null) return; - - float modelX = modelCoords[0]; - float modelY = modelCoords[1]; - - logger.debug("点击位置:({}, {})", modelX, modelY); - - for (ModelClickListener listener : clickListeners) { - try { - listener.onModelClicked(getSelectedMesh(), modelX, modelY, screenX, screenY); - } catch (Exception ex) { - logger.error("点击事件监听器执行出错", ex); - } - } - } catch (Exception ex) { - logger.error("处理鼠标点击时出错", ex); - } - }); - - // 如果有激活的工具,优先交给工具处理 - if (toolManagement.hasActiveTool() && modelCoords != null) { - toolManagement.handleMouseClicked(e, modelCoords[0], modelCoords[1]); - doubleClickTimer.restart(); - } - } - } - - /** - * 处理鼠标移动事件 - */ - public void handleMouseMove(MouseEvent e) { - if (!glContextManager.isContextInitialized()) return; - - final int screenX = e.getX(); - final int screenY = e.getY(); - - if (glContextManager.isCameraDragging()) { - return; - } - - float[] modelCoords = worldManagement.screenToModelCoordinates(screenX, screenY); - float modelX = modelCoords[0]; - float modelY = modelCoords[1]; - for (ModelClickListener listener : clickListeners) { - try { - listener.onModelHover(getSelectedMesh(), modelX, modelY, screenX, screenY); - } catch (Exception ex) { - logger.error("点击事件监听器执行出错", ex); - } - } - - // 如果有激活的工具,优先交给工具处理 - if (toolManagement.hasActiveTool() && modelCoords != null) { - toolManagement.handleMouseMoved(e, modelCoords[0], modelCoords[1]); - } - } - - /** - * 根据悬停状态更新光标(无坐标版本,用于鼠标释放后) - */ - private void updateCursorForHoverState() { - Point mousePos = getMousePosition(); - if (mousePos != null) { - float[] modelCoords = worldManagement.screenToModelCoordinates(mousePos.x, mousePos.y); - if (modelCoords != null) { - // 委托给工具管理系统的当前工具 - Tool currentTool = toolManagement.getCurrentTool(); - if (currentTool != null) { - setCursor(currentTool.getToolCursor()); - } - } - } else { - // 鼠标不在面板内,恢复默认光标 - setCursor(Cursor.getDefaultCursor()); - } - } - - /** - * Creates or re-creates the VolatileImage based on the panel's current size. - */ - private void createVolatileImage() { - GraphicsConfiguration gc = getGraphicsConfiguration(); - if (gc != null) { - vImage = gc.createCompatibleVolatileImage(getWidth(), getHeight()); - } - } - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - - // If panel size changes, our volatile image becomes invalid. - if (vImage == null || vImage.getWidth() != getWidth() || vImage.getHeight() != getHeight()) { - createVolatileImage(); - } - - // The core loop for drawing with a VolatileImage - do { - // First, validate the image. It returns a code indicating the image's state. - int validationCode = vImage.validate(getGraphicsConfiguration()); - - // The image was lost and needs to be restored. - if (validationCode == VolatileImage.IMAGE_RESTORED) { - // The contents are gone, but the image object is still good. - // We just need to re-render our content to it on this loop iteration. - } - // The image has become incompatible (e.g., screen mode change). - // We need to scrap it and create a new one. - else if (validationCode == VolatileImage.IMAGE_INCOMPATIBLE) { - createVolatileImage(); - } - - // --- Main Rendering Step --- - // 1. Get the BufferedImage from our OpenGL context. - BufferedImage frameFromGL = glContextManager.getCurrentFrame(); - - if (frameFromGL != null) { - // 2. Get the graphics context of our hardware-accelerated VolatileImage. - Graphics2D g2d = vImage.createGraphics(); - - // 3. Copy the CPU image to the GPU image. This is the only slow part, - // but it's much faster than drawing the BufferedImage directly to the screen. - g2d.drawImage(frameFromGL, 0, 0, null); - g2d.dispose(); - } - - // --- Final Presentation Step --- - // 4. Draw the VolatileImage to the screen. This is a very fast hardware blit. - g.drawImage(vImage, 0, 0, this); - - // Loop if the image was lost and we had to re-render it. - // This ensures we successfully draw a complete frame. - } while (vImage.contentsLost()); - - // Fallback text drawing (can be drawn after the main image) - if (getModel() == null) { - g.setColor(new Color(255, 255, 0, 200)); - g.drawString("模型未加载", 10, 20); - } - } - - public void modelsUpdate(Model2D model){ - for (int i = 0; i < model.getParts().size(); i++) { - model.getParts().get(i).setPosition(model.getParts().get(i).getPosition().x, model.getParts().get(i).getPosition().y); - } - } - - /** - * 获取当前渲染的模型 - */ - public Model2D getModel() { - if (modelRef.get() == null) { - try { - return glContextManager.waitForModel().get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException("无法获取模型引用: " + e.getMessage(), e); - } - } - return modelRef.get(); - } - - /** - * 异步加载新的模型并更新所有组件状态。 - * * 这个方法解决了新模型加载后,各种工具(如 SelectionTool)仍然指向旧模型或未初始化的状态的问题。 - * 它确保在模型加载完成后,清除旧的选中状态,并将工具切换回默认状态。 - * * @param modelPath 新的模型文件路径。 - * @return 包含加载完成的模型对象的 CompletableFuture。 - */ - public CompletableFuture loadModel(String modelPath) { - CompletableFuture loadFuture = glContextManager.loadModel(modelPath); - return loadFuture.whenComplete((model, ex) -> { - SwingUtilities.invokeLater(() -> { - if (ex == null && model != null) { - this.modelRef.set(model); - resetPostLoadState(model); - modelsUpdate(model); - logger.info("ModelRenderPanel 模型更新完成,工具状态已重置。"); - } else { - this.modelRef.set(null); - resetPostLoadState(null); - logger.error("模型加载失败,ModelRenderPanel 状态已重置。"); - } - }); - }); - } - - /** - * 加载模型 - */ - public void loadModel(Model2D model) { - glContextManager.loadModel(model); - this.modelRef.set(model); - resetPostLoadState(model); - modelsUpdate(model); - logger.info("ModelRenderPanel 模型更新完成,工具状态已重置。"); - } - - /** - * 重置加载新模型后需要清理或初始化的状态。 - */ - private void resetPostLoadState(Model2D model) { - Tool defaultTool = toolManagement.getDefaultTool(); - if (defaultTool instanceof SelectionTool) { - ((SelectionTool) defaultTool).clearSelectedMeshes(); - } - toolManagement.switchToDefaultTool(); - if (model != null) { - resetCamera(); - } - } - - /** - * 重新设置面板大小 - *

- * 说明:当 Swing 面板被放大时,需要同时调整离屏 GLFW 窗口像素大小、GL 视口以及重分配像素读取缓冲, - * 否则将把较小分辨率的图像拉伸到更大面板上导致模糊。 - */ - public void resize(int newWidth, int newHeight) { - // 更新 Swing 尺寸 - setPreferredSize(new Dimension(newWidth, newHeight)); - revalidate(); - glContextManager.resize(newWidth, newHeight); - } - - /** - * 获取全局操作历史管理器 - */ - public StatusRecordManagement getStatusRecordManagement() { - return statusRecordManagement; - } - - /** - * 获取 GL 上下文管理器 - */ - public GLContextManager getGlContextManager() { - return glContextManager; - } - - /** - * 获取鼠标管理器 - */ - public MouseManagement getMouseManagement() { - return mouseManagement; - } - - /** - * 获取相机管理器 - */ - public CameraManagement getCameraManagement() { - return cameraManagement; - } - - /** - * 获取键盘管理器 - */ - public KeyboardManager getKeyboardManager() { - return keyboardManager; - } - - // ================== 保留的辅助方法 ================== - - /** - * 通过网格查找对应的 ModelPart - */ - public ModelPart findPartByMesh(Mesh2D mesh) { - Model2D model = getModel(); - if (model == null) return null; - for (ModelPart part : model.getParts()) { - ModelPart found = findPartByMeshRecursive(part, mesh); - if (found != null) { - return found; - } - } - return null; - } - - /** - * 递归查找包含指定网格的部件 - */ - private ModelPart findPartByMeshRecursive(ModelPart part, Mesh2D targetMesh) { - if (part == null || targetMesh == null) return null; - - // 检查当前部件的网格 - for (Mesh2D mesh : part.getMeshes()) { - if (mesh == targetMesh) { - return part; - } - } - - // 递归检查子部件 - for (ModelPart child : part.getChildren()) { - ModelPart found = findPartByMeshRecursive(child, targetMesh); - if (found != null) { - return found; - } - } - - return null; - } - - /** - * 获取参数管理器 - */ - public ParametersManagement getParametersManagement() { - return parametersManagement.get(); - } - - /** - * 设置参数管理器 - */ - public void setParametersManagement(ParametersManagement parametersManagement) { - this.parametersManagement.set(parametersManagement); - glContextManager.executeInGLContext(() -> ModelRenderPanel.this.parametersManagement.set(parametersManagement)); - } - - public enum DragMode { - NONE, // 无拖拽 - MOVE, // 移动部件 - RESIZE_LEFT, // 调整左边 - RESIZE_RIGHT, // 调整右边 - RESIZE_TOP, // 调整上边 - RESIZE_BOTTOM, // 调整下边 - RESIZE_TOP_LEFT, // 调整左上角 - RESIZE_TOP_RIGHT, // 调整右上角 - RESIZE_BOTTOM_LEFT, // 调整左下角 - RESIZE_BOTTOM_RIGHT, // 调整右下角 - ROTATE, // 新增:旋转 - MOVE_PIVOT, // 新增:移动中心点 - MOVE_PRIMARY_VERTEX, // 新增:移动二级顶点 - MOVE_PUPPET_PIN // 新增:移动 puppetPin - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ParametersPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ParametersPanel.java deleted file mode 100644 index e2a9db9..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ParametersPanel.java +++ /dev/null @@ -1,610 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement; -import com.chuangzhou.vivid2D.render.awt.tools.VertexDeformationTool; -import com.chuangzhou.vivid2D.render.model.AnimationParameter; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; - -import javax.swing.*; -import javax.swing.Timer; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import java.awt.*; -import java.awt.event.*; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.*; -import java.util.List; - - -/** - * 窗口参数管理面板(使用你给定的结构) - * 功能: - * - 当没有选中网格时显示“未选择网格”的占位 - * - 当选中网格时,找到对应的 ModelPart,列出其所有 AnimationParameter - * - [!] 使用 KeyframeSlider 替换 JSlider,以显示/吸附关键帧 - * - [!] 双击参数列表项可打开 KeyframeEditorDialog - * - 支持新增、删除、重命名、修改参数值 - * - 广播参数相关事件 - * - 按下 ESC 取消选择并广播取消事件 - */ -public class ParametersPanel extends JPanel { - private final ModelRenderPanel renderPanel; - private final Model2D model; - private AnimationParameter selectParameter; - public ParametersManagement parametersManagement; - // UI - private final CardLayout cardLayout = new CardLayout(); - private final JPanel cardRoot = new JPanel(cardLayout); - - private final DefaultListModel listModel = new DefaultListModel<>(); - private final JList parameterList = new JList<>(listModel); - - private final JButton addBtn = new JButton("新建"); - private final JButton delBtn = new JButton("删除"); - private final JButton renameBtn = new JButton("重命名"); - - // --- 修改:使用 KeyframeSlider 替换 JSlider --- - private final KeyframeSlider valueSlider = new KeyframeSlider(); - // ------------------------------------------ - - private final JLabel valueLabel = new JLabel("值: -"); - private final Timer pollTimer; - - // 当前绑定的 ModelPart(对应选中网格) - private volatile ModelPart currentPart = null; - - public ParametersPanel(ModelRenderPanel renderPanel) { - this.renderPanel = renderPanel; - this.model = renderPanel.getModel(); - - setLayout(new BorderLayout()); - setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6)); - - // emptyPanel - JPanel emptyPanel = new JPanel(new BorderLayout()); - emptyPanel.add(new JLabel("未选择网格", SwingConstants.CENTER), BorderLayout.CENTER); - - // paramPanel 构建 - parameterList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - parameterList.setCellRenderer((list, value, index, isSelected, cellHasFocus) -> { - JLabel l = new JLabel(value == null ? "" : value.getId()); - l.setOpaque(true); - l.setBackground(isSelected ? UIManager.getColor("List.selectionBackground") : UIManager.getColor("List.background")); - l.setForeground(isSelected ? UIManager.getColor("List.selectionForeground") : UIManager.getColor("List.foreground")); - return l; - }); - - JScrollPane scroll = new JScrollPane(parameterList); - scroll.setPreferredSize(new Dimension(260, 160)); - - JPanel topBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 6)); - topBar.add(addBtn); - topBar.add(delBtn); - topBar.add(renameBtn); - - JPanel bottomBar = new JPanel(new BorderLayout(6, 6)); - JPanel sliderPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 6)); - sliderPanel.add(new JLabel("参数值:")); - valueSlider.setPreferredSize(new Dimension(150, 25)); // 给自定义滑块一个合适的大小 - sliderPanel.add(valueSlider); - sliderPanel.add(valueLabel); - bottomBar.add(sliderPanel, BorderLayout.CENTER); - - JPanel paramPanel = new JPanel(new BorderLayout(6, 6)); - paramPanel.add(topBar, BorderLayout.NORTH); - paramPanel.add(scroll, BorderLayout.CENTER); - paramPanel.add(bottomBar, BorderLayout.SOUTH); - - cardRoot.add(emptyPanel, "EMPTY"); - cardRoot.add(paramPanel, "PARAM"); - add(cardRoot, BorderLayout.CENTER); - - // 按键 ESC 绑定:取消选择 - setupEscBinding(); - - // 事件绑定 - bindActions(); - - // 定期轮询选中网格(200ms) - pollTimer = new Timer(200, e -> pollSelectedMesh()); - pollTimer.start(); - - // 初次展示 - updateCard(); - } - - private void setupEscBinding() { - InputMap im = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); - ActionMap am = getActionMap(); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancelSelection"); - am.put("cancelSelection", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - clearSelection(); - ParameterEventBroadcaster.getInstance().fireCancelEvent(); - } - }); - } - - private void bindActions() { - addBtn.addActionListener(e -> onAddParameter()); - delBtn.addActionListener(e -> onDeleteParameter()); - renameBtn.addActionListener(e -> onRenameParameter()); - - parameterList.addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - if (!e.getValueIsAdjusting()) { - selectParameter = parameterList.getSelectedValue(); - updateSliderForSelected(); - ParameterEventBroadcaster.getInstance().fireSelectEvent(selectParameter); - } - } - }); - - // --- 新增:双击打开关键帧编辑器 --- - parameterList.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - AnimationParameter p = parameterList.getSelectedValue(); - if (p != null) { - // 弹出编辑器 - KeyframeEditorDialog.showEditor(SwingUtilities.getWindowAncestor(ParametersPanel.this), p, parametersManagement, currentPart); - // 编辑器关闭后,刷新滑块的显示 - valueSlider.repaint(); - } - } - } - }); - // -------------------------------- - valueSlider.addChangeListener(e -> { - if (selectParameter == null) return; - valueLabel.setText(String.format("%.3f", selectParameter.getValue())); - ParameterEventBroadcaster.getInstance().fireUpdateEvent(selectParameter); - markModelNeedsUpdate(); - }); - } - - private void pollSelectedMesh() { - Mesh2D mesh = getSelectedMesh(); - if (mesh == null) { - if (currentPart != null) { - currentPart = null; - listModel.clear(); - selectParameter = null; - updateCard(); - } - return; - } - ModelPart part = renderPanel.findPartByMesh(mesh); - if (part == null) { - if (currentPart != null) { - currentPart = null; - listModel.clear(); - selectParameter = null; - updateCard(); - } - return; - } - if (currentPart == part) return; // 未变更 - // 切换到新部件 - currentPart = part; - loadParametersFromCurrentPart(); - updateCard(); - } - - private void updateCard() { - if (currentPart == null) { - cardLayout.show(cardRoot, "EMPTY"); - addBtn.setEnabled(false); - delBtn.setEnabled(false); - renameBtn.setEnabled(false); - valueSlider.setEnabled(false); - } else { - cardLayout.show(cardRoot, "PARAM"); - addBtn.setEnabled(true); - delBtn.setEnabled(!listModel.isEmpty()); - renameBtn.setEnabled(!listModel.isEmpty()); - valueSlider.setEnabled(!listModel.isEmpty() && selectParameter != null); - } - } - - private void loadParametersFromCurrentPart() { - listModel.clear(); - selectParameter = null; - if (currentPart == null) return; - try { - Map map = currentPart.getParameters(); - if (map != null) { - for (AnimationParameter p : map.values()) { - listModel.addElement(p); - } - } - } catch (Exception ex) { - ex.printStackTrace(); - } - if (!listModel.isEmpty()) { - parameterList.setSelectedIndex(0); - } - } - - private void onAddParameter() { - if (currentPart == null) return; - JPanel panel = new JPanel(new GridLayout(4, 2, 4, 4)); - JTextField idField = new JTextField(); - JTextField minField = new JTextField("0.0"); - JTextField maxField = new JTextField("1.0"); - JTextField defField = new JTextField("0.0"); - panel.add(new JLabel("参数ID:")); - panel.add(idField); - panel.add(new JLabel("最小值:")); - panel.add(minField); - panel.add(new JLabel("最大值:")); - panel.add(maxField); - panel.add(new JLabel("默认值:")); - panel.add(defField); - - int res = JOptionPane.showConfirmDialog(this, panel, "新建参数", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); - if (res != JOptionPane.OK_OPTION) return; - - String id = idField.getText().trim(); - if (id.isEmpty()) { - JOptionPane.showMessageDialog(this, "参数ID不能为空", "错误", JOptionPane.ERROR_MESSAGE); - return; - } - try { - float min = Float.parseFloat(minField.getText().trim()); - float max = Float.parseFloat(maxField.getText().trim()); - float def = Float.parseFloat(defField.getText().trim()); - - // 使用 ModelPart.createParameter 如果可用 - try { - AnimationParameter newP = currentPart.createParameter(id, min, max, def); - // 如果 createParameter 返回了对象,直接使用;否则通过 getParameter 获取 - if (newP == null) newP = currentPart.getParameter(id); - - // 插入 UI 列表 - if (newP != null) { - listModel.addElement(newP); - parameterList.setSelectedValue(newP, true); - ParameterEventBroadcaster.getInstance().fireAddEvent(newP); - markModelNeedsUpdate(); - } else { - JOptionPane.showMessageDialog(this, "新参数创建失败", "错误", JOptionPane.ERROR_MESSAGE); - } - } catch (NoSuchMethodError | NoClassDefFoundError ignore) { - // 兜底:通过反射直接修改 internal map(风险自负) - try { - Method m = currentPart.getClass().getMethod("createParameter", String.class, float.class, float.class, float.class); - AnimationParameter newP = (AnimationParameter) m.invoke(currentPart, id, min, max, def); - if (newP != null) { - listModel.addElement(newP); - parameterList.setSelectedValue(newP, true); - ParameterEventBroadcaster.getInstance().fireAddEvent(newP); - markModelNeedsUpdate(); - } - } catch (Exception ex) { - ex.printStackTrace(); - JOptionPane.showMessageDialog(this, "创建参数失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - } - } - } catch (NumberFormatException nfe) { - JOptionPane.showMessageDialog(this, "数值格式错误", "错误", JOptionPane.ERROR_MESSAGE); - } - updateCard(); - } - - private void onDeleteParameter() { - if (currentPart == null) return; - AnimationParameter sel = parameterList.getSelectedValue(); - if (sel == null) return; - int r = JOptionPane.showConfirmDialog(this, "确认删除参数: " + sel.getId() + " ?", "确认删除", JOptionPane.YES_NO_OPTION); - if (r != JOptionPane.YES_OPTION) return; - - try { - Map map = currentPart.getParameters(); - if (map != null) { - map.remove(sel.getId()); - } else { - // 反射尝试 - Field f = currentPart.getClass().getDeclaredField("parameters"); - f.setAccessible(true); - Object o = f.get(currentPart); - if (o instanceof Map) { - ((Map) o).remove(sel.getId()); - } - } - renderPanel.getParametersManagement().removeParameter(currentPart, sel.getId()); - listModel.removeElement(sel); - selectParameter = null; - ParameterEventBroadcaster.getInstance().fireRemoveEvent(sel); - markModelNeedsUpdate(); - } catch (Exception ex) { - ex.printStackTrace(); - JOptionPane.showMessageDialog(this, "删除失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - } - updateCard(); - } - - private void onRenameParameter() { - if (currentPart == null) return; - AnimationParameter sel = parameterList.getSelectedValue(); - if (sel == null) return; - String newId = JOptionPane.showInputDialog(this, "输入新 ID:", sel.getId()); - if (newId == null || newId.trim().isEmpty()) return; - newId = newId.trim(); - try { - Map map = currentPart.getParameters(); - if (map != null) { - // 创建新 entry,移除旧 entry,保留值范围和值 - AnimationParameter old = map.remove(sel.getId()); - if (old != null) { - AnimationParameter copy = new AnimationParameter(newId, old.getMinValue(), old.getMaxValue(), old.getValue()); - // 复制关键帧 - old.getKeyframes().forEach(copy::addKeyframe); - map.put(newId, copy); - // 刷新 UI - loadParametersFromCurrentPart(); - ParameterEventBroadcaster.getInstance().fireRenameEvent(old, copy); - markModelNeedsUpdate(); - } - } else { - // 反射处理 - Field f = currentPart.getClass().getDeclaredField("parameters"); - f.setAccessible(true); - Object o = f.get(currentPart); - if (o instanceof Map) { - Map pm = (Map) o; - AnimationParameter old = pm.remove(sel.getId()); - if (old != null) { - AnimationParameter copy = new AnimationParameter(newId, old.getMinValue(), old.getMaxValue(), old.getValue()); - // 复制关键帧 - old.getKeyframes().forEach(copy::addKeyframe); - pm.put(newId, copy); - loadParametersFromCurrentPart(); - ParameterEventBroadcaster.getInstance().fireRenameEvent(old, copy); - markModelNeedsUpdate(); - } - } - } - } catch (Exception ex) { - ex.printStackTrace(); - JOptionPane.showMessageDialog(this, "重命名失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - } - updateCard(); - } - - - /** - * 修改:更新滑块以显示新选中的参数。 - */ - private void updateSliderForSelected() { - AnimationParameter p = selectParameter; - if (p == null) { - valueSlider.setEnabled(false); - valueSlider.setParameter(null); // 清空滑块的参数 - valueLabel.setText("值: -"); - } else { - valueSlider.setEnabled(true); - valueSlider.setParameter(p); // 将参数设置给自定义滑块 - valueLabel.setText(String.format("%.3f", p.getValue())); - } - valueSlider.repaint(); - } - - private void setParameterValue(AnimationParameter param, float value) { - if (param == null) return; - // 先尝试 param.setValue - try { - Method m = param.getClass().getMethod("setValue", float.class); - m.invoke(param, value); - return; - } catch (Exception ignored) { - } - // 兜底:反射写字段 - try { - Field f = param.getClass().getDeclaredField("value"); - f.setAccessible(true); - f.setFloat(param, value); - } catch (Exception ignored) { - } - - // 如果 ModelPart 有 setParameterValue 方法,调用之以标记 dirty - if (currentPart != null) { - try { - Method m2 = currentPart.getClass().getMethod("setParameterValue", String.class, float.class); - m2.invoke(currentPart, param.getId(), value); - } catch (Exception ignored) { - } - } - } - - private void clearSelection() { - parameterList.clearSelection(); - selectParameter = null; - updateSliderForSelected(); - } - - /** - * 外部可调用,获取当前选中的网格(基于 renderPanel) - */ - private Mesh2D getSelectedMesh() { - if (renderPanel.getSelectedMesh() == null - && renderPanel.getToolManagement().getCurrentTool() instanceof VertexDeformationTool){ - return ((VertexDeformationTool) renderPanel.getToolManagement().getCurrentTool()).getTargetMesh(); - } - return renderPanel.getSelectedMesh(); - } - - public ModelRenderPanel getRenderPanel() { - return renderPanel; - } - - public AnimationParameter getSelectParameter() { - return selectParameter; - } - - private void markModelNeedsUpdate() { - try { - if (model == null) return; - Method m = model.getClass().getMethod("markNeedsUpdate"); - m.invoke(model); - } catch (Exception ignored) { - } - } - - public void dispose() { - if (pollTimer != null) pollTimer.stop(); - } - - /** - * 获取当前选中参数上被“选中”的关键帧。 - * “选中”定义为:滑块的当前值正好(或非常接近)一个关键帧的值。 - * - * @param isPreciseCheck 如果为 true,则只有当 currentValue 几乎精确等于关键帧值时才返回; - * 否则允许在 epsilon 阈值内的吸附。 - * @return 如果当前值命中了关键帧,则返回该帧的值;否则返回 null。 - */ - public Float getSelectedKeyframe(boolean isPreciseCheck) { - if (selectParameter == null) { - return null; - } - - float currentValue = selectParameter.getValue(); - float range = selectParameter.getMaxValue() - selectParameter.getMinValue(); - if (range <= 0) return null; - - // 设置吸附/命中阈值,例如范围的 0.5% - // 注意:这个阈值应该和 KeyframeSlider 中的吸附逻辑保持一致 - float epsilon = range * 0.005f; - // 用于判断浮点数是否"相等"的极小值 - final float EQUALITY_TOLERANCE = 1e-5f; - - // 1. 检查是否有精确匹配的关键帧 - Float nearest = selectParameter.getNearestKeyframe(currentValue, epsilon); - if (nearest != null) { - // 检查是否在吸附阈值内 (旧逻辑) - if (Math.abs(currentValue - nearest) <= epsilon) { - - // ------------------------------------------------------------- - // 2. 新增逻辑:精确检查判断 - if (isPreciseCheck) { - // 如果要求精确检查,则只有当它们几乎相等时才返回 - if (Math.abs(currentValue - nearest) <= EQUALITY_TOLERANCE) { - return nearest; - } else { - // 如果不相等,则不认为是“选中”的关键帧 - return -114514f; - } - } - // ------------------------------------------------------------- - - // 3. 原有吸附逻辑 (仅在非精确检查时执行,或精确检查通过时执行) - // 如果差值大于 EQUALITY_TOLERANCE,说明发生了吸附,需要更新参数值 - if (Math.abs(currentValue - nearest) > EQUALITY_TOLERANCE) { - setParameterValue(selectParameter, nearest); - valueLabel.setText(String.format("%.3f", nearest)); - ParameterEventBroadcaster.getInstance().fireUpdateEvent(selectParameter); - } - - // 返回吸附后的值 (或精确匹配的值) - return nearest; - } - } - return -114514f; - } - - - // =================== 简单事件广播器与监听器 (未修改) =================== - - public interface ParameterEventListener { - default void onParameterAdded(AnimationParameter p) {} - default void onParameterRemoved(AnimationParameter p) {} - default void onParameterUpdated(AnimationParameter p) {} - default void onParameterRenamed(AnimationParameter oldP, AnimationParameter newP) {} - default void onParameterSelected(AnimationParameter p) {} - default void onCancelSelection() {} - } - - public static class ParameterEventBroadcaster { - private static final ParameterEventBroadcaster INSTANCE = new ParameterEventBroadcaster(); - private final List listeners = Collections.synchronizedList(new ArrayList<>()); - - public static ParameterEventBroadcaster getInstance() { - return INSTANCE; - } - - public void addListener(ParameterEventListener l) { - if (l == null) return; - listeners.add(l); - } - - public void removeListener(ParameterEventListener l) { - listeners.remove(l); - } - - public void fireAddEvent(AnimationParameter p) { - SwingUtilities.invokeLater(() -> { - synchronized (listeners) { - for (ParameterEventListener l : new ArrayList<>(listeners)) { - try { l.onParameterAdded(p); } catch (Exception ignored) {} - } - } - }); - } - - public void fireRemoveEvent(AnimationParameter p) { - SwingUtilities.invokeLater(() -> { - synchronized (listeners) { - for (ParameterEventListener l : new ArrayList<>(listeners)) { - try { l.onParameterRemoved(p); } catch (Exception ignored) {} - } - } - }); - } - - public void fireUpdateEvent(AnimationParameter p) { - SwingUtilities.invokeLater(() -> { - synchronized (listeners) { - for (ParameterEventListener l : new ArrayList<>(listeners)) { - try { l.onParameterUpdated(p); } catch (Exception ignored) {} - } - } - }); - } - - public void fireRenameEvent(AnimationParameter oldP, AnimationParameter newP) { - SwingUtilities.invokeLater(() -> { - synchronized (listeners) { - for (ParameterEventListener l : new ArrayList<>(listeners)) { - try { l.onParameterRenamed(oldP, newP); } catch (Exception ignored) {} - } - } - }); - } - - public void fireSelectEvent(AnimationParameter p) { - SwingUtilities.invokeLater(() -> { - synchronized (listeners) { - for (ParameterEventListener l : new ArrayList<>(listeners)) { - try { l.onParameterSelected(p); } catch (Exception ignored) {} - } - } - }); - } - - public void fireCancelEvent() { - SwingUtilities.invokeLater(() -> { - synchronized (listeners) { - for (ParameterEventListener l : new ArrayList<>(listeners)) { - try { l.onCancelSelection(); } catch (Exception ignored) {} - } - } - }); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/TransformPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/TransformPanel.java deleted file mode 100644 index 81479f8..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/TransformPanel.java +++ /dev/null @@ -1,761 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal; -import com.chuangzhou.vivid2D.render.model.ModelEvent; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import org.joml.Vector2f; - -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import java.awt.*; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - -/** - * @author tzdwindows 7 - */ -public class TransformPanel extends JPanel implements ModelEvent { - private final ModelRenderPanel renderPanel; - private final List selectedParts = new ArrayList<>(); - - // 位置控制 - private JTextField positionXField; - private JTextField positionYField; - - // 旋转控制 - private JTextField rotationField; - - // 缩放控制 - private JTextField scaleXField; - private JTextField scaleYField; - - // 中心点控制 - private JTextField pivotXField; - private JTextField pivotYField; - - // 按钮 - private JButton flipXButton; - private JButton flipYButton; - private JButton rotate90CWButton; - private JButton rotate90CCWButton; - private JButton resetScaleButton; - - private boolean updatingUI = false; // 防止UI更新时触发事件 - private javax.swing.Timer transformTimer; // 用于延迟处理变换输入 - - // 【新增字段】用于多选时的位移计算(记录多选部件的初始平均位置或第一个部件的位置) - private Vector2f initialPosition = new Vector2f(); - - private final OperationHistoryGlobal operationHistory; - - public TransformPanel(ModelRenderPanel renderPanel) { - this.renderPanel = renderPanel; - this.operationHistory = OperationHistoryGlobal.getInstance(); - initComponents(); - setupListeners(); - updateUIState(); - } - - private void initComponents() { - setLayout(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(3, 5, 3, 5); - gbc.fill = GridBagConstraints.HORIZONTAL; - - int row = 0; - - // 位置控制 - gbc.gridx = 0; - gbc.gridy = row; - add(new JLabel("位置 X:"), gbc); - - gbc.gridx = 1; - gbc.gridy = row; - positionXField = new JTextField("0.00"); - add(positionXField, gbc); - - gbc.gridx = 2; - gbc.gridy = row; - add(new JLabel("Y:"), gbc); - - gbc.gridx = 3; - gbc.gridy = row++; - positionYField = new JTextField("0.00"); - add(positionYField, gbc); - - // 分隔线 - gbc.gridx = 0; - gbc.gridy = row++; - gbc.gridwidth = 4; - add(new JSeparator(SwingConstants.HORIZONTAL), gbc); - - - // 旋转控制 - gbc.gridx = 0; - gbc.gridy = row; - gbc.gridwidth = 1; - add(new JLabel("旋转角度:"), gbc); - - gbc.gridx = 1; - gbc.gridy = row; - rotationField = new JTextField("0.00"); - add(rotationField, gbc); - - gbc.gridx = 2; - gbc.gridy = row; - gbc.gridwidth = 2; - rotate90CWButton = new JButton("+90°"); - rotate90CWButton.setToolTipText("顺时针旋转90度"); - add(rotate90CWButton, gbc); - - gbc.gridx = 0; - gbc.gridy = ++row; - gbc.gridwidth = 4; - rotate90CCWButton = new JButton("-90°"); - rotate90CCWButton.setToolTipText("逆时针旋转90度"); - add(rotate90CCWButton, gbc); - - // 分隔线 - gbc.gridx = 0; - gbc.gridy = ++row; - gbc.gridwidth = 4; - add(new JSeparator(SwingConstants.HORIZONTAL), gbc); - - // 缩放控制 - gbc.gridx = 0; - gbc.gridy = ++row; - gbc.gridwidth = 1; - add(new JLabel("缩放 X:"), gbc); - - gbc.gridx = 1; - gbc.gridy = row; - scaleXField = new JTextField("1.00"); - add(scaleXField, gbc); - - gbc.gridx = 2; - gbc.gridy = row; - add(new JLabel("Y:"), gbc); - - gbc.gridx = 3; - gbc.gridy = row; - scaleYField = new JTextField("1.00"); - add(scaleYField, gbc); - - gbc.gridx = 0; - gbc.gridy = ++row; - gbc.gridwidth = 2; - flipXButton = new JButton("水平翻转"); - add(flipXButton, gbc); - - gbc.gridx = 2; - gbc.gridy = row; - gbc.gridwidth = 2; - flipYButton = new JButton("垂直翻转"); - add(flipYButton, gbc); - - gbc.gridx = 0; - gbc.gridy = ++row; - gbc.gridwidth = 4; - resetScaleButton = new JButton("重置缩放"); - resetScaleButton.setToolTipText("重置为1:1缩放"); - add(resetScaleButton, gbc); - - // 分隔线 - gbc.gridx = 0; - gbc.gridy = ++row; - gbc.gridwidth = 4; - add(new JSeparator(SwingConstants.HORIZONTAL), gbc); - - // 中心点控制 - gbc.gridx = 0; - gbc.gridy = ++row; - gbc.gridwidth = 1; - add(new JLabel("中心点 X:"), gbc); - - gbc.gridx = 1; - gbc.gridy = row; - pivotXField = new JTextField("0.00"); - add(pivotXField, gbc); - - gbc.gridx = 2; - gbc.gridy = row; - add(new JLabel("Y:"), gbc); - - gbc.gridx = 3; - gbc.gridy = row; - pivotYField = new JTextField("0.00"); - add(pivotYField, gbc); - - // 占位符,确保组件靠上 - gbc.gridx = 0; - gbc.gridy = ++row; - gbc.gridwidth = 4; - gbc.weighty = 1.0; - add(new JPanel(), gbc); - - // 初始化定时器,用于延迟处理变换输入 - transformTimer = new javax.swing.Timer(300, e -> applyTransformChanges()); - transformTimer.setRepeats(false); // 只执行一次 - } - - private void setupListeners() { - // 为所有文本框添加文档监听器 - DocumentListener documentListener = new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { - scheduleTransformUpdate(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - scheduleTransformUpdate(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - scheduleTransformUpdate(); - } - }; - - positionXField.getDocument().addDocumentListener(documentListener); - positionYField.getDocument().addDocumentListener(documentListener); - rotationField.getDocument().addDocumentListener(documentListener); - scaleXField.getDocument().addDocumentListener(documentListener); - scaleYField.getDocument().addDocumentListener(documentListener); - pivotXField.getDocument().addDocumentListener(documentListener); - pivotYField.getDocument().addDocumentListener(documentListener); - - // 添加焦点监听,当失去焦点时立即应用 - java.awt.event.FocusAdapter focusAdapter = new java.awt.event.FocusAdapter() { - @Override - public void focusLost(java.awt.event.FocusEvent e) { - transformTimer.stop(); - applyTransformChanges(); - } - }; - - positionXField.addFocusListener(focusAdapter); - positionYField.addFocusListener(focusAdapter); - rotationField.addFocusListener(focusAdapter); - scaleXField.addFocusListener(focusAdapter); - scaleYField.addFocusListener(focusAdapter); - pivotXField.addFocusListener(focusAdapter); - pivotYField.addFocusListener(focusAdapter); - - // 添加回车键监听 - java.awt.event.ActionListener enterListener = e -> { - transformTimer.stop(); - applyTransformChanges(); - }; - - positionXField.addActionListener(enterListener); - positionYField.addActionListener(enterListener); - rotationField.addActionListener(enterListener); - scaleXField.addActionListener(enterListener); - scaleYField.addActionListener(enterListener); - pivotXField.addActionListener(enterListener); - pivotYField.addActionListener(enterListener); - - // 旋转按钮监听器修改(支持多选)- 保持不变 - rotate90CWButton.addActionListener(e -> { - if (!selectedParts.isEmpty()) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - Map oldRotations = new HashMap<>(); - Map newRotations = new HashMap<>(); - - for (ModelPart part : selectedParts) { - float oldRotation = part.getRotation(); - oldRotations.put(part, oldRotation); - - float currentRotation = (float) Math.toDegrees(oldRotation); - float newRotation = normalizeAngle(currentRotation + 90.0f); - part.setRotation((float) Math.toRadians(newRotation)); - - newRotations.put(part, part.getRotation()); - } - - // 记录多选操作历史 - recordMultiPartOperation("ROTATION", - oldRotations.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e2 -> (Object) e2.getValue())), - newRotations.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e2 -> (Object) e2.getValue()))); - - SwingUtilities.invokeLater(this::updateUIFromSelectedParts); - renderPanel.repaint(); - }); - } - }); - - rotate90CCWButton.addActionListener(e -> { - if (!selectedParts.isEmpty()) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - Map oldRotations = new HashMap<>(); - Map newRotations = new HashMap<>(); - - for (ModelPart part : selectedParts) { - float oldRotation = part.getRotation(); - oldRotations.put(part, oldRotation); - - float currentRotation = (float) Math.toDegrees(oldRotation); - float newRotation = normalizeAngle(currentRotation - 90.0f); - part.setRotation((float) Math.toRadians(newRotation)); - - newRotations.put(part, part.getRotation()); - } - - // 记录多选操作历史 - recordMultiPartOperation("ROTATION", - oldRotations.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e2 -> (Object) e2.getValue())), - newRotations.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e2 -> (Object) e2.getValue()))); - - SwingUtilities.invokeLater(this::updateUIFromSelectedParts); - renderPanel.repaint(); - }); - } - }); - - // 翻转按钮监听器修改(支持多选)- 保持不变 - flipXButton.addActionListener(e -> { - if (!selectedParts.isEmpty()) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - Map oldScales = new HashMap<>(); - Map newScales = new HashMap<>(); - - for (ModelPart part : selectedParts) { - Vector2f oldScale = new Vector2f(part.getScale()); - oldScales.put(part, oldScale); - - float currentScaleX = part.getScaleX(); - float currentScaleY = part.getScaleY(); - part.setScale(currentScaleX * -1, currentScaleY); - - newScales.put(part, new Vector2f(part.getScale())); - } - - // 记录多选操作历史 - recordMultiPartOperation("SCALE", - oldScales.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e2 -> (Object) e2.getValue())), - newScales.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e2 -> (Object) e2.getValue()))); - - SwingUtilities.invokeLater(this::updateUIFromSelectedParts); - renderPanel.repaint(); - }); - } - }); - - flipYButton.addActionListener(e -> { - if (!selectedParts.isEmpty()) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - Map oldScales = new HashMap<>(); - Map newScales = new HashMap<>(); - - for (ModelPart part : selectedParts) { - Vector2f oldScale = new Vector2f(part.getScale()); - oldScales.put(part, oldScale); - - float currentScaleX = part.getScaleX(); - float currentScaleY = part.getScaleY(); - part.setScale(currentScaleX, currentScaleY * -1); - - newScales.put(part, new Vector2f(part.getScale())); - } - - // 记录多选操作历史 - recordMultiPartOperation("SCALE", - oldScales.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e2 -> (Object) e2.getValue())), - newScales.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e2 -> (Object) e2.getValue()))); - - SwingUtilities.invokeLater(this::updateUIFromSelectedParts); - renderPanel.repaint(); - }); - } - }); - - // 重置缩放按钮监听器修改(支持多选)- 保持不变 - resetScaleButton.addActionListener(e -> { - if (!selectedParts.isEmpty()) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - Map oldScales = new HashMap<>(); - Map newScales = new HashMap<>(); - - for (ModelPart part : selectedParts) { - Vector2f oldScale = new Vector2f(part.getScale()); - oldScales.put(part, oldScale); - - part.setScale(1.0f, 1.0f); - - newScales.put(part, new Vector2f(part.getScale())); - } - - // 记录多选操作历史 - recordMultiPartOperation("SCALE", - oldScales.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e2 -> (Object) e2.getValue())), - newScales.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e2 -> (Object) e2.getValue()))); - - SwingUtilities.invokeLater(this::updateUIFromSelectedParts); - renderPanel.repaint(); - }); - } - }); - } - - /** - * 记录多部件操作历史 - * 【修复】简化操作历史记录,不再需要复杂的 Object[] 数组,直接记录 Map - */ - private void recordMultiPartOperation(String operationType, Map oldValues, Map newValues) { - if (operationHistory != null && !selectedParts.isEmpty()) { - List params = new ArrayList<>(); - params.add(new ArrayList<>(selectedParts)); - params.add(oldValues); - params.add(newValues); - operationHistory.recordOperation("MULTI_" + operationType, params.toArray()); - } - } - - /** - * 批量应用变换到所有选中部件 - * 【修复】拆分逻辑,这里只处理绝对值(旋转、缩放、中心点),位移在 applyTransformChanges 中处理 - */ - private void applyAbsoluteTransformToAllParts(float rotationDegrees, - float scaleX, float scaleY, float pivotX, float pivotY) { - // 记录变换前的状态 - Map oldStates = new HashMap<>(); - Map newStates = new HashMap<>(); - - for (ModelPart part : selectedParts) { - // 记录旧状态 (只记录绝对变换) - Object[] oldState = new Object[]{ - part.getRotation(), - new Vector2f(part.getScale()), - new Vector2f(part.getPivot()) - }; - oldStates.put(part, oldState); - - // 应用绝对变换 - part.setRotation((float) Math.toRadians(rotationDegrees)); - part.setScale(scaleX, scaleY); - part.setPivot(pivotX, pivotY); - - // 记录新状态 - Object[] newState = new Object[]{ - part.getRotation(), - new Vector2f(part.getScale()), - new Vector2f(part.getPivot()) - }; - newStates.put(part, newState); - } - - // 记录批量操作历史 - recordMultiPartOperation("BATCH_ABS_TRANSFORM", oldStates, newStates); - } - - /** - * 批量应用相对位移到所有选中部件 - * 模仿 SelectionTool 的多选移动逻辑:计算相对位移并应用 - */ - private void applyRelativePositionToAllParts(float targetPosX, float targetPosY) { - if (selectedParts.isEmpty()) return; - - // 1. 计算相对位移 - float deltaX = targetPosX - initialPosition.x; - float deltaY = targetPosY - initialPosition.y; - - if (deltaX == 0.0f && deltaY == 0.0f) return; - - // 2. 记录旧状态 - Map oldPositions = new HashMap<>(); - Map newPositions = new HashMap<>(); - - for (ModelPart part : selectedParts) { - Vector2f oldPos = new Vector2f(part.getPosition()); - oldPositions.put(part, oldPos); - - // 3. 应用相对位移 - part.setPosition(oldPos.x + deltaX, oldPos.y + deltaY); - - // 4. 记录新状态 - newPositions.put(part, new Vector2f(part.getPosition())); - } - - // 5. 更新初始位置为新的目标位置 - initialPosition.set(targetPosX, targetPosY); - - // 6. 记录操作历史 - recordMultiPartOperation("POSITION", oldPositions, newPositions); - } - - /** - * 事件监听器实现 - 当任何选中部件的属性变化时更新UI - */ - @Override - public void trigger(String eventName, Object source) { - // 【修复】确保即使在多选时,来自 GLContext 的单个部件更新也能触发 UI 刷新 - if (!(source instanceof ModelPart) || !selectedParts.contains(source)) return; - - SwingUtilities.invokeLater(() -> { - updatingUI = true; - try { - if (selectedParts.size() == 1) { - // 单选:显示具体值 - ModelPart part = (ModelPart) source; - updateUIFromSinglePart(part); - } else { - // 多选:更新 UI,但不需要记录历史(防止循环) - updateUIForMultiSelection(); - } - } catch (Exception ex) { - ex.printStackTrace(); - } - updatingUI = false; - }); - } - - /** - * 调度变换更新(延迟处理) - */ - private void scheduleTransformUpdate() { - if (updatingUI || selectedParts.isEmpty()) return; - transformTimer.stop(); - transformTimer.start(); - } - - /** - * 将角度标准化到0-360度范围内 - */ - private float normalizeAngle(float degrees) { - degrees = degrees % 360; - if (degrees < 0) { - degrees += 360; - } - return degrees; - } - - /** - * 应用所有变换更改(支持多选) - * 【修复】拆分位移和绝对变换逻辑 - */ - private void applyTransformChanges() { - if (updatingUI || selectedParts.isEmpty()) return; - - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - float posX = Float.parseFloat(positionXField.getText()); - float posY = Float.parseFloat(positionYField.getText()); - float rotationDegrees = Float.parseFloat(rotationField.getText()); - rotationDegrees = normalizeAngle(rotationDegrees); - float scaleX = Float.parseFloat(scaleXField.getText()); - float scaleY = Float.parseFloat(scaleYField.getText()); - float pivotX = Float.parseFloat(pivotXField.getText()); - float pivotY = Float.parseFloat(pivotYField.getText()); - - // 1. 处理位置/位移 (相对变换) - applyRelativePositionToAllParts(posX, posY); - - // 2. 处理绝对变换 (旋转、缩放、中心点) - applyAbsoluteTransformToAllParts(rotationDegrees, scaleX, scaleY, pivotX, pivotY); - - SwingUtilities.invokeLater(() -> updateUIFromSelectedParts()); // 确保 UI 立即刷新以反映新的初始位置 - renderPanel.repaint(); - } catch (NumberFormatException ex) { - // 输入无效时恢复之前的值 - SwingUtilities.invokeLater(this::updateUIFromSelectedParts); - } - }); - } - - /** - * 从选中的部件更新UI(支持多选) - * 【修复】在多选模式下,显示各属性的平均值,并将该平均值作为新的 initialPosition - */ - private void updateUIFromSelectedParts() { - if (selectedParts.isEmpty()) return; - - updatingUI = true; - try { - if (selectedParts.size() == 1) { - ModelPart part = selectedParts.get(0); - updateUIFromSinglePart(part); - // 记录单选时的初始位置 - initialPosition.set(part.getPosition()); - } else { - updateUIForMultiSelection(); - } - } catch (Exception ex) { - ex.printStackTrace(); - } - updatingUI = false; - } - - /** - * 从单个部件更新UI - 保持不变 - */ - private void updateUIFromSinglePart(ModelPart part) { - // 更新位置 - Vector2f position = part.getPosition(); - positionXField.setText(String.format("%.2f", position.x)); - positionYField.setText(String.format("%.2f", position.y)); - - // 更新旋转 - float currentRotation = (float) Math.toDegrees(part.getRotation()); - currentRotation = normalizeAngle(currentRotation); - rotationField.setText(String.format("%.2f", currentRotation)); - - // 更新缩放 - Vector2f scale = part.getScale(); - scaleXField.setText(String.format("%.2f", scale.x)); - scaleYField.setText(String.format("%.2f", scale.y)); - - // 更新中心点 - Vector2f pivot = part.getPivot(); - pivotXField.setText(String.format("%.2f", pivot.x)); - pivotYField.setText(String.format("%.2f", pivot.y)); - } - - /** - * 多选时的UI显示 - * 【改进】计算并显示平均值作为多选时的参考值 - */ - private void updateUIForMultiSelection() { - // 计算平均值 - float avgX = 0; - float avgY = 0; - float avgRot = 0; - float avgScaleX = 0; - float avgScaleY = 0; - float avgPivotX = 0; - float avgPivotY = 0; - - int count = selectedParts.size(); - for (ModelPart part : selectedParts) { - avgX += part.getPosition().x; - avgY += part.getPosition().y; - avgRot += normalizeAngle((float) Math.toDegrees(part.getRotation())); - avgScaleX += part.getScale().x; - avgScaleY += part.getScale().y; - avgPivotX += part.getPivot().x; - avgPivotY += part.getPivot().y; - } - - avgX /= count; - avgY /= count; - avgRot /= count; - avgScaleX /= count; - avgScaleY /= count; - avgPivotX /= count; - avgPivotY /= count; - - // 设置平均值到字段,并更新初始位置 - initialPosition.set(avgX, avgY); - positionXField.setText(String.format("%.2f", avgX)); - positionYField.setText(String.format("%.2f", avgY)); - rotationField.setText(String.format("%.2f", avgRot)); - scaleXField.setText(String.format("%.2f", avgScaleX)); - scaleYField.setText(String.format("%.2f", avgScaleY)); - pivotXField.setText(String.format("%.2f", avgPivotX)); - pivotYField.setText(String.format("%.2f", avgPivotY)); - - // 【可选改进】如果某个属性在多选部件间不一致,可以显示特殊标记,例如: - // if (!isUniformRotation()) rotationField.setText("[混合]"); - } - - // 【新增辅助方法】检查多选部件的旋转值是否一致 - private boolean isUniformRotation() { - if (selectedParts.isEmpty()) return true; - float firstRotation = selectedParts.get(0).getRotation(); - for (ModelPart part : selectedParts) { - if (Math.abs(part.getRotation() - firstRotation) > 0.0001f) { - return false; - } - } - return true; - } - - /** - * 设置选中的部件(支持多选) - * 【修复】添加了对 initialPosition 的初始化 - */ - public void setSelectedParts(List parts) { - // 移除旧部件的事件监听 - for (ModelPart oldPart : selectedParts) { - oldPart.removeEvent(this); - } - - this.selectedParts.clear(); - if (parts != null) { - this.selectedParts.addAll(parts); - - // 添加新部件的事件监听 - for (ModelPart newPart : selectedParts) { - newPart.addEvent(this); - } - } - - // 【关键修复】更新 UI 状态后,会设置 initialPosition - updateUIState(); - } - - // ... (addSelectedPart, removeSelectedPart, clearSelectedParts, getSelectedPart, getSelectedParts, getSelectedPartsCount, isMultiSelection 保持不变) - - private void updateUIState() { - updatingUI = true; - if (!selectedParts.isEmpty()) { - updateUIFromSelectedParts(); - setControlsEnabled(true); - } else { - // 清空所有字段 - positionXField.setText("0.00"); - positionYField.setText("0.00"); - rotationField.setText("0.00"); - scaleXField.setText("1.00"); - scaleYField.setText("1.00"); - pivotXField.setText("0.00"); - pivotYField.setText("0.00"); - initialPosition.set(0.0f, 0.0f); // 清空初始位置 - setControlsEnabled(false); - } - updatingUI = false; - } - - private void setControlsEnabled(boolean enabled) { - positionXField.setEnabled(enabled); - positionYField.setEnabled(enabled); - rotationField.setEnabled(enabled); - scaleXField.setEnabled(enabled); - scaleYField.setEnabled(enabled); - pivotXField.setEnabled(enabled); - pivotYField.setEnabled(enabled); - flipXButton.setEnabled(enabled); - flipYButton.setEnabled(enabled); - rotate90CWButton.setEnabled(enabled); - rotate90CCWButton.setEnabled(enabled); - resetScaleButton.setEnabled(enabled); - } - - @Override - public void removeNotify() { - super.removeNotify(); - // 清理定时器资源和事件监听 - if (transformTimer != null) { - transformTimer.stop(); - } - // 移除所有部件的事件监听 - for (ModelPart part : selectedParts) { - part.removeEvent(this); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/CameraManagement.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/CameraManagement.java deleted file mode 100644 index 88b19f2..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/CameraManagement.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.manager; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.systems.Camera; -import org.joml.Vector2f; - -public class CameraManagement { - private final ModelRenderPanel modelRenderPanel; - private final GLContextManager glContextManager; - private final WorldManagement worldManagement; - - private volatile int lastCameraDragX, lastCameraDragY; - private final Vector2f rotationCenter = new Vector2f(); - - public static final float ZOOM_STEP = 1.15f; // 每格滚轮的指数因子(>1 放大) - public static final float ZOOM_MIN = 0.1f; - public static final float ZOOM_MAX = 8.0f; - public static final float ROTATION_HANDLE_DISTANCE = 30.0f; - - public CameraManagement(ModelRenderPanel modelRenderPanel, GLContextManager glContextManager, WorldManagement worldManagement){ - this.modelRenderPanel = modelRenderPanel; - this.glContextManager = glContextManager; - this.worldManagement = worldManagement; - } - - public void resizingApplications(int screenX, int screenY, int notches, boolean fine){ - glContextManager.executeInGLContext(() -> { - Camera camera = ModelRender.getCamera(); - float oldZoom = camera.getZoom(); - float[] worldPosBefore = worldManagement.screenToModelCoordinates(screenX, screenY); - if (worldPosBefore == null) return; - double step = fine ? Math.pow(ZOOM_STEP, 0.25) : ZOOM_STEP; - float newZoom = oldZoom; - if (notches > 0) { // 缩小 - newZoom /= (float) Math.pow(step, notches); - } else { // 放大 - newZoom *= (float) Math.pow(step, -notches); - } - newZoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, newZoom)); - if (Math.abs(newZoom - oldZoom) < 1e-6f) { - return; - } - camera.setZoom(newZoom); - float[] worldPosAfter = worldManagement.screenToModelCoordinates(screenX, screenY); - if (worldPosAfter == null) { - camera.setZoom(oldZoom); - return; - } - float panX = worldPosBefore[0] - worldPosAfter[0]; - float panY = worldPosBefore[1] - worldPosAfter[1]; - camera.move(panX, panY); - glContextManager.setDisplayScale(newZoom); - glContextManager.setTargetScale(newZoom); - }); - } - - /** - * 计算当前缩放因子(模型单位与屏幕像素的比例) - */ - public float calculateScaleFactor() { - int panelWidth = modelRenderPanel.getWidth(); - int panelHeight = modelRenderPanel.getHeight(); - - if (panelWidth <= 0 || panelHeight <= 0 || glContextManager.getHeight() <= 0 || glContextManager.getHeight() <= 0) { - return 1.0f; - } - - // 计算面板与离屏缓冲区的比例 - float scaleX = (float) panelWidth / glContextManager.getWidth(); - float scaleY = (float) panelHeight / glContextManager.getHeight(); - - // 基本面板缩放(保持与现有逻辑一致) - float base = Math.min(scaleX, scaleY); - - // 乘以平滑的 displayScale,使视觉上缩放与检测区域一致 - return base * glContextManager.displayScale; - } - - public int getLastCameraDragX() { - return lastCameraDragX; - } - - public int getLastCameraDragY() { - return lastCameraDragY; - } - - public void setLastCameraDragX(int lastCameraDragX) { - this.lastCameraDragX = lastCameraDragX; - } - - public void setLastCameraDragY(int lastCameraDragY) { - this.lastCameraDragY = lastCameraDragY; - } - - public Vector2f getRotationCenter() { - return rotationCenter; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java deleted file mode 100644 index be07d7d..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java +++ /dev/null @@ -1,526 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.manager; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import org.lwjgl.glfw.GLFW; -import org.lwjgl.opengl.*; -import org.lwjgl.system.MemoryUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.*; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.awt.image.DataBufferInt; -import java.nio.ByteBuffer; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.LockSupport; -import java.util.concurrent.locks.ReentrantLock; - -public class GLContextManager { - private static final Logger logger = LoggerFactory.getLogger(GLContextManager.class); - private long windowId; - private volatile boolean running = true; - private Thread renderThread; - private volatile int width; - private volatile int height; - - private volatile boolean contextInitialized = false; - private final CompletableFuture contextReady = new CompletableFuture<>(); - private String modelPath; - private final AtomicReference modelRef = new AtomicReference<>(); - - // --- FIX: CPU-side double buffering to prevent flickering --- - private volatile BufferedImage frontBuffer; - private BufferedImage backBuffer; - private int[] backBufferPixelArray; // Direct reference to backBuffer's data - private final ReentrantLock bufferSwapLock = new ReentrantLock(); - - // --- Optimization: Asynchronous Pixel Buffer Objects (PBOs) --- - private final int[] pboIds = new int[2]; - private int pboIndex = 0; - private int nextPboIndex = 1; - - public volatile float displayScale = 1.0f; - public volatile float targetScale = 1.0f; - - private final BlockingQueue glTaskQueue = new LinkedBlockingQueue<>(); - private final ExecutorService taskExecutor = Executors.newSingleThreadExecutor(); - - private volatile boolean cameraDragging = false; - private static final float ZOOM_SMOOTHING = 0.18f; - private RepaintCallback repaintCallback; - - private final CompletableFuture modelReady = new CompletableFuture<>(); - - public GLContextManager(String modelPath, int width, int height) { - this.modelPath = modelPath; - this.width = width; - this.height = height; - } - - public GLContextManager(Model2D model, int width, int height) { - this.modelPath = null; - this.width = width; - this.height = height; - this.modelRef.set(model); - if (model != null && !modelReady.isDone()) { - modelReady.complete(model); - } - } - - public int getHeight() { - return height; - } - - public int getWidth() { - return width; - } - - private void createOffscreenContext() throws Exception { - GLFW.glfwDefaultWindowHints(); - GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GL11.GL_TRUE); - GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, 4); - - windowId = GLFW.glfwCreateWindow(width, height, "Offscreen Render", MemoryUtil.NULL, MemoryUtil.NULL); - if (windowId == MemoryUtil.NULL) { - throw new Exception("无法创建离屏 OpenGL 上下文"); - } - - GLFW.glfwMakeContextCurrent(windowId); - GL.createCapabilities(); - logger.info("OpenGL context created successfully"); - - RenderSystem.beginInitialization(); - RenderSystem.initRenderThread(); - RenderSystem.viewport(0, 0, width, height); - - initializeFrameResources(); - - ModelRender.initialize(); - RenderSystem.finishInitialization(); - loadModelInContext(); - - contextInitialized = true; - contextReady.complete(null); - logger.info("Offscreen context initialization completed"); - } - - /** - * Initializes or re-initializes PBOs and the double-buffered ImageBuffers. - */ - private void initializeFrameResources() { - final int w = Math.max(1, this.width); - final int h = Math.max(1, this.height); - final int bufferSize = w * h * 4; // 4 bytes per pixel (RGBA) - - // Create and initialize PBOs - GL15.glGenBuffers(pboIds); - - GL15.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, pboIds[0]); - GL15.glBufferData(GL21.GL_PIXEL_PACK_BUFFER, bufferSize, GL15.GL_STREAM_READ); - - GL15.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, pboIds[1]); - GL15.glBufferData(GL21.GL_PIXEL_PACK_BUFFER, bufferSize, GL15.GL_STREAM_READ); - - GL15.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, 0); - - // Create two buffers for CPU-side double buffering - this.frontBuffer = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - this.backBuffer = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - this.backBufferPixelArray = ((DataBufferInt) this.backBuffer.getRaster().getDataBuffer()).getData(); - } - - public void setRepaintCallback(RepaintCallback callback) { - this.repaintCallback = callback; - } - - private void loadModelInContext() { - try { - if (modelPath != null) { - Model2D model; - try { - model = Model2D.loadFromFile(modelPath); - } catch (Throwable e) { - model = new Model2D("新的项目"); - } - modelRef.set(model); - logger.info("模型加载成功: {}", modelPath); - if (model != null && !modelReady.isDone()) { - modelReady.complete(model); - } - } - } catch (Exception e) { - logger.error("模型加载失败: {}", e.getMessage(), e); - } - } - - public void startRendering() { - if (!GLFW.glfwInit()) { - throw new RuntimeException("无法初始化 GLFW"); - } - renderThread = new Thread(() -> { - try { - if (modelRef.get() != null && !modelReady.isDone()) { - modelReady.complete(modelRef.get()); - } - createOffscreenContext(); - contextReady.get(); - GLFW.glfwMakeContextCurrent(windowId); - - final long targetNs = 1_000_000_000L / 60L; // 60 FPS - while (running && !GLFW.glfwWindowShouldClose(windowId)) { - long start = System.nanoTime(); - - processGLTasks(); - displayScale += (targetScale - displayScale) * ZOOM_SMOOTHING; - renderFrame(); - - long elapsed = System.nanoTime() - start; - long sleepNs = targetNs - elapsed; - if (sleepNs > 0) { - LockSupport.parkNanos(sleepNs); - } - } - } catch (Exception e) { - logger.error("渲染线程异常", e); - } finally { - cleanup(); - } - }); - renderThread.setDaemon(true); - renderThread.setName("GL-Render-Thread"); - renderThread.start(); - } - - private void renderFrame() { - if (!contextInitialized || windowId == 0) return; - GLFW.glfwMakeContextCurrent(windowId); - Model2D currentModel = modelRef.get(); - if (currentModel != null) { - Color panelBackground = UIManager.getColor("Panel.background").darker(); - RenderSystem.setClearColor( - panelBackground.getRed() / 255.0f, - panelBackground.getGreen() / 255.0f, - panelBackground.getBlue() / 255.0f, - panelBackground.getAlpha() / 255.0f - ); - RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT); - - ModelRender.render(1.0f / 60f, currentModel); - - } else { - RenderSystem.setClearColor(0.1f, 0.1f, 0.15f, 1f); - RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT); - } - - readPixelsToImage(); - - if (repaintCallback != null) { - repaintCallback.repaint(); - } - } - - /** - * Reads pixels asynchronously using PBOs into the back buffer, then swaps it to the front. - */ - private void readPixelsToImage() { - final int w = Math.max(1, this.width); - final int h = Math.max(1, this.height); - - GL15.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, pboIds[pboIndex]); - - RenderSystem.readPixels(0, 0, w, h, GL13.GL_BGRA, GL13.GL_UNSIGNED_INT_8_8_8_8_REV, 0); - - GL15.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, pboIds[nextPboIndex]); - ByteBuffer byteBuffer = GL15.glMapBuffer(GL21.GL_PIXEL_PACK_BUFFER, GL15.GL_READ_ONLY); - - if (byteBuffer != null) { - // Always write to the back buffer's pixel array - byteBuffer.asIntBuffer().get(backBufferPixelArray); - - // Flip the image vertically in the back buffer - for (int y = 0; y < h / 2; y++) { - int row1 = y * w; - int row2 = (h - 1 - y) * w; - for (int x = 0; x < w; x++) { - int pixel1 = backBufferPixelArray[row1 + x]; - backBufferPixelArray[row1 + x] = backBufferPixelArray[row2 + x]; - backBufferPixelArray[row2 + x] = pixel1; - } - } - - GL15.glUnmapBuffer(GL21.GL_PIXEL_PACK_BUFFER); - } - - GL15.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, 0); - pboIndex = (pboIndex + 1) % 2; - nextPboIndex = (nextPboIndex + 1) % 2; - - // Atomically swap the back buffer to the front for the UI thread to read - swapBuffers(); - } - - private void swapBuffers() { - bufferSwapLock.lock(); - try { - BufferedImage temp = this.frontBuffer; - this.frontBuffer = this.backBuffer; - this.backBuffer = temp; - this.backBufferPixelArray = ((DataBufferInt) this.backBuffer.getRaster().getDataBuffer()).getData(); - } finally { - bufferSwapLock.unlock(); - } - } - - private void processGLTasks() { - Runnable task; - while ((task = glTaskQueue.poll()) != null) { - try { - task.run(); - } catch (Exception e) { - logger.error("执行 GL 任务时出错", e); - } - } - } - - public void resize(int newWidth, int newHeight) { - executeInGLContext(() -> { - if (contextInitialized && windowId != 0) { - this.width = Math.max(1, newWidth); - this.height = Math.max(1, newHeight); - GLFW.glfwMakeContextCurrent(windowId); - GLFW.glfwSetWindowSize(windowId, this.width, this.height); - RenderSystem.viewport(0, 0, this.width, this.height); - ModelRender.setViewport(this.width, this.height); - - GL15.glDeleteBuffers(pboIds); - initializeFrameResources(); - - } else { - this.width = Math.max(1, newWidth); - this.height = Math.max(1, newHeight); - } - }); - } - - public CompletableFuture waitForContext() { - return contextReady; - } - - public boolean isContextInitialized() { - return contextInitialized; - } - - public boolean isRunning() { - return running && contextInitialized; - } - - public void dispose() { - running = false; - cameraDragging = false; - taskExecutor.shutdown(); - - if (renderThread != null) { - try { - renderThread.join(2000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - cleanup(); - } - - private void cleanup() { - CompletableFuture cleanupFuture = executeInGLContext(() -> { - if (ModelRender.isInitialized()) { - ModelRender.cleanup(); - logger.info("ModelRender 已清理"); - } - if (contextInitialized) { - GL15.glDeleteBuffers(pboIds); - } - }); - - try { - cleanupFuture.get(2, TimeUnit.SECONDS); - } catch (Exception e) { - logger.error("Error during GL resource cleanup", e); - } - - if (windowId != 0) { - GLFW.glfwDestroyWindow(windowId); - windowId = 0; - } - - GLFW.glfwTerminate(); - logger.info("OpenGL 资源已清理"); - } - - public CompletableFuture executeInGLContext(Runnable task) { - CompletableFuture future = new CompletableFuture<>(); - if (!running) { - future.completeExceptionally(new IllegalStateException("渲染线程已停止")); - return future; - } - contextReady.thenRun(() -> { - try { - glTaskQueue.put(() -> { - try { - task.run(); - future.complete(null); - } catch (Exception e) { - future.completeExceptionally(e); - } - }); - } catch (Exception e) { - future.completeExceptionally(e); - } - }); - return future; - } - - public CompletableFuture executeInGLContext(Callable task) { - CompletableFuture future = new CompletableFuture<>(); - if (!running) { - future.completeExceptionally(new IllegalStateException("渲染线程已停止")); - return future; - } - contextReady.thenRun(() -> { - try { - boolean offered = glTaskQueue.offer(() -> { - try { - future.complete(task.call()); - } catch (Exception e) { - future.completeExceptionally(e); - } - }, 5, TimeUnit.SECONDS); - - if (!offered) { - future.completeExceptionally(new TimeoutException("任务队列已满,无法在5秒内添加任务")); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - future.completeExceptionally(new IllegalStateException("任务提交被中断", e)); - } - }); - return future; - } - - public void executeInGLContextSync(Runnable task) { - if (!running) { - throw new IllegalStateException("渲染线程已停止"); - } - try { - executeInGLContext(task).get(10, TimeUnit.SECONDS); - } catch (Exception e) { - throw new RuntimeException("执行同步GL任务时出错", e); - } - } - - public T executeInGLContextSync(Callable task) throws Exception { - if (!running) { - throw new IllegalStateException("渲染线程已停止"); - } - return executeInGLContext(task).get(10, TimeUnit.SECONDS); - } - - public void setDisplayScale(float scale) { - this.displayScale = scale; - } - - public void setTargetScale(float scale) { - this.targetScale = scale; - } - - public float getDisplayScale() { - return displayScale; - } - - public float getTargetScale() { - return targetScale; - } - - public CompletableFuture loadModel(String newModelPath) { - return executeInGLContext(() -> { - Model2D model; - try { - if (newModelPath != null && !newModelPath.isEmpty()) { - model = Model2D.loadFromFile(newModelPath); - logger.info("动态加载模型成功: {}", newModelPath); - } else { - model = new Model2D("新的空项目"); - logger.info("创建新的空模型项目"); - } - this.modelPath = newModelPath; - modelRef.set(model); - if (!modelReady.isDone()) { - modelReady.complete(model); - } - if (repaintCallback != null) { - SwingUtilities.invokeLater(repaintCallback::repaint); - } - return model; - } catch (Throwable e) { - logger.error("动态加载模型失败: {}", e.getMessage(), e); - Model2D emptyModel = new Model2D("加载失败"); - modelRef.set(emptyModel); - this.modelPath = null; - if (repaintCallback != null) { - SwingUtilities.invokeLater(repaintCallback::repaint); - } - throw new Exception("模型加载失败: " + e.getMessage(), e); - } - }); - } - - public void loadModel(Model2D newModel) { - executeInGLContext(() -> { - modelRef.set(newModel); - if (!modelReady.isDone()) { - modelReady.complete(newModel); - } - if (repaintCallback != null) { - SwingUtilities.invokeLater(repaintCallback::repaint); - } - }); - } - - public interface RepaintCallback { - void repaint(); - } - - /** - * Returns the current, complete frame to be drawn by the UI thread. - * This is guaranteed to be a stable image that is not being written to. - */ - public BufferedImage getCurrentFrame() { - return frontBuffer; - } - - public boolean isCameraDragging() { - return cameraDragging; - } - - public void setCameraDragging(boolean cameraDragging) { - this.cameraDragging = cameraDragging; - } - - public String getModelPath() { - return modelPath; - } - - public Model2D getModel() { - return modelRef.get(); - } - - public CompletableFuture waitForModel() { - return modelReady; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/KeyboardManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/KeyboardManager.java deleted file mode 100644 index 3ffba4b..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/KeyboardManager.java +++ /dev/null @@ -1,375 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.manager; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.awt.tools.Tool; -import com.chuangzhou.vivid2D.render.systems.Camera; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.*; -import java.awt.event.ActionEvent; -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.util.HashMap; -import java.util.Map; - -public class KeyboardManager { - private static final Logger logger = LoggerFactory.getLogger(KeyboardManager.class); - private final ModelRenderPanel panel; - private volatile boolean shiftPressed = false; - private volatile boolean ctrlPressed = false; - - // 存储自定义快捷键 - private final Map customShortcuts = new HashMap<>(); - private final Map customActions = new HashMap<>(); - - public KeyboardManager(ModelRenderPanel panel){ - this.panel = panel; - } - - /** - * 初始化键盘快捷键 - */ - public void initKeyboardShortcuts() { - // 获取输入映射和动作映射 - InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); - ActionMap actionMap = panel.getActionMap(); - - // 撤回快捷键:Ctrl+Z - registerShortcut("undo", KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK), - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - panel.getStatusRecordManagement().undo(); - } - }); - - // 重做快捷键:Ctrl+Y 或 Ctrl+Shift+Z - registerShortcut("redo", KeyStroke.getKeyStroke(KeyEvent.VK_Y, KeyEvent.CTRL_DOWN_MASK), - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - panel.getStatusRecordManagement().redo(); - } - }); - registerShortcut("redo2", KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK), - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - panel.getStatusRecordManagement().redo(); - } - }); - - // 清除历史记录:Ctrl+Shift+Delete - registerShortcut("clearHistory", KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK), - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - panel.getStatusRecordManagement().clearHistory(); - } - }); - - // 摄像机重置快捷键:Ctrl+R - registerShortcut("resetCamera", KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK), - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - panel.resetCamera(); - logger.info("重置摄像机"); - } - }); - - // 摄像机启用/禁用快捷键:Ctrl+E - registerShortcut("toggleCamera", KeyStroke.getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK), - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - Camera camera = ModelRender.getCamera(); - boolean newState = !camera.isEnabled(); - camera.setEnabled(newState); - logger.info("{}摄像机", newState ? "启用" : "禁用"); - } - }); - registerToolShortcuts(); - setupKeyListeners(); - } - - /** - * 注册工具快捷键 - */ - private void registerToolShortcuts() { - registerShortcut("vertexTool", KeyStroke.getKeyStroke(KeyEvent.VK_T, KeyEvent.CTRL_DOWN_MASK), - new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - panel.switchTool("顶点变形工具"); - logger.info("切换到顶点变形工具"); - } - }); - } - - /** - * 注册自定义快捷键 - * @param actionName 动作名称 - * @param keyStroke 按键组合 - * @param action 对应的动作 - */ - public void registerShortcut(String actionName, KeyStroke keyStroke, AbstractAction action) { - InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); - ActionMap actionMap = panel.getActionMap(); - - // 如果已存在相同的快捷键,先移除 - if (customShortcuts.containsKey(actionName)) { - KeyStroke oldKeyStroke = customShortcuts.get(actionName); - inputMap.remove(oldKeyStroke); - } - - // 注册新的快捷键 - inputMap.put(keyStroke, actionName); - actionMap.put(actionName, action); - - // 保存到自定义快捷键映射 - customShortcuts.put(actionName, keyStroke); - customActions.put(actionName, action); - - logger.debug("注册快捷键: {} -> {}", keyStroke, actionName); - } - - /** - * 注销自定义快捷键 - * @param actionName 动作名称 - */ - public void unregisterShortcut(String actionName) { - InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); - ActionMap actionMap = panel.getActionMap(); - - if (customShortcuts.containsKey(actionName)) { - KeyStroke keyStroke = customShortcuts.get(actionName); - inputMap.remove(keyStroke); - actionMap.remove(actionName); - - customShortcuts.remove(actionName); - customActions.remove(actionName); - - logger.debug("注销快捷键: {}", actionName); - } - } - - /** - * 注册工具快捷键 - * @param toolName 工具名称 - * @param keyStroke 按键组合 - */ - public void registerToolShortcut(String toolName, KeyStroke keyStroke) { - String actionName = "tool_" + toolName; - registerShortcut(actionName, keyStroke, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - panel.switchTool(toolName); - logger.info("切换到工具: {}", toolName); - } - }); - } - - /** - * 注册工具循环切换快捷键 - * @param keyStroke 按键组合 - */ - public void registerToolCycleShortcut(KeyStroke keyStroke) { - registerShortcut("cycleTools", keyStroke, new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - panel.getToolManagement().switchToPreviousTool(); - Tool currentTool = panel.getCurrentTool(); - if (currentTool != null) { - logger.info("切换到上一个工具: {}", currentTool.getToolName()); - } - } - }); - } - - /** - * 设置键盘监听器 - */ - private void setupKeyListeners() { - panel.addKeyListener(new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - handleKeyPressed(e); - } - - @Override - public void keyReleased(KeyEvent e) { - handleKeyReleased(e); - } - }); - } - - /** - * 处理按键按下 - */ - private void handleKeyPressed(KeyEvent e) { - int keyCode = e.getKeyCode(); - - // 更新修饰键状态 - if (keyCode == KeyEvent.VK_SHIFT) { - shiftPressed = true; - } - - // 处理功能快捷键 - if (ctrlPressed) { - switch (keyCode) { - case KeyEvent.VK_A: - // Ctrl+A 全选 - e.consume(); - panel.selectAllMeshes(); - logger.debug("全选所有网格"); - break; - case KeyEvent.VK_D: - // Ctrl+D 取消选择 - e.consume(); - panel.clearSelectedMeshes(); - logger.debug("取消所有选择"); - break; - case KeyEvent.VK_1: - // Ctrl+1 切换到第一个工具 - e.consume(); - switchToToolByIndex(0); - break; - case KeyEvent.VK_2: - // Ctrl+2 切换到第二个工具 - e.consume(); - switchToToolByIndex(1); - break; - case KeyEvent.VK_3: - // Ctrl+3 切换到第三个工具 - e.consume(); - switchToToolByIndex(2); - break; - } - } - - // 单独按键处理 - switch (keyCode) { - case KeyEvent.VK_ESCAPE: - // ESC 键取消所有选择或退出工具 - e.consume(); - if (panel.getToolManagement().hasActiveTool() && - !panel.getToolManagement().getCurrentTool().getToolName().equals("选择工具")) { - panel.switchToDefaultTool(); - logger.info("按ESC键切换到选择工具"); - } else { - panel.clearSelectedMeshes(); - logger.info("按ESC键取消所有选择"); - } - break; - case KeyEvent.VK_SPACE: - // 空格键临时切换到手型工具(用于移动视图) - if (!e.isConsumed()) { - // 这里可以添加空格键拖拽视图的功能 - // 需要与鼠标管理中键拖拽功能配合 - } - break; - } - } - - /** - * 处理按键释放 - */ - private void handleKeyReleased(KeyEvent e) { - int keyCode = e.getKeyCode(); - - // 更新修饰键状态 - if (keyCode == KeyEvent.VK_SHIFT) { - shiftPressed = false; - } else if (keyCode == KeyEvent.VK_CONTROL) { - ctrlPressed = false; - } - } - - /** - * 根据索引切换到工具 - */ - private void switchToToolByIndex(int index) { - java.util.List tools = panel.getToolManagement().getRegisteredTools(); - if (index >= 0 && index < tools.size()) { - Tool tool = tools.get(index); - panel.switchTool(tool.getToolName()); - logger.info("切换到工具: {}", tool.getToolName()); - } - } - - /** - * 获取所有注册的快捷键信息 - */ - public Map getShortcutInfo() { - Map info = new HashMap<>(); - for (Map.Entry entry : customShortcuts.entrySet()) { - info.put(entry.getKey(), entry.getValue().toString()); - } - return info; - } - - /** - * 重新加载所有快捷键 - */ - public void reloadShortcuts() { - // 清除所有自定义快捷键 - InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); - ActionMap actionMap = panel.getActionMap(); - - for (String actionName : customShortcuts.keySet()) { - KeyStroke keyStroke = customShortcuts.get(actionName); - inputMap.remove(keyStroke); - actionMap.remove(actionName); - } - - customShortcuts.clear(); - customActions.clear(); - - // 重新初始化 - initKeyboardShortcuts(); - logger.info("重新加载所有快捷键"); - } - - /** - * 获取Shift键状态 - */ - public boolean getIsShiftPressed(){ - return shiftPressed; - } - - /** - * 获取Ctrl键状态 - */ - public boolean getIsCtrlPressed(){ - return ctrlPressed; - } - - /** - * 清理资源 - */ - public void dispose() { - // 移除所有键盘监听器 - for (KeyListener listener : panel.getKeyListeners()) { - panel.removeKeyListener(listener); - } - - // 清除所有快捷键 - InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); - ActionMap actionMap = panel.getActionMap(); - - for (String actionName : customShortcuts.keySet()) { - KeyStroke keyStroke = customShortcuts.get(actionName); - inputMap.remove(keyStroke); - actionMap.remove(actionName); - } - - customShortcuts.clear(); - customActions.clear(); - - logger.info("键盘管理器已清理"); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/LayerOperationManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/LayerOperationManager.java deleted file mode 100644 index 0f8809d..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/LayerOperationManager.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.manager; - -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; - -import java.io.Serial; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class LayerOperationManager { - private final Model2D model; - - public static class LayerInfo implements Serializable { - @Serial - private static final long serialVersionUID = 2L; - String name; - int orderIndex; - - public LayerInfo(String name, int orderIndex) { - this.name = name; - this.orderIndex = orderIndex; - } - - @Override - public String toString() { - return "LayerInfo{" + - "name='" + name + '\'' + - ", orderIndex=" + orderIndex + - '}'; - } - } - - public final List layerMetadata; - - public LayerOperationManager(Model2D model) { - this.model = model; - this.layerMetadata = new ArrayList<>(); - initializeMetadata(); - } - - /** - * 加载并替换当前的图层元数据列表,并根据加载的顺序重新排列 Model2D 内部的图层。 - * 通常在反序列化(加载文件)时调用。 - * @param loadedMetadata 从文件加载的 LayerInfo 列表 - */ - public void loadMetadata(List loadedMetadata) { - if (loadedMetadata == null || loadedMetadata.isEmpty()) return; - this.layerMetadata.clear(); - this.layerMetadata.addAll(loadedMetadata); - Map partMap = model.getPartMap(); - if (partMap == null || partMap.isEmpty()) return; - List modelReorderList = new ArrayList<>(loadedMetadata.size()); - for (LayerInfo info : loadedMetadata) { - ModelPart part = partMap.get(info.name); - if (part != null) { - modelReorderList.add(part); - } else { - System.err.println("Warning: ModelPart with name '" + info.name + "' not found during metadata loading. Skipping part."); - } - } - if (!modelReorderList.isEmpty()) { - replaceModelPartsList(modelReorderList); - model.markNeedsUpdate(); - } else { - System.err.println("Error: Could not reconstruct model parts list from loaded metadata."); - } - } - - private void initializeMetadata() { - layerMetadata.clear(); - List parts = model.getParts(); - if (parts != null) { - for (int i = 0; i < parts.size(); i++) { - ModelPart part = parts.get(i); - layerMetadata.add(new LayerInfo(part.getName(), i)); - } - } - } - - public void addLayer(String name) { - ModelPart newPart = model.createPart(name); - if (newPart != null) { - int newIndex = model.getParts() != null ? model.getParts().size() - 1 : 0; - layerMetadata.add(new LayerInfo(newPart.getName(), newIndex)); - } - model.markNeedsUpdate(); - } - - public void removeLayer(ModelPart part) { - if (part == null) return; - List parts = model.getParts(); - if (parts != null) parts.remove(part); - Map partMap = model.getPartMap(); - if (partMap != null) partMap.remove(part.getName()); - initializeMetadata(); - model.markNeedsUpdate(); - } - - public void moveLayer(List visualOrder) { - List newModelParts = new ArrayList<>(visualOrder.size()); - for (int i = visualOrder.size() - 1; i >= 0; i--) { - newModelParts.add(visualOrder.get(i)); - } - replaceModelPartsList(newModelParts); - initializeMetadata(); - model.markNeedsUpdate(); - } - - public void setLayerOpacity(ModelPart part, float opacity) { - part.setOpacity(opacity); - model.markNeedsUpdate(); - } - - public void setLayerVisibility(ModelPart part, boolean visible) { - part.setVisible(visible); - model.markNeedsUpdate(); - } - - private void replaceModelPartsList(List newParts) { - if (model == null) return; - try { - java.lang.reflect.Field partsField = model.getClass().getDeclaredField("parts"); - partsField.setAccessible(true); - Object old = partsField.get(model); - if (old instanceof List) { - ((List) old).clear(); - ((List) old).addAll(newParts); - } else { - partsField.set(model, newParts); - } - } catch (Exception e) { - e.printStackTrace(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/MouseManagement.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/MouseManagement.java deleted file mode 100644 index 77c06b5..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/MouseManagement.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.manager; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; - -import java.awt.*; -import java.awt.event.*; - -public class MouseManagement { - - private final ModelRenderPanel modelRenderPanel; - private final GLContextManager glContextManager; - private final CameraManagement cameraManagement; - private final KeyboardManager keyboardManager; - - public MouseManagement(ModelRenderPanel modelRenderPanel, - GLContextManager glContextManager, - CameraManagement cameraManagement, - KeyboardManager keyboardManager){ - this.modelRenderPanel = modelRenderPanel; - this.glContextManager = glContextManager; - this.cameraManagement = cameraManagement; - this.keyboardManager = keyboardManager; - } - - /** - * 添加鼠标事件监听器 - */ - public void addMouseListeners() { - modelRenderPanel.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - modelRenderPanel.handleMouseClick(e); - } - - @Override - public void mousePressed(MouseEvent e) { - modelRenderPanel.handleMousePressed(e); - } - - @Override - public void mouseReleased(MouseEvent e) { - modelRenderPanel.handleMouseReleased(e); - } - - @Override - public void mouseExited(MouseEvent e) { - modelRenderPanel.setCursor(Cursor.getDefaultCursor()); - } - }); - - modelRenderPanel.addMouseWheelListener(new MouseWheelListener() { - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - if (!glContextManager.isContextInitialized()) return; - final int screenX = e.getX(); - final int screenY = e.getY(); - final int notches = e.getWheelRotation(); - final boolean fine = e.isShiftDown(); - cameraManagement.resizingApplications(screenX, screenY, notches, fine); - } - }); - - modelRenderPanel.addMouseMotionListener(new MouseMotionAdapter() { - @Override - public void mouseMoved(MouseEvent e) { - modelRenderPanel.handleMouseMove(e); - } - - @Override - public void mouseDragged(MouseEvent e) { - modelRenderPanel.handleMouseDragged(e); - } - }); - - modelRenderPanel.addMouseWheelListener(e -> { - int notches = e.getWheelRotation(); - boolean fine = (e.isShiftDown() || keyboardManager.getIsShiftPressed()); // 支持 Shift 更精细控制 - double step = fine ? Math.pow(CameraManagement.ZOOM_STEP, 0.25) : CameraManagement.ZOOM_STEP; - if (notches > 0) { - // 滚轮下:缩小 - glContextManager.targetScale *= Math.pow(1.0 / step, notches); - } else if (notches < 0) { - // 滚轮上:放大 - glContextManager.targetScale *= Math.pow(step, -notches); - } - glContextManager.targetScale = Math.max(CameraManagement.ZOOM_MIN, Math.min(CameraManagement.ZOOM_MAX, glContextManager.targetScale)); - }); - modelRenderPanel.setFocusable(true); - modelRenderPanel.requestFocusInWindow(); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java deleted file mode 100644 index 816a9be..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java +++ /dev/null @@ -1,482 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.manager; - -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.awt.ParametersPanel; -import com.chuangzhou.vivid2D.render.awt.manager.data.LayerOperationManagerData; -import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData; -import com.chuangzhou.vivid2D.render.model.AnimationParameter; -import com.chuangzhou.vivid2D.render.model.ModelEvent; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.util.Vertex; -import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; -import org.joml.Matrix3f; -import org.joml.Vector2f; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileInputStream; -import java.io.ObjectInputStream; -import java.util.*; - -public class ParametersManagement { - private static final Logger logger = LoggerFactory.getLogger(ParametersManagement.class); - private final ParametersPanel parametersPanel; - public List oldValues = new ArrayList<>(); - - public ParametersManagement(ParametersPanel parametersPanel) { - this.parametersPanel = parametersPanel; - ModelRenderPanel renderPanel = parametersPanel.getRenderPanel(); - installingCallbacks(); - renderPanel.getModel().addEvent((eventName, eventBus) -> { - if (eventName.equals("model_part_added")) { - installingCallbacks(); - } - }); - } - - /** - * 安装参数管理回调 - */ - public void installingCallbacks() { - ModelRenderPanel renderPanel = parametersPanel.getRenderPanel(); - for (int i = 0; i < renderPanel.getModel().getParts().size(); i++) { - ModelPart modelPart = renderPanel.getModel().getParts().get(i); - modelPart.addEvent((eventName, eventBus) -> { - if (eventName.equals("vertex_position")){ - //logger.info("顶点位置已更新: {}", eventBus); - updateVertex((ModelPart) eventBus); - } - }); - } - } - - /** - * 查找并替换一个顶点的关键帧数据。 - * 它会基于旧的顶点状态(oldVertexObj)在当前选定的关键帧上查找一个完全匹配的记录。 - * - 如果找到,则用新顶点状态(newVertexObj)的数据替换它。 - * - 如果没有找到,则调用 broadcast 创建一个新的关键帧记录。 - * - * @param caller 触发事件的 ModelPart - */ - public void updateVertex(ModelPart caller) { - for (int i = 0; i < oldValues.size(); i++) { - Parameter existingParameter = oldValues.get(i); - - if (!existingParameter.modelPart().equals(caller)) { - continue; - } - - List originalValues = existingParameter.value(); - List updatedValues = new ArrayList<>(originalValues); - boolean vertexDataChanged = false; - - for (int j = 0; j < updatedValues.size(); j++) { - if (!"meshVertices".equals(existingParameter.paramId().get(j))) { - continue; - } - - Object value = originalValues.get(j); - if (!(value instanceof Map)) { - continue; - } - - @SuppressWarnings("unchecked") - Map payload = (Map) value; - String storedVertexId = (String) payload.get("id"); - Vertex sourceVertex = (Vertex) payload.get("Vertex"); - - if (sourceVertex == null) continue; - //Vertex newVertex = findLiveVertex(caller,sourceVertex).copy(); - Vertex newVertex = sourceVertex.copy(); - Vector2f worldPoint = Matrix3fUtils.transformPoint(caller.getWorldTransform(), newVertex.originalPosition); - newVertex.position.set(worldPoint); - Map newVertexUpdatePayload = new HashMap<>(); - newVertexUpdatePayload.put("id", storedVertexId); - newVertexUpdatePayload.put("Vertex", newVertex); - updatedValues.set(j, newVertexUpdatePayload); - vertexDataChanged = true; - //logger.info("已更新顶点: {} -> {}", storedVertexId, worldPoint); - } - - if (vertexDataChanged) { - Parameter updatedParameter = new Parameter( - existingParameter.modelPart(), - new ArrayList<>(existingParameter.animationParameter()), - new ArrayList<>(existingParameter.paramId()), - updatedValues, - new ArrayList<>(existingParameter.keyframe()), - new ArrayList<>(existingParameter.isKeyframe()) - ); - oldValues.set(i, updatedParameter); - } - } - } - - private Vertex findLiveVertex(ModelPart part, Vertex vertex) { - for (com.chuangzhou.vivid2D.render.model.Mesh2D mesh : part.getMeshes()) { - for (Vertex v : mesh.getActiveVertexList()) { - if (vertex._equals(v)) { - return v; - } - } - } - logger.warn("未找到匹配的顶点: {}", vertex); - return vertex; - } - - public static ParametersManagement getInstance(ParametersPanel parametersPanel) { - String managementFilePath = parametersPanel.getRenderPanel().getGlContextManager().getModelPath() + ".data"; - File managementFile = new File(managementFilePath); - ParametersManagement instance = new ParametersManagement(parametersPanel); - if (managementFile.exists()) { - logger.info("已找到参数管理数据文件: {}", managementFilePath); - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(managementFile))) { - Object layerDataObject = ois.readObject(); - Object o = ois.readObject(); - if (o instanceof ParametersManagementData managementData) { - List parts = parametersPanel.getRenderPanel().getModel().getParts(); - ParametersManagement management = managementData.toParametersManagement(parametersPanel, parts); - //logger.info("参数管理数据转换成功: {}", management); - instance = new ParametersManagement(parametersPanel); - instance.oldValues = management.oldValues; - //logger.info("参数管理数据加载成功: {}", management.oldValues); - parametersPanel.parametersManagement = instance; - return instance; - } else { - logger.warn("加载参数管理数据失败: 预期第二个对象为ParametersManagementData,但实际为 {}", o != null ? o.getClass().getName() : "null"); - } - } catch (Exception e) { - logger.warn("加载参数管理数据失败: {}", e.getMessage()); - } - } else { - logger.info("未找到参数管理数据文件 {},创建新的参数管理实例", managementFilePath); - } - parametersPanel.parametersManagement = instance; - return instance; - } - - /** - * 获取ModelPart的所有参数 - * @param modelPart 部件 - * @return 该部件的所有参数 - */ - public Parameter getModelPartParameters(ModelPart modelPart) { - for (Parameter parameter : oldValues) { - if (parameter.modelPart().equals(modelPart)) { - return parameter; - } - } - return null; - } - - /** - * 获取当前选中的帧 - * position List.of(float modelX, float modelY) - * rotate float modelAngle - * @param isPreciseCheck 是否精确检查 - * @return 当前选中的帧 - */ - public Float getSelectedKeyframe(boolean isPreciseCheck) { - return parametersPanel.getSelectedKeyframe(isPreciseCheck); - } - - /** - * 获取当前选中的参数 - * @return 当前选中的参数 - */ - public AnimationParameter getSelectParameter() { - if (parametersPanel.getSelectParameter() == null){ - // System.out.println("getSelectParameter() is null"); - return null; - } - return parametersPanel.getSelectParameter().copy(); - } - - /** - * 精确地从 ModelPart 的记录中删除指定索引的参数条目。 - * * @param targetModelPart 目标 ModelPart - * @param indexToRemove 在该 ModelPart 记录内部的索引 - */ - public void removeParameterAt(ModelPart targetModelPart, int indexToRemove) { - for (int i = 0; i < oldValues.size(); i++) { - Parameter existingParameter = oldValues.get(i); - if (existingParameter.modelPart().equals(targetModelPart)) { - int size = existingParameter.keyframe().size(); - if (indexToRemove >= 0 && indexToRemove < size) { - List newAnimationParameters = new ArrayList<>(existingParameter.animationParameter()); - List newParamIds = new ArrayList<>(existingParameter.paramId()); - List newValues = new ArrayList<>(existingParameter.value()); - List newKeyframes = new ArrayList<>(existingParameter.keyframe()); - List newIsKeyframes = new ArrayList<>(existingParameter.isKeyframe()); - newAnimationParameters.remove(indexToRemove); - newParamIds.remove(indexToRemove); - newValues.remove(indexToRemove); - newKeyframes.remove(indexToRemove); - newIsKeyframes.remove(indexToRemove); - if (newKeyframes.isEmpty()) { - oldValues.remove(i); - } else { - Parameter updatedParameter = new Parameter( - targetModelPart, - newAnimationParameters, - newParamIds, - newValues, - newKeyframes, - newIsKeyframes - ); - oldValues.set(i, updatedParameter); - } - } - return; - } - } - } - - /** - * 监听参数变化 (强制添加新记录,即使 paramId 已存在) - * 如果列表中已存在相同 modelPart 的记录,则添加新参数到该记录的列表尾部;否则添加新记录。 - * @param modelPart 变化的部件 - * @param paramId 参数id - * @param value 最终值 - */ - public void broadcast(ModelPart modelPart, String paramId, Object value, Float specifiedKeyframe) { - if (getSelectParameter() == null) { - return; - } - AnimationParameter currentAnimParam = getSelectParameter(); - if (specifiedKeyframe == null) { - return; - } - boolean isKeyframe = currentAnimParam.getKeyframes().contains(specifiedKeyframe); - String newId = null; - if (paramId.equals("meshVertices") && value instanceof Map) { - @SuppressWarnings("unchecked") - Map payload = (Map) value; - Object idObj = payload.get("id"); - if (idObj instanceof String) { - newId = (String) idObj; - } - } - for (int i = 0; i < oldValues.size(); i++) { - Parameter existingParameter = oldValues.get(i); - if (existingParameter.modelPart().equals(modelPart)) { - List newAnimationParameters = new ArrayList<>(existingParameter.animationParameter()); - List newParamIds = new ArrayList<>(existingParameter.paramId()); - List newValues = new ArrayList<>(existingParameter.value()); - List newKeyframes = new ArrayList<>(existingParameter.keyframe()); - List newIsKeyframes = new ArrayList<>(existingParameter.isKeyframe()); - - int existingIndex = -1; - for (int j = 0; j < newKeyframes.size(); j++) { - boolean keyframeMatches = Objects.equals(newKeyframes.get(j), specifiedKeyframe); - boolean paramIdMatches = paramId.equals(newParamIds.get(j)); - AnimationParameter recordAnimParam = newAnimationParameters.get(j); - boolean animParamMatches = recordAnimParam != null && recordAnimParam.equals(currentAnimParam); - boolean idMatches = true; - - if (paramIdMatches && paramId.equals("meshVertices")) { - Object oldValue = newValues.get(j); - if (oldValue instanceof Map) { - @SuppressWarnings("unchecked") - Map oldPayload = (Map) oldValue; - Object oldIdObj = oldPayload.get("id"); - String oldId = (oldIdObj instanceof String) ? (String) oldIdObj : null; - idMatches = Objects.equals(newId, oldId); - } else { - idMatches = false; - } - } - - if (keyframeMatches && paramIdMatches && animParamMatches && idMatches) { - existingIndex = j; - break; - } - } - - if (existingIndex != -1) { - newValues.set(existingIndex, value); - } else { - newAnimationParameters.add(currentAnimParam); - newParamIds.add(paramId); - newValues.add(value); - newKeyframes.add(specifiedKeyframe); - newIsKeyframes.add(isKeyframe); - } - - Parameter updatedParameter = new Parameter(modelPart, newAnimationParameters, newParamIds, newValues, newKeyframes, newIsKeyframes); - oldValues.set(i, updatedParameter); - return; - } - } - - // 如果没有找到现有的参数记录,创建新的 - Parameter parameter = new Parameter( - modelPart, - Collections.singletonList(currentAnimParam), - Collections.singletonList(paramId), - Collections.singletonList(value), - Collections.singletonList(specifiedKeyframe), - Collections.singletonList(isKeyframe) - ); - oldValues.add(parameter); - } - - public void broadcast(ModelPart modelPart, String paramId, Object value) { - Float currentKeyframe = getSelectedKeyframe(false); - broadcast(modelPart, paramId, value, currentKeyframe); - } - - /** - * 移除特定参数 - * @param modelPart 部件 - * @param paramId 参数id - */ - public void removeParameter(ModelPart modelPart, String paramId) { - for (int i = 0; i < oldValues.size(); i++) { - Parameter existingParameter = oldValues.get(i); - if (existingParameter.modelPart().equals(modelPart)) { - if ("all".equals(paramId)) { - oldValues.remove(i); - return; - } - List newAnimationParameters = new ArrayList<>(existingParameter.animationParameter()); - List newParamIds = new ArrayList<>(existingParameter.paramId()); - List newValues = new ArrayList<>(existingParameter.value()); - List newKeyframes = new ArrayList<>(existingParameter.keyframe()); - List newIsKeyframes = new ArrayList<>(existingParameter.isKeyframe()); - - int paramIndex = newParamIds.indexOf(paramId); - if (paramIndex != -1) { - newAnimationParameters.remove(paramIndex); // NEW - newParamIds.remove(paramIndex); - newValues.remove(paramIndex); - newKeyframes.remove(paramIndex); - newIsKeyframes.remove(paramIndex); - - if (newParamIds.isEmpty()) { - oldValues.remove(i); - } else { - // 更新记录 - Parameter updatedParameter = new Parameter( - existingParameter.modelPart(), - newAnimationParameters, - newParamIds, - newValues, - newKeyframes, - newIsKeyframes - ); - oldValues.set(i, updatedParameter); - } - } - return; - } - } - } - - /** - * 获取参数值 (返回 ModelPart 的所有参数的防御性副本) - * @param modelPart 部件 - * @param paramId 参数id (该参数在此方法中将被忽略,因为返回的是所有参数) - * @return 该部件所有参数的 Parameter 记录的副本 - */ - public Parameter getValue(ModelPart modelPart, String paramId) { - for (Parameter parameter : oldValues) { - if (parameter.modelPart().equals(modelPart)) { - List indices = new ArrayList<>(); - for (int i = 0; i < parameter.paramId().size(); i++) { - if (parameter.paramId().get(i).equals(paramId)) indices.add(i); - } - if (indices.isEmpty()) return null; - List anims = new ArrayList<>(); - List ids = new ArrayList<>(); - List values = new ArrayList<>(); - List keyframes = new ArrayList<>(); - List isKeyframes = new ArrayList<>(); - for (int idx : indices) { - anims.add(parameter.animationParameter().get(idx)); - ids.add(parameter.paramId().get(idx)); - values.add(parameter.value().get(idx)); - keyframes.add(parameter.keyframe().get(idx)); - isKeyframes.add(parameter.isKeyframe().get(idx)); - } - return new Parameter(parameter.modelPart(), anims, ids, values, keyframes, isKeyframes); - } - } - return null; - } - - public ParametersPanel getParametersPanel() { - return parametersPanel; - } - - public record Parameter( - ModelPart modelPart, - List animationParameter, - List paramId, - List value, - List keyframe, - List isKeyframe - ) { - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - String partName = (modelPart != null) ? modelPart.getName() : "[NULL ModelPart]"; - sb.append("Parameter[Part=").append(partName).append(", "); - sb.append("Details=["); - int size = paramId.size(); - for (int i = 0; i < size; i++) { - String id = paramId.get(i); - Object val = (value != null && value.size() > i) ? value.get(i) : null; - Float kf = (keyframe != null && keyframe.size() > i) ? keyframe.get(i) : null; - Boolean isKf = (isKeyframe != null && isKeyframe.size() > i) ? isKeyframe.get(i) : false; - if (i > 0) { - sb.append("; "); - } - sb.append(String.format("{ID=%s, V=%s, KF=%s, IsKF=%b}", - id, - val, - kf != null ? String.valueOf(kf) : "null", - isKf)); - } - sb.append("]]"); - return sb.toString(); - } - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("ParametersManagement State:\n"); - - if (oldValues.isEmpty()) { - sb.append(" No recorded parameters (oldValues is empty).\n"); - } else { - for (int i = 0; i < oldValues.size(); i++) { - Parameter p = oldValues.get(i); - sb.append(String.format(" --- Record %d ---\n", i)); - String partName; - if (p.modelPart() != null) { - partName = p.modelPart().getName(); - } else { - partName = "[NULL]"; - } - sb.append(String.format(" ModelPart: Part: %s\n", partName)); - int numParams = p.paramId().size(); - for (int j = 0; j < numParams; j++) { - String id = p.paramId().get(j); - Object val = (p.value() != null && p.value().size() > j) ? p.value().get(j) : "[MISSING_VALUE]"; - Float kf = (p.keyframe() != null && p.keyframe().size() > j) ? p.keyframe().get(j) : null; - Boolean isKf = (p.isKeyframe() != null && p.isKeyframe().size() > j) ? p.isKeyframe().get(j) : false; - sb.append(String.format(" - Param ID: %s, Value: %s, Keyframe: %s, IsKeyframe: %b\n", - id, - val != null ? String.valueOf(val) : "[NULL_VALUE]", - kf != null ? String.valueOf(kf) : "[NULL_KEYFRAME]", - isKf)); - } - } - } - - return sb.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/StatusRecordManagement.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/StatusRecordManagement.java deleted file mode 100644 index cc6d384..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/StatusRecordManagement.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.manager; - -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import org.joml.Vector2f; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class StatusRecordManagement { - private final ModelRenderPanel panel; - private final OperationHistoryGlobal operationHistory; - - public StatusRecordManagement(ModelRenderPanel panel, OperationHistoryGlobal operationHistory){ - this.operationHistory = operationHistory; - this.panel = panel; - } - - /** - * 重做操作 - */ - public void redo() { - if (operationHistory != null && operationHistory.canRedo()) { - panel.getGlContextManager().executeInGLContext(() -> { - boolean success = operationHistory.redo(); - if (success) { - panel.repaint(); - System.out.println("重做: " + operationHistory.getRedoDescription()); - } - }); - } else { - System.out.println("没有可重做的操作"); - } - } - - /** - * 记录位置变化操作 - */ - private void recordPositionChange(ModelPart part, Vector2f oldPosition, Vector2f newPosition) { - if (operationHistory != null && part != null) { - operationHistory.recordOperation("SET_POSITION", part, oldPosition, newPosition); - } - } - - /** - * 记录缩放变化操作 - */ - private void recordScaleChange(ModelPart part, Vector2f oldScale, Vector2f newScale) { - if (operationHistory != null && part != null) { - operationHistory.recordOperation("SET_SCALE", part, oldScale, newScale); - } - } - - /** - * 记录旋转变化操作 - */ - private void recordRotationChange(ModelPart part, float oldRotation, float newRotation) { - if (operationHistory != null && part != null) { - operationHistory.recordOperation("SET_ROTATION", part, oldRotation, newRotation); - } - } - - /** - * 记录中心点变化操作 - */ - private void recordPivotChange(ModelPart part, Vector2f oldPivot, Vector2f newPivot) { - if (operationHistory != null && part != null) { - operationHistory.recordOperation("SET_PIVOT", part, oldPivot, newPivot); - } - } - - /** - * 记录拖拽结束操作 - */ - public void recordDragEnd(List parts, Map startPositions) { - if (operationHistory != null && parts != null && !parts.isEmpty()) { - List params = new ArrayList<>(); - params.add(parts); - params.add(startPositions); - // 添加当前位置 - for (ModelPart part : parts) { - params.add(part.getPosition()); - } - operationHistory.recordOperation("DRAG_PART_END", params.toArray()); - } - } - - /** - * 记录调整大小结束操作 - */ - public void recordResizeEnd(List parts, Map startScales) { - if (operationHistory != null && parts != null && !parts.isEmpty()) { - List params = new ArrayList<>(); - params.add(parts); - params.add(startScales); - // 添加当前缩放 - for (ModelPart part : parts) { - params.add(part.getScale()); - } - operationHistory.recordOperation("RESIZE_PART_END", params.toArray()); - } - } - - /** - * 记录旋转结束操作 - */ - public void recordRotateEnd(List parts, Map startRotations) { - if (operationHistory != null && parts != null && !parts.isEmpty()) { - List params = new ArrayList<>(); - params.add(parts); - params.add(startRotations); - // 添加当前旋转 - for (ModelPart part : parts) { - params.add(part.getRotation()); - } - operationHistory.recordOperation("ROTATE_PART_END", params.toArray()); - } - } - - /** - * 记录移动中心点结束操作 - */ - public void recordMovePivotEnd(List parts, Map startPivots) { - if (operationHistory != null && parts != null && !parts.isEmpty()) { - List params = new ArrayList<>(); - params.add(parts); - params.add(startPivots); - // 添加当前中心点 - for (ModelPart part : parts) { - params.add(part.getPivot()); - } - operationHistory.recordOperation("MOVE_PIVOT_END", params.toArray()); - } - } - - /** - * 撤回操作 - */ - public void undo() { - if (operationHistory != null && operationHistory.canUndo()) { - panel.getGlContextManager().executeInGLContext(() -> { - boolean success = operationHistory.undo(); - if (success) { - panel.repaint(); - System.out.println("撤回: " + operationHistory.getUndoDescription()); - } - }); - } else { - System.out.println("没有可撤回的操作"); - } - } - - /** - * 清除操作历史 - */ - public void clearHistory() { - if (operationHistory != null) { - operationHistory.clearHistory(); - System.out.println("操作历史已清除"); - } - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ThumbnailManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ThumbnailManager.java deleted file mode 100644 index ed911fc..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ThumbnailManager.java +++ /dev/null @@ -1,244 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.manager; - -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.Texture; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class ThumbnailManager { - private static final int THUMBNAIL_WIDTH = 48; - private static final int THUMBNAIL_HEIGHT = 48; - - private final Map thumbnailCache = new HashMap<>(); - private ModelRenderPanel renderPanel; - - public ThumbnailManager(ModelRenderPanel renderPanel) { - this.renderPanel = renderPanel; - } - - public BufferedImage getThumbnail(ModelPart part) { - return thumbnailCache.get(part); - } - - public void generateThumbnail(ModelPart part) { - if (renderPanel == null) return; - - try { - BufferedImage thumbnail = renderPanel.getGlContextManager() - .executeInGLContext(() -> renderPartThumbnail(part)) - .get(); - - if (thumbnail != null) { - thumbnailCache.put(part, thumbnail); - } - } catch (Exception e) { - thumbnailCache.put(part, createDefaultThumbnail()); - } - } - - public void removeThumbnail(ModelPart part) { - thumbnailCache.remove(part); - } - - public void clearCache() { - thumbnailCache.clear(); - } - - /** - * 渲染单个部件的缩略图 - */ - private BufferedImage renderPartThumbnail(ModelPart part) { - if (renderPanel == null) return createDefaultThumbnail(); - - try { - return createThumbnailForPart(part); - } catch (Exception e) { - e.printStackTrace(); - return createDefaultThumbnail(); - } - } - - private BufferedImage createThumbnailForPart(ModelPart part) { - BufferedImage thumbnail = new BufferedImage(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = thumbnail.createGraphics(); - - // 设置抗锯齿 - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - - // 绘制背景 - g2d.setColor(new Color(40, 40, 40)); - g2d.fillRect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT); - - try { - // 尝试获取部件的纹理 - Texture texture = null; - List meshes = part.getMeshes(); - if (meshes != null && !meshes.isEmpty()) { - for (Mesh2D mesh : meshes) { - texture = mesh.getTexture(); - if (texture != null) break; - } - } - - if (texture != null && !texture.isDisposed()) { - // 获取纹理的 BufferedImage - BufferedImage textureImage = textureToBufferedImage(texture); - if (textureImage != null) { - // 计算缩放比例以保持宽高比 - int imgWidth = textureImage.getWidth(); - int imgHeight = textureImage.getHeight(); - - if (imgWidth > 0 && imgHeight > 0) { - float scale = Math.min( - (float)(THUMBNAIL_WIDTH - 8) / imgWidth, - (float)(THUMBNAIL_HEIGHT - 8) / imgHeight - ); - - int scaledWidth = (int)(imgWidth * scale); - int scaledHeight = (int)(imgHeight * scale); - int x = (THUMBNAIL_WIDTH - scaledWidth) / 2; - int y = (THUMBNAIL_HEIGHT - scaledHeight) / 2; - - // 绘制纹理图片 - g2d.drawImage(textureImage, x, y, scaledWidth, scaledHeight, null); - - // 绘制边框 - g2d.setColor(Color.WHITE); - g2d.drawRect(x, y, scaledWidth - 1, scaledHeight - 1); - } - } - } - - } catch (Exception e) { - System.err.println("生成缩略图失败: " + part.getName() + " - " + e.getMessage()); - } - - // 如果部件不可见,绘制红色斜线覆盖 - if (!part.isVisible()) { - g2d.setColor(new Color(255, 0, 0, 128)); // 半透明红色 - g2d.setStroke(new BasicStroke(3)); - g2d.drawLine(2, 2, THUMBNAIL_WIDTH - 2, THUMBNAIL_HEIGHT - 2); - g2d.drawLine(THUMBNAIL_WIDTH - 2, 2, 2, THUMBNAIL_HEIGHT - 2); - } - - g2d.dispose(); - return thumbnail; - } - - /** - * 将Texture转换为BufferedImage - */ - private BufferedImage textureToBufferedImage(Texture texture) { - try { - // 确保纹理有像素数据缓存 - texture.ensurePixelDataCached(); - - if (!texture.hasPixelData()) { - System.err.println("纹理没有像素数据: " + texture.getName()); - return null; - } - - byte[] pixelData = texture.getPixelData(); - if (pixelData == null || pixelData.length == 0) { - return null; - } - - int width = texture.getWidth(); - int height = texture.getHeight(); - Texture.TextureFormat format = texture.getFormat(); - int components = format.getComponents(); - - // 创建BufferedImage - BufferedImage image; - switch (components) { - case 1: // 单通道 - image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); - break; - case 3: // RGB - image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); - break; - case 4: // RGBA - image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - break; - default: - System.err.println("不支持的纹理格式组件数量: " + components); - return null; - } - - // 将像素数据复制到BufferedImage,同时翻转垂直方向 - if (components == 4) { - // RGBA格式 - 垂直翻转 - int[] pixels = new int[width * height]; - for (int y = 0; y < height; y++) { - int srcY = height - 1 - y; // 翻转Y坐标 - for (int x = 0; x < width; x++) { - int srcIndex = (srcY * width + x) * 4; - int dstIndex = y * width + x; - - int r = pixelData[srcIndex] & 0xFF; - int g = pixelData[srcIndex + 1] & 0xFF; - int b = pixelData[srcIndex + 2] & 0xFF; - int a = pixelData[srcIndex + 3] & 0xFF; - pixels[dstIndex] = (a << 24) | (r << 16) | (g << 8) | b; - } - } - image.setRGB(0, 0, width, height, pixels, 0, width); - } else if (components == 3) { - // RGB格式 - 垂直翻转 - for (int y = 0; y < height; y++) { - int srcY = height - 1 - y; // 翻转Y坐标 - for (int x = 0; x < width; x++) { - int srcIndex = (srcY * width + x) * 3; - int r = pixelData[srcIndex] & 0xFF; - int g = pixelData[srcIndex + 1] & 0xFF; - int b = pixelData[srcIndex + 2] & 0xFF; - int rgb = (r << 16) | (g << 8) | b; - image.setRGB(x, y, rgb); - } - } - } else if (components == 1) { - // 单通道格式 - 垂直翻转 - for (int y = 0; y < height; y++) { - int srcY = height - 1 - y; // 翻转Y坐标 - for (int x = 0; x < width; x++) { - int srcIndex = srcY * width + x; - int gray = pixelData[srcIndex] & 0xFF; - int rgb = (gray << 16) | (gray << 8) | gray; - image.setRGB(x, y, rgb); - } - } - } - - return image; - - } catch (Exception e) { - System.err.println("转换纹理到BufferedImage失败: " + texture.getName() + " - " + e.getMessage()); - e.printStackTrace(); - return null; - } - } - - private BufferedImage createDefaultThumbnail() { - BufferedImage thumbnail = new BufferedImage(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = thumbnail.createGraphics(); - - g2d.setColor(new Color(60, 60, 60)); - g2d.fillRect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT); - - g2d.setColor(Color.GRAY); - g2d.drawRect(2, 2, THUMBNAIL_WIDTH - 5, THUMBNAIL_HEIGHT - 5); - - g2d.setColor(Color.WHITE); - g2d.drawString("?", THUMBNAIL_WIDTH/2 - 4, THUMBNAIL_HEIGHT/2 + 4); - - g2d.dispose(); - return thumbnail; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ToolManagement.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ToolManagement.java deleted file mode 100644 index 6af39d2..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ToolManagement.java +++ /dev/null @@ -1,394 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.manager; - -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.awt.tools.SelectionTool; -import com.chuangzhou.vivid2D.render.awt.tools.Tool; -import com.chuangzhou.vivid2D.render.model.util.manager.RanderToolsManager; -import com.chuangzhou.vivid2D.render.model.util.tools.RanderTools; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.awt.event.MouseEvent; -import java.util.*; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; // 用于线程安全的监听器列表 - -/** - * 工具管理器 - * 负责注册、管理和切换各种编辑工具 - */ -public class ToolManagement { - private static final Logger logger = LoggerFactory.getLogger(ToolManagement.class); - - private final ModelRenderPanel renderPanel; - private final Map registeredTools; - private final RanderToolsManager randerToolsManager; - private Tool currentTool = null; - private Tool previousTool = null; - - // 【新增】工具切换监听器列表 - private final List listeners; - - // 默认工具(选择工具) - private final Tool defaultTool; - - public ToolManagement(ModelRenderPanel renderPanel, RanderToolsManager randerToolsManager) { - this.renderPanel = renderPanel; - this.registeredTools = new ConcurrentHashMap<>(); - this.randerToolsManager = randerToolsManager; - // 【新增】初始化监听器列表 - this.listeners = new CopyOnWriteArrayList<>(); - - // 创建默认选择工具 - this.defaultTool = new SelectionTool(renderPanel); - registerTool(defaultTool); - - // 设置默认工具为当前工具 - switchTool(defaultTool.getToolName()); - } - - // 【新增】工具切换监听器接口 - /** - * 工具切换监听器接口 - */ - public interface ToolChangeListener { - /** - * 当当前工具发生变化时调用 - * @param newTool 切换后的新工具 - */ - void onToolChanged(Tool newTool); - } - - // 【新增】添加监听器方法 - /** - * 注册工具切换监听器 - */ - public void addToolChangeListener(ToolChangeListener listener) { - if (listener != null) { - listeners.add(listener); - } - } - - // 【新增】移除监听器方法 - /** - * 移除工具切换监听器 - */ - public void removeToolChangeListener(ToolChangeListener listener) { - if (listener != null) { - listeners.remove(listener); - } - } - - // 【新增】触发监听器方法 - /** - * 触发所有监听器的 onToolChanged 方法 - */ - private void fireToolChanged(Tool newTool) { - for (ToolChangeListener listener : listeners) { - try { - listener.onToolChanged(newTool); - } catch (Exception e) { - logger.error("工具切换监听器回调失败: {}", e.getMessage(), e); - } - } - } - - // ================== 工具注册管理 ================== - - /** - * 注册工具 - */ - public void registerTool(Tool tool, RanderTools randerTools) { - if (tool == null) { - logger.warn("尝试注册空工具"); - return; - } - - String toolName = tool.getToolName(); - if (registeredTools.containsKey(toolName)) { - logger.warn("工具已存在: {}", toolName); - return; - } - registeredTools.put(toolName, tool); - randerToolsManager.bindToolWithRanderTools(tool, randerTools); - tool.setAssociatedRanderTools(randerTools); - logger.info("注册工具: {}", toolName); - } - - /** - * 注册工具 - */ - public void registerTool(Tool tool) { - if (tool == null) { - logger.warn("尝试注册空工具"); - return; - } - - String toolName = tool.getToolName(); - if (registeredTools.containsKey(toolName)) { - logger.warn("工具已存在: {}", toolName); - return; - } - - registeredTools.put(toolName, tool); - logger.info("注册工具: {}", toolName); - } - - /** - * 注销工具 - */ - public void unregisterTool(String toolName) { - Tool tool = registeredTools.get(toolName); - if (tool == null) { - logger.warn("工具不存在: {}", toolName); - return; - } - - // 如果要注销的工具是当前工具,先停用它 - if (currentTool == tool) { - switchToDefaultTool(); - } - - tool.dispose(); - registeredTools.remove(toolName); - logger.info("注销工具: {}", toolName); - } - - /** - * 获取所有注册的工具 - */ - public List getRegisteredTools() { - return new ArrayList<>(registeredTools.values()); - } - - /** - * 根据名称获取工具 - */ - public Tool getTool(String toolName) { - return registeredTools.get(toolName); - } - - // ================== 工具切换管理 ================== - - /** - * 切换到指定工具 - */ - public boolean switchTool(String toolName) { - Tool targetTool = registeredTools.get(toolName); - if (targetTool == null) { - logger.warn("工具不存在: {}", toolName); - return false; - } - - return switchTool(targetTool); - } - - /** - * 切换到指定工具实例 - */ - public boolean switchTool(Tool tool) { - if (tool == null) { - logger.warn("尝试切换到空工具"); - return false; - } - - // 检查工具是否可用 - if (!tool.isAvailable()) { - logger.warn("工具不可用: {}", tool.getToolName()); - return false; - } - - // 如果已经是当前工具,直接返回 - if (currentTool == tool) { - return true; - } - - // 停用当前工具 - if (currentTool != null) { - currentTool.deactivate(); - previousTool = currentTool; - } - - // 激活新工具 - currentTool = tool; - currentTool.activate(); - - // 更新光标 - updateCursor(); - - logger.info("切换到工具: {}", currentTool.getToolName()); - - // 【新增】触发工具切换回调 - fireToolChanged(currentTool); - - return true; - } - - /** - * 切换到默认工具 - */ - public void switchToDefaultTool() { - switchTool(defaultTool); - } - - /** - * 切换到上一个工具 - */ - public void switchToPreviousTool() { - if (previousTool != null && previousTool.isAvailable()) { - switchTool(previousTool); - } else { - switchToDefaultTool(); - } - } - - /** - * 获取当前工具 - */ - public Tool getCurrentTool() { - return currentTool; - } - - // 【新增】提供 getActiveTool() 方法以解决 MainWindow 中的编译问题 - /** - * 获取当前活动的工具(与 getCurrentTool 相同,提供别名以提高兼容性) - */ - public Tool getActiveTool() { - return currentTool; - } - - /** - * 获取上一个工具 - */ - public Tool getPreviousTool() { - return previousTool; - } - - /** - * 获取默认工具 - */ - public Tool getDefaultTool() { - return defaultTool; - } - - // ================== 事件转发 ================== - - /** - * 处理鼠标按下事件 - */ - public void handleMousePressed(MouseEvent e, float modelX, float modelY) { - if (currentTool != null) { - currentTool.onMousePressed(e, modelX, modelY); - } - } - - /** - * 处理鼠标释放事件 - */ - public void handleMouseReleased(MouseEvent e, float modelX, float modelY) { - if (currentTool != null) { - currentTool.onMouseReleased(e, modelX, modelY); - } - } - - /** - * 处理鼠标拖拽事件 - */ - public void handleMouseDragged(MouseEvent e, float modelX, float modelY) { - if (currentTool != null) { - currentTool.onMouseDragged(e, modelX, modelY); - } - } - - /** - * 处理鼠标移动事件 - */ - public void handleMouseMoved(MouseEvent e, float modelX, float modelY) { - if (currentTool != null) { - currentTool.onMouseMoved(e, modelX, modelY); - } - } - - /** - * 处理鼠标点击事件 - */ - public void handleMouseClicked(MouseEvent e, float modelX, float modelY) { - if (currentTool != null) { - currentTool.onMouseClicked(e, modelX, modelY); - } - } - - /** - * 处理鼠标双击事件 - */ - public void handleMouseDoubleClicked(MouseEvent e, float modelX, float modelY) { - if (currentTool != null) { - currentTool.onMouseDoubleClicked(e, modelX, modelY); - } - } - - // ================== 工具状态管理 ================== - - /** - * 更新光标 - */ - private void updateCursor() { - if (currentTool != null) { - Cursor cursor = currentTool.getToolCursor(); - if (cursor != null) { - renderPanel.setCursor(cursor); - } - } - } - - /** - * 检查是否有工具处于激活状态 - */ - public boolean hasActiveTool() { - return currentTool != null && currentTool.isActive(); - } - - /** - * 停用所有工具 - */ - public void deactivateAllTools() { - for (Tool tool : registeredTools.values()) { - if (tool.isActive()) { - tool.deactivate(); - } - } - currentTool = null; - renderPanel.setCursor(Cursor.getDefaultCursor()); - } - - /** - * 清理所有工具资源 - */ - public void dispose() { - deactivateAllTools(); - for (Tool tool : registeredTools.values()) { - tool.dispose(); - } - registeredTools.clear(); - listeners.clear(); // 【新增】清理监听器 - logger.info("工具管理器已清理"); - } - - /** - * 获取工具统计信息 - */ - public String getToolStatistics() { - int activeCount = 0; - for (Tool tool : registeredTools.values()) { - if (tool.isActive()) { - activeCount++; - } - } - - return String.format("工具统计: 注册%d个, 激活%d个, 当前工具: %s", - registeredTools.size(), activeCount, - currentTool != null ? currentTool.getToolName() : "无"); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/WorldManagement.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/WorldManagement.java deleted file mode 100644 index 5392984..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/WorldManagement.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.manager; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import org.joml.Vector2f; - -public class WorldManagement { - private final ModelRenderPanel modelRenderPanel; - private final GLContextManager glContextManager; - - public WorldManagement(ModelRenderPanel modelRenderPanel, GLContextManager glContextManager) { - this.modelRenderPanel = modelRenderPanel; - this.glContextManager = glContextManager; - } - - /** - * 将屏幕坐标转换为模型坐标 - */ - public float[] screenToModelCoordinates(int screenX, int screenY) { - if (!glContextManager.isContextInitialized() || glContextManager.getWidth() <= 0 || glContextManager.getHeight() <= 0) return null; - float glX = (float) screenX * glContextManager.getWidth() / modelRenderPanel.getWidth(); - float glY = (float) screenY * glContextManager.getHeight() / modelRenderPanel.getHeight(); - float ndcX = (2.0f * glX) / glContextManager.getWidth() - 1.0f; - float ndcY = 1.0f - (2.0f * glY) / glContextManager.getHeight(); - Vector2f camOffset = ModelRender.getCameraOffset(); - float zoom = ModelRender.getCamera().getZoom(); - float modelX = (ndcX * glContextManager.getWidth() / (2.0f * zoom)) + camOffset.x; - float modelY = (ndcY * glContextManager.getHeight() / (-2.0f * zoom)) + camOffset.y; - return new float[]{modelX, modelY}; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/data/LayerOperationManagerData.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/data/LayerOperationManagerData.java deleted file mode 100644 index d473e22..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/data/LayerOperationManagerData.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.manager.data; - -import com.chuangzhou.vivid2D.render.awt.manager.LayerOperationManager; - -import java.io.Serial; -import java.io.Serializable; -import java.util.List; - -public class LayerOperationManagerData implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - - public List layerMetadata; - public LayerOperationManagerData(List layerMetadata) { - this.layerMetadata = layerMetadata; - } - - public LayerOperationManagerData() {} -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/data/ParametersManagementData.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/data/ParametersManagementData.java deleted file mode 100644 index 061ea33..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/data/ParametersManagementData.java +++ /dev/null @@ -1,246 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.manager.data; - -import com.chuangzhou.vivid2D.render.awt.ParametersPanel; -import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement; -import com.chuangzhou.vivid2D.render.model.AnimationParameter; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.data.ParameterData; -import com.chuangzhou.vivid2D.render.model.data.PartData; -import com.chuangzhou.vivid2D.render.model.data.VertexData; -import com.chuangzhou.vivid2D.render.model.util.Vertex; // 导入 Vertex -import org.joml.Vector2f; // 导入 Vector2f - -import java.io.Serial; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * ParametersManagement 的序列化数据类 - * 修复了直接序列化 Vertex 运行时对象导致的问题 - */ -public class ParametersManagementData implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - - public List oldValues; - public boolean isBreakage; - - public ParametersManagementData(boolean isBreakage) { - this.oldValues = new ArrayList<>(); - this.isBreakage = isBreakage; - } - - public ParametersManagementData() { - this.oldValues = new ArrayList<>(); - this.isBreakage = false; - } - - public ParametersManagementData(ParametersManagement management) { - this(); - if (management != null) { - for (ParametersManagement.Parameter param : management.oldValues) { - ManagementParameterRecord paramRecord = new ManagementParameterRecord(param); - this.oldValues.add(paramRecord); - } - } - } - - public ParametersManagement toParametersManagement(ParametersPanel parametersPanel) { - return toParametersManagement(parametersPanel, null); - } - - public ParametersManagement toParametersManagement(ParametersPanel parametersPanel, List modelParts) { - ParametersManagement management = new ParametersManagement(parametersPanel); - - if (this.oldValues != null) { - for (ManagementParameterRecord paramRecord : this.oldValues) { - ParametersManagement.Parameter param = paramRecord.toParameter(modelParts); - management.oldValues.add(param); - } - } - - return management; - } - - public ParametersManagementData copy() { - ParametersManagementData copy = new ParametersManagementData(); - copy.oldValues = new ArrayList<>(); - copy.isBreakage = this.isBreakage; - if (this.oldValues != null) { - for (ManagementParameterRecord paramRecord : this.oldValues) { - copy.oldValues.add(paramRecord.copy()); - } - } - return copy; - } - - // ==================== 内部类 ==================== - - public static class ManagementParameterRecord implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - - public String modelPartName; - public PartData modelPartData; - public List animationParameters; - public List paramIds; - public List values; // 这里将存储 SerializableVertex 而不是 Vertex - public List keyframes; - public List isKeyframes; - - public ManagementParameterRecord() { - this.animationParameters = new ArrayList<>(); - this.paramIds = new ArrayList<>(); - this.values = new ArrayList<>(); - this.keyframes = new ArrayList<>(); - this.isKeyframes = new ArrayList<>(); - } - - public ManagementParameterRecord(ParametersManagement.Parameter parameter) { - this(); - if (parameter.modelPart() != null) { - this.modelPartName = parameter.modelPart().getName(); - this.modelPartData = new PartData(parameter.modelPart()); - } - - if (parameter.animationParameter() != null) { - for (AnimationParameter animParam : parameter.animationParameter()) { - this.animationParameters.add(new ParameterData(animParam)); - } - } - - if (parameter.paramId() != null) { - this.paramIds.addAll(parameter.paramId()); - } - - // [核心修复]:深拷贝 values,并检查是否包含 Vertex 对象 - if (parameter.value() != null) { - for (Object val : parameter.value()) { - this.values.add(convertValueForSerialization(val)); - } - } - - if (parameter.keyframe() != null) { - this.keyframes.addAll(parameter.keyframe()); - } - if (parameter.isKeyframe() != null) { - this.isKeyframes.addAll(parameter.isKeyframe()); - } - } - - /** - * 将运行时对象转换为可序列化对象 - */ - private Object convertValueForSerialization(Object val) { - // 检查是否是包含 Vertex 的 Map (对应 "meshVertices" 参数) - if (val instanceof Map) { - Map originalMap = (Map) val; - // 浅拷贝 Map 结构 - Map newMap = new HashMap<>(); - for (Map.Entry entry : originalMap.entrySet()) { - String key = String.valueOf(entry.getKey()); - Object value = entry.getValue(); - - if ("Vertex".equals(key) && value instanceof Vertex) { - // 将 Vertex 转换为 DTO - newMap.put(key, new VertexData((Vertex) value)); - } else { - // 递归处理还是直接存放? 目前直接存放,假设其他都是基本类型或String - newMap.put(key, value); - } - } - return newMap; - } - return val; - } - - /** - * 将序列化对象还原为运行时对象 - */ - private Object convertValueFromSerialization(Object val) { - if (val instanceof Map) { - Map storedMap = (Map) val; - Map runtimeMap = new HashMap<>(); - for (Map.Entry entry : storedMap.entrySet()) { - String key = String.valueOf(entry.getKey()); - Object value = entry.getValue(); - - if ("Vertex".equals(key) && value instanceof VertexData) { - // 将 DTO 还原为 Vertex - runtimeMap.put(key, ((VertexData) value).toVertex()); - } else { - runtimeMap.put(key, value); - } - } - return runtimeMap; - } - return val; - } - - public ParametersManagement.Parameter toParameter(List modelParts) { - ModelPart modelPart = null; - - if (this.modelPartName != null && modelParts != null) { - for (ModelPart part : modelParts) { - if (this.modelPartName.equals(part.getName())) { - modelPart = part; - break; - } - } - } - if (modelPart == null && this.modelPartData != null) { - try { - modelPart = this.modelPartData.toModelPart(new HashMap<>()); - } catch (Exception ignored) {} - } - - List animParams = new ArrayList<>(); - if (this.animationParameters != null) { - for (ParameterData p : this.animationParameters) { - animParams.add(p.toAnimationParameter()); - } - } - - // [核心修复]:还原 values 中的 Vertex 对象 - List runtimeValues = new ArrayList<>(); - if (this.values != null) { - for (Object val : this.values) { - runtimeValues.add(convertValueFromSerialization(val)); - } - } - - return new ParametersManagement.Parameter( - modelPart, - animParams, - this.paramIds != null ? new ArrayList<>(this.paramIds) : new ArrayList<>(), - runtimeValues, - this.keyframes != null ? new ArrayList<>(this.keyframes) : new ArrayList<>(), - this.isKeyframes != null ? new ArrayList<>(this.isKeyframes) : new ArrayList<>() - ); - } - - public ManagementParameterRecord copy() { - ManagementParameterRecord copy = new ManagementParameterRecord(); - copy.modelPartName = this.modelPartName; - copy.modelPartData = this.modelPartData != null ? this.modelPartData.copy() : null; - - copy.animationParameters = new ArrayList<>(); - if (this.animationParameters != null) { - for (ParameterData p : this.animationParameters) copy.animationParameters.add(p.copy()); - } - - copy.paramIds = this.paramIds != null ? new ArrayList<>(this.paramIds) : new ArrayList<>(); - // 注意:values 里的 SerializableVertex 也应该 copy,或者依赖其不可变性/序列化特性 - // 这里做简单的列表拷贝,因为 SerializableVertex 通常是纯数据 - copy.values = this.values != null ? new ArrayList<>(this.values) : new ArrayList<>(); - - copy.keyframes = this.keyframes != null ? new ArrayList<>(this.keyframes) : new ArrayList<>(); - copy.isKeyframes = this.isKeyframes != null ? new ArrayList<>(this.isKeyframes) : new ArrayList<>(); - - return copy; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java deleted file mode 100644 index 8f88370..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java +++ /dev/null @@ -1,1262 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.tools; - -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.awt.manager.CameraManagement; -import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.BoundingBox; -import com.chuangzhou.vivid2D.render.model.util.Vertex; -import org.joml.Vector2f; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; -import java.util.*; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 选择工具(默认工具) - * 用于选择和管理网格 - */ -public class SelectionTool extends Tool { - private static final Logger logger = LoggerFactory.getLogger(SelectionTool.class); - // 选择工具专用字段 - private volatile Mesh2D hoveredMesh = null; - private final Set selectedMeshes = Collections.newSetFromMap(new ConcurrentHashMap<>()); - private final List callQueue = new LinkedList<>(); - private volatile Mesh2D lastSelectedMesh = null; - private volatile ModelPart draggedPart = null; - private volatile float dragStartX, dragStartY; - - // 拖拽相关字段 - private volatile ModelRenderPanel.DragMode currentDragMode = ModelRenderPanel.DragMode.NONE; - private volatile boolean shiftDuringDrag = false; - private volatile float rotationStartAngle = 0.0f; - private volatile float resizeStartWidth, resizeStartHeight; - - // 状态记录 - private final Map dragStartPositions = new HashMap<>(); - private final Map dragStartScales = new HashMap<>(); - private final Map dragStartRotations = new HashMap<>(); - private final Map dragStartPivots = new HashMap<>(); - private volatile Cursor rotationCursor = null; - private volatile float resizeAnchorX = 0.0f; - private volatile float resizeAnchorY = 0.0f; - private static final int ROTATION_CURSOR_PREFERRED_SIZE = 32; - private static final float MIN_RESIZE_PIXEL_DIM = 6.0f; // 最小像素尺寸,避免非常小的初始宽度导致放大失控 - private static final float MIN_REL_SCALE = 0.01f; // 最小相对缩放,防止接近或等于 0 - private static final float MAX_REL_SCALE = 100.0f; // 最大相对缩放,防止一两像素拖动导致极端放大 - - public SelectionTool(ModelRenderPanel renderPanel) { - super(renderPanel, "选择工具", "选择和操作网格对象"); - - // 初始化自定义旋转光标(现代风格的圆弧箭头) - try { - Toolkit tk = Toolkit.getDefaultToolkit(); - Dimension best = tk.getBestCursorSize(ROTATION_CURSOR_PREFERRED_SIZE, ROTATION_CURSOR_PREFERRED_SIZE); - int w = (best.width > 0 ? best.width : ROTATION_CURSOR_PREFERRED_SIZE); - int h = (best.height > 0 ? best.height : ROTATION_CURSOR_PREFERRED_SIZE); - - BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = img.createGraphics(); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g.setComposite(AlphaComposite.Src); - - // 缩小比例(让箭头与弧变小) - float scale = 0.5f; - - int cx = w / 2; - int cy = h / 2; - int baseRadius = Math.min(w, h) / 2 - 4; - int radius = Math.max(6, (int) (baseRadius * scale)); - - // 外层白色弧(较粗)——制造白色描边效果 - float outerStroke = Math.max(2f, w / 10f * scale); - g.setStroke(new BasicStroke(outerStroke, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); - g.setColor(Color.WHITE); - g.drawArc(cx - radius - 2, cy - radius - 2, (radius + 2) * 2, (radius + 2) * 2, 45, 270); - - // 内层黑色弧(覆盖在白弧之上) - float innerStroke = Math.max(1.5f, outerStroke * 0.6f); - g.setStroke(new BasicStroke(innerStroke, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); - g.setColor(Color.BLACK); - g.drawArc(cx - radius, cy - radius, radius * 2, radius * 2, 45, 270); - - // 箭头(缩小、黑色填充、白色描边) - double angle = Math.toRadians(45); - int ax = cx + (int) (Math.cos(angle) * radius); - int ay = cy - (int) (Math.sin(angle) * radius); - - int ah = Math.max(4, (int) (w / 12.0 * scale)); - Polygon arrow = new Polygon(); - arrow.addPoint(ax, ay); - arrow.addPoint(ax - ah, ay + ah + 1); - arrow.addPoint(ax + ah, ay + ah / 2); - - // 填充黑色 - g.setColor(Color.BLACK); - g.fill(arrow); - - // 白色描边(细) - g.setStroke(new BasicStroke(Math.max(1f, innerStroke * 0.4f), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); - g.setColor(Color.WHITE); - g.draw(arrow); - - g.dispose(); - - rotationCursor = tk.createCustomCursor(img, new Point(cx, cy), "rotationCursor"); - } catch (Throwable t) { - rotationCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); - } - } - - - @Override - public void activate() { - isActive = true; - // 激活时恢复默认光标 - renderPanel.setCursor(Cursor.getDefaultCursor()); - } - - @Override - public void deactivate() { - isActive = false; - clearSelectedMeshes(); - } - - public void addCall(Call call) { - callQueue.add(call); - } - - public void removeCall(Call call) { - callQueue.remove(call); - } - - private void runCall(List meshes) { - for (Call call : callQueue) { - call.call(meshes); - } - } - - @Override - public void onMousePressed(MouseEvent e, float modelX, float modelY) { - if (!renderPanel.getGlContextManager().isContextInitialized()) return; - - shiftDuringDrag = e.isShiftDown(); - - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - // 清空之前的状态记录 - dragStartPositions.clear(); - dragStartScales.clear(); - dragStartRotations.clear(); - dragStartPivots.clear(); - - // 首先检查是否点击了选择框的调整手柄 - // 多选时只对最后一个选中的网格进行操作 - Mesh2D targetMeshForHandle = selectedMeshes.isEmpty() ? null : lastSelectedMesh; - ModelRenderPanel.DragMode dragMode = targetMeshForHandle != null ? - checkResizeHandleHit(modelX, modelY, targetMeshForHandle) : ModelRenderPanel.DragMode.NONE; - - if (dragMode == ModelRenderPanel.DragMode.ROTATE) { - // 开始旋转 - 记录初始旋转状态 - List selectedParts = getSelectedParts(); - for (ModelPart part : selectedParts) { - dragStartRotations.put(part, part.getRotation()); - } - currentDragMode = ModelRenderPanel.DragMode.ROTATE; - dragStartX = modelX; - dragStartY = modelY; - - // 获取边界框和中心点 - // 确保使用世界坐标下的边界框 - BoundingBox bounds = targetMeshForHandle.getBounds(); - renderPanel.getCameraManagement().getRotationCenter().set( - (bounds.getMinX() + bounds.getMaxX()) / 2.0f, - (bounds.getMinY() + bounds.getMaxY()) / 2.0f - ); - - // 计算初始角度 - rotationStartAngle = (float) Math.atan2( - dragStartY - renderPanel.getCameraManagement().getRotationCenter().y, - dragStartX - renderPanel.getCameraManagement().getRotationCenter().x - ); - - } else if (dragMode == ModelRenderPanel.DragMode.MOVE_PIVOT && targetMeshForHandle != null) { - // 开始移动中心点 - 记录初始中心点状态 - List selectedParts = getSelectedParts(); - for (ModelPart part : selectedParts) { - dragStartPivots.put(part, new Vector2f(part.getPivot())); - } - currentDragMode = ModelRenderPanel.DragMode.MOVE_PIVOT; - dragStartX = modelX; - dragStartY = modelY; - - // 记录初始中心点位置 - // 确保使用世界坐标下的边界框 - BoundingBox bounds = targetMeshForHandle.getBounds(); - renderPanel.getCameraManagement().getRotationCenter().set( - (bounds.getMinX() + bounds.getMaxX()) / 2.0f, - (bounds.getMinY() + bounds.getMaxY()) / 2.0f - ); - } else if (dragMode != ModelRenderPanel.DragMode.NONE) { - // 开始调整大小 - 记录初始缩放状态,同时记录初始位置与中心点,计算缩放锚点 - List selectedParts = getSelectedParts(); - for (ModelPart part : selectedParts) { - dragStartScales.put(part, new Vector2f(part.getScale())); - dragStartPositions.put(part, new Vector2f(part.getPosition())); // 新增:记录位置 - dragStartPivots.put(part, new Vector2f(part.getPivot())); // 新增:记录中心点 - } - currentDragMode = dragMode; - dragStartX = modelX; - dragStartY = modelY; - - // 确保使用世界坐标下的边界框 - BoundingBox bounds = targetMeshForHandle.getBounds(); - resizeStartWidth = bounds.getWidth(); - resizeStartHeight = bounds.getHeight(); - - // 计算缩放锚点(根据不同手柄,锚点为对侧固定点) - switch (dragMode) { - case RESIZE_LEFT: - resizeAnchorX = bounds.getMaxX(); - resizeAnchorY = (bounds.getMinY() + bounds.getMaxY()) / 2.0f; - break; - case RESIZE_RIGHT: - resizeAnchorX = bounds.getMinX(); - resizeAnchorY = (bounds.getMinY() + bounds.getMaxY()) / 2.0f; - break; - case RESIZE_TOP: - resizeAnchorY = bounds.getMaxY(); - resizeAnchorX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f; - break; - case RESIZE_BOTTOM: - resizeAnchorY = bounds.getMinY(); - resizeAnchorX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f; - break; - case RESIZE_TOP_LEFT: - resizeAnchorX = bounds.getMaxX(); - resizeAnchorY = bounds.getMaxY(); - break; - case RESIZE_TOP_RIGHT: - resizeAnchorX = bounds.getMinX(); - resizeAnchorY = bounds.getMaxY(); - break; - case RESIZE_BOTTOM_LEFT: - resizeAnchorX = bounds.getMaxX(); - resizeAnchorY = bounds.getMinY(); - break; - case RESIZE_BOTTOM_RIGHT: - resizeAnchorX = bounds.getMinX(); - resizeAnchorY = bounds.getMinY(); - break; - default: - // 保持默认(中心缩放) - resizeAnchorX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f; - resizeAnchorY = (bounds.getMinY() + bounds.getMaxY()) / 2.0f; - break; - } - } else { - // 检查是否点击了网格(移动操作) - Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY); - - if (clickedMesh != null) { - handleMultiSelect(clickedMesh, e.isShiftDown(), e.isControlDown()); - - // 记录初始位置状态 - List selectedParts = getSelectedParts(); - for (ModelPart part : selectedParts) { - dragStartPositions.put(part, new Vector2f(part.getPosition())); - } - - // 设置拖拽目标 - draggedPart = findPartByMesh(clickedMesh); - dragStartX = modelX; - dragStartY = modelY; - currentDragMode = ModelRenderPanel.DragMode.MOVE; - } else { - // 点击空白区域 - if (!e.isControlDown() && !e.isShiftDown()) { - clearSelectedMeshes(); - } - } - } - - // 更新拖拽过程中的光标 - updateCursorForDragMode(currentDragMode); - - } catch (Exception ex) { - logger.error("选择工具处理鼠标按下时出错", ex); - } - }); - } - - @Override - public void onMouseReleased(MouseEvent e, float modelX, float modelY) { - if (currentDragMode != ModelRenderPanel.DragMode.NONE) { - // 记录操作历史 - try { - List selectedParts = getSelectedParts(); - switch (currentDragMode) { - case MOVE: - if (!dragStartPositions.isEmpty() && !selectedParts.isEmpty()) { - renderPanel.getStatusRecordManagement().recordDragEnd( - selectedParts, new HashMap<>(dragStartPositions) - ); - } - break; - case ROTATE: - if (!dragStartRotations.isEmpty() && !selectedParts.isEmpty()) { - renderPanel.getStatusRecordManagement().recordRotateEnd( - selectedParts, new HashMap<>(dragStartRotations) - ); - } - break; - case MOVE_PIVOT: - if (!dragStartPivots.isEmpty() && !selectedParts.isEmpty()) { - renderPanel.getStatusRecordManagement().recordMovePivotEnd( - selectedParts, new HashMap<>(dragStartPivots) - ); - } - break; - default: - if (!dragStartScales.isEmpty() && !selectedParts.isEmpty()) { - renderPanel.getStatusRecordManagement().recordResizeEnd( - selectedParts, new HashMap<>(dragStartScales) - ); - } - break; - } - } catch (Exception ex) { - logger.error("选择工具记录操作历史时出错", ex); - } - } - - // 重置状态 - draggedPart = null; - currentDragMode = ModelRenderPanel.DragMode.NONE; - shiftDuringDrag = false; - - // 清空状态记录 - dragStartPositions.clear(); - dragStartScales.clear(); - dragStartRotations.clear(); - dragStartPivots.clear(); - - // 恢复悬停状态的光标 - updateCursorForHoverState(modelX, modelY); - } - - @Override - public void onMouseDragged(MouseEvent e, float modelX, float modelY) { - if (currentDragMode == ModelRenderPanel.DragMode.NONE) return; - - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - switch (currentDragMode) { - case MOVE: - handleMoveDrag(modelX, modelY); - break; - case ROTATE: - handleRotateDrag(modelX, modelY); - break; - case MOVE_PIVOT: - handleMovePivotDrag(modelX, modelY); - break; - default: - handleResizeDrag(modelX, modelY); - break; - } - } catch (Exception ex) { - logger.error("选择工具处理鼠标拖拽时出错", ex); - } - }); - } - - @Override - public void onMouseMoved(MouseEvent e, float modelX, float modelY) { - if (!renderPanel.getGlContextManager().isContextInitialized()) return; - - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - // 检测悬停的网格 - Mesh2D newHoveredMesh = findMeshAtPosition(modelX, modelY); - - // 如果悬停网格发生变化,更新状态 - if (newHoveredMesh != hoveredMesh) { - if (hoveredMesh != null) { - hoveredMesh.setSuspension(false); - hoveredMesh.setRenderVertices(false); - } - - hoveredMesh = newHoveredMesh; - if (hoveredMesh != null) { - hoveredMesh.setSuspension(true); - } - } - - updateCursorForHoverState(modelX, modelY, newHoveredMesh); - - } catch (Exception ex) { - logger.error("选择工具处理鼠标移动时出错", ex); - } - }); - } - - @Override - public void onMouseClicked(MouseEvent e, float modelX, float modelY) { - } - - @Override - public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - // 检测双击的网格 - Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY); - if (clickedMesh != null) { - enterLiquifyMode(clickedMesh); - } - } catch (Exception ex) { - logger.error("选择工具处理双击时出错", ex); - } - }); - } - - @Override - public Cursor getToolCursor() { - return Cursor.getDefaultCursor(); - } - - // ================== 选择工具专用方法 ================== - - /** - * 处理移动拖拽 - */ - private void handleMoveDrag(float modelX, float modelY) { - if (selectedMeshes.isEmpty()) return; - - float deltaX = modelX - dragStartX; - float deltaY = modelY - dragStartY; - List selectedParts = getSelectedParts(); - for (ModelPart part : selectedParts) { - Vector2f startPos = dragStartPositions.getOrDefault(part, new Vector2f(part.getPosition())); - float newX = startPos.x + deltaX; - float newY = startPos.y + deltaY; - part.setPosition(newX, newY); - renderPanel.getParametersManagement().broadcast(part, "position", List.of(newX, newY)); - updateMeshVertices(); - } - dragStartX = modelX; - dragStartY = modelY; - } - - /** - * 处理旋转拖拽 - */ - private void handleRotateDrag(float modelX, float modelY) { - if (lastSelectedMesh == null) return; - float currentAngle = (float) Math.atan2( - modelY - renderPanel.getCameraManagement().getRotationCenter().y, - modelX - renderPanel.getCameraManagement().getRotationCenter().x - ); - float deltaAngle = currentAngle - rotationStartAngle; - if (renderPanel.getKeyboardManager().getIsShiftPressed() || shiftDuringDrag) { - float constraintStep = (float) (Math.PI / 12); // 15度 - deltaAngle = Math.round(deltaAngle / constraintStep) * constraintStep; - } - List selectedParts = getSelectedParts(); - for (ModelPart part : selectedParts) { - float startAngle = dragStartRotations.getOrDefault(part, part.getRotation()); - float targetAngle = startAngle + deltaAngle; - try { - part.getClass().getMethod("setRotation", float.class).invoke(part, targetAngle); - } catch (NoSuchMethodException nsme) { - float cur = part.getRotation(); - float delta = targetAngle - cur; - part.rotate(delta); - } catch (Exception ignored) { - part.rotate(deltaAngle); - } - renderPanel.getParametersManagement().broadcast(part, "rotate", part.getRotation()); - updateMeshVertices(); - } - rotationStartAngle = currentAngle; - } - - /** - * 处理移动中心点拖拽 - */ - private void handleMovePivotDrag(float modelX, float modelY) { - if (lastSelectedMesh == null) return; - - float deltaX = modelX - dragStartX; - float deltaY = modelY - dragStartY; - - // 只移动主选中网格的中心点 - ModelPart selectedPart = findPartByMesh(lastSelectedMesh); - if (selectedPart == null) return; - - Vector2f currentPivot = selectedPart.getPivot(); - float newPivotX = currentPivot.x + deltaX; - float newPivotY = currentPivot.y + deltaY; - renderPanel.getParametersManagement().broadcast(selectedPart, "pivot", List.of(newPivotX, newPivotY)); - updateMeshVertices(); - if (selectedPart.setPivot(newPivotX, newPivotY)) { - dragStartX = modelX; - dragStartY = modelY; - renderPanel.getCameraManagement().getRotationCenter().set(newPivotX, newPivotY); - } - } - - /** - * 替换 handleResizeDrag:基于“宽/高的绝对变化 = 初始宽高 ± 鼠标世界坐标位移”来计算新的宽高, - * 然后通过 newWidth/startWidth 得到相对缩放比。相比之前的算法: - * - 不做像素/相机缩放折算(直接用世界坐标),避免像素换算错误引起跳变; - * - 不在每帧更新 dragStartX/Y 或 startWidth/startHeight,避免指数式累积; - * - 对宽高做合理下限保护(采用初始尺寸的小比例)以避免除零/反向导致巨大缩放; - * - 同时保持以 resizeAnchor 为锚点调整部件位置,使视觉上“跟随”鼠标。 - */ - private void handleResizeDrag(float modelX, float modelY) { - if (lastSelectedMesh == null) return; - - // 鼠标相对按下时的世界坐标增量 - float deltaX = modelX - dragStartX; - float deltaY = modelY - dragStartY; - - // 保护初始边界,避免除以 0 - float startW = Math.max(resizeStartWidth, 1e-4f); - float startH = Math.max(resizeStartHeight, 1e-4f); - - // 计算新的宽高(世界坐标) - float newW = startW; - float newH = startH; - - switch (currentDragMode) { - case RESIZE_LEFT: - newW = startW - deltaX; - break; - case RESIZE_RIGHT: - newW = startW + deltaX; - break; - case RESIZE_TOP: - newH = startH - deltaY; - break; - case RESIZE_BOTTOM: - newH = startH + deltaY; - break; - case RESIZE_TOP_LEFT: - newW = startW - deltaX; - newH = startH - deltaY; - break; - case RESIZE_TOP_RIGHT: - newW = startW + deltaX; - newH = startH - deltaY; - break; - case RESIZE_BOTTOM_LEFT: - newW = startW - deltaX; - newH = startH + deltaY; - break; - case RESIZE_BOTTOM_RIGHT: - newW = startW + deltaX; - newH = startH + deltaY; - break; - default: - break; - } - - // 最小尺寸保护:使用按下时尺寸的 1% 或绝对小值,避免鼠标移动越过锚点导致 newW <= 0 从而放大异常 - float minWorldDim = Math.max(Math.min(startW, startH) * 0.01f, 1e-4f); - newW = Math.max(newW, minWorldDim); - newH = Math.max(newH, minWorldDim); - - // 计算相对缩放比(以按下时为基准) - float relScaleX = newW / startW; - float relScaleY = newH / startH; - - // 如果按住 Shift 做等比缩放,使用平均值 - if (renderPanel.getKeyboardManager().getIsShiftPressed() || shiftDuringDrag) { - float uniform = (relScaleX + relScaleY) * 0.5f; - relScaleX = uniform; - relScaleY = uniform; - } - - // 在应用到 ModelPart 上前再用上下限保护防止极端值 - relScaleX = Math.max(relScaleX, MIN_REL_SCALE); - relScaleY = Math.max(relScaleY, MIN_REL_SCALE); - relScaleX = Math.min(relScaleX, MAX_REL_SCALE); - relScaleY = Math.min(relScaleY, MAX_REL_SCALE); - - List selectedParts = getSelectedParts(); - if (selectedParts.isEmpty()) return; - - for (ModelPart part : selectedParts) { - Vector2f startScale = dragStartScales.getOrDefault(part, new Vector2f(part.getScale())); - Vector2f startPos = dragStartPositions.getOrDefault(part, new Vector2f(part.getPosition())); - - // 计算新的绝对缩放(基于按下时的 scale) - float newScaleX = startScale.x * relScaleX; - float newScaleY = startScale.y * relScaleY; - - // 保护缩放范围 - newScaleX = Math.max(newScaleX, MIN_REL_SCALE); - newScaleY = Math.max(newScaleY, MIN_REL_SCALE); - newScaleX = Math.min(newScaleX, startScale.x * MAX_REL_SCALE); - newScaleY = Math.min(newScaleY, startScale.y * MAX_REL_SCALE); - - // 使用 resizeAnchor 作为锚点,按比例变换位置(世界坐标) - float newPosX = resizeAnchorX + (startPos.x - resizeAnchorX) * relScaleX; - float newPosY = resizeAnchorY + (startPos.y - resizeAnchorY) * relScaleY; - - // 应用:先设置缩放再设置位置(以确保内部变换顺序保持一致) - part.setScale(newScaleX, newScaleY); - part.setPosition(newPosX, newPosY); - - // 广播同步参数 - renderPanel.getParametersManagement().broadcast(part, "scale", List.of(newScaleX, newScaleY)); - renderPanel.getParametersManagement().broadcast(part, "position", List.of(newPosX, newPosY)); - updateMeshVertices(); - } - - dragStartX = modelX; - dragStartY = modelY; - resizeStartWidth *= relScaleX; - resizeStartHeight *= relScaleY; - } - - - private Vector2f getMultiSelectionCenter() { - List selectedParts = getSelectedParts(); - if (selectedParts.isEmpty()) return new Vector2f(0, 0); - - float sumX = 0f; - float sumY = 0f; - - for (ModelPart part : selectedParts) { - Vector2f pos = part.getPosition(); - sumX += pos.x; - sumY += pos.y; - } - - return new Vector2f(sumX / selectedParts.size(), sumY / selectedParts.size()); - } - - /** - * 处理多选逻辑 - */ - private void handleMultiSelect(Mesh2D clickedMesh, boolean isShiftDown, boolean isCtrlDown) { - if (isCtrlDown) { - if (selectedMeshes.contains(clickedMesh)) { - removeSelectedMesh(clickedMesh); - } else { - addSelectedMesh(clickedMesh); - } - } else if (isShiftDown && lastSelectedMesh != null) { - selectRange(lastSelectedMesh, clickedMesh); - } else if (!isInMultiSelection()) { - if (!selectedMeshes.contains(clickedMesh)) { - setSelectedMesh(clickedMesh); - } - } - } - - /** - * 检查是否在选中的网格上 - */ - private ModelRenderPanel.DragMode checkResizeHandleHit(float modelX, float modelY, Mesh2D targetMesh) { - if (targetMesh == null) { - return ModelRenderPanel.DragMode.NONE; - } - - // 统一使用世界坐标边界框 - BoundingBox bounds; - Vector2f center; - - if (targetMesh.isInMultiSelection()) { - bounds = targetMesh.getMultiSelectionBounds(); - center = bounds.getCenter(); - } else { - bounds = targetMesh.getBounds(); - center = targetMesh.getPivot(); - } - - float scaleFactor = renderPanel.getCameraManagement().calculateScaleFactor(); - float borderThickness = ModelRenderPanel.BORDER_THICKNESS / scaleFactor; - float cornerSize = ModelRenderPanel.CORNER_SIZE / scaleFactor; - - float minX = bounds.getMinX(); - float minY = bounds.getMinY(); - float maxX = bounds.getMaxX(); - float maxY = bounds.getMaxY(); - - ModelRenderPanel.DragMode result = ModelRenderPanel.DragMode.NONE; - - // 检查中心点 - if (isPointInCenterHandle(modelX, modelY, center.x, center.y, cornerSize)) { - result = ModelRenderPanel.DragMode.MOVE_PIVOT; - } - - // 检查旋转手柄 - if (result == ModelRenderPanel.DragMode.NONE && - isPointInRotationHandle(modelX, modelY, center.x, minY, cornerSize, minX, maxX, scaleFactor)) { - result = ModelRenderPanel.DragMode.ROTATE; - } - - // 扩展边界以包含调整手柄区域 - float expandedMinX = minX - borderThickness; - float expandedMinY = minY - borderThickness; - float expandedMaxX = maxX + borderThickness; - float expandedMaxY = maxY + borderThickness; - - // 如果不在扩展边界内,直接返回NONE - if (result == ModelRenderPanel.DragMode.NONE) { - if (modelX < expandedMinX || modelX > expandedMaxX || - modelY < expandedMinY || modelY > expandedMaxY) { - return ModelRenderPanel.DragMode.NONE; - } - } - - // 检查角点 - if (result == ModelRenderPanel.DragMode.NONE && isPointInCorner(modelX, modelY, minX, minY, cornerSize)) { - result = ModelRenderPanel.DragMode.RESIZE_TOP_LEFT; - } - if (result == ModelRenderPanel.DragMode.NONE && isPointInCorner(modelX, modelY, maxX, minY, cornerSize)) { - result = ModelRenderPanel.DragMode.RESIZE_TOP_RIGHT; - } - if (result == ModelRenderPanel.DragMode.NONE && isPointInCorner(modelX, modelY, minX, maxY, cornerSize)) { - result = ModelRenderPanel.DragMode.RESIZE_BOTTOM_LEFT; - } - if (result == ModelRenderPanel.DragMode.NONE && isPointInCorner(modelX, modelY, maxX, maxY, cornerSize)) { - result = ModelRenderPanel.DragMode.RESIZE_BOTTOM_RIGHT; - } - - // 检查边 - if (result == ModelRenderPanel.DragMode.NONE) { - if (modelX >= minX - borderThickness && modelX <= minX + borderThickness) { - result = ModelRenderPanel.DragMode.RESIZE_LEFT; - } else if (modelX >= maxX - borderThickness && modelX <= maxX + borderThickness) { - result = ModelRenderPanel.DragMode.RESIZE_RIGHT; - } else if (modelY >= minY - borderThickness && modelY <= minY + borderThickness) { - result = ModelRenderPanel.DragMode.RESIZE_TOP; - } else if (modelY >= maxY - borderThickness && modelY <= maxY + borderThickness) { - result = ModelRenderPanel.DragMode.RESIZE_BOTTOM; - } - } - - logger.debug("手柄检测: 位置({}, {}), 边界[{}, {}, {}, {}], 结果: {}", - modelX, modelY, minX, minY, maxX, maxY, result); - - return result; - } - - - // 辅助方法:检查点是否在中心点、旋转手柄、角点区域内 - private boolean isPointInCenterHandle(float x, float y, float centerX, float centerY, float handleSize) { - return Math.abs(x - centerX) <= handleSize && Math.abs(y - centerY) <= handleSize; - } - - private boolean isPointInRotationHandle(float x, float y, float centerX, float topY, float handleSize, - float minX, float maxX, float scaleFactor) { - // 旋转手柄距离顶部一定像素(以世界坐标换算) - float rotationHandleY = topY - CameraManagement.ROTATION_HANDLE_DISTANCE / Math.max(1e-6f, scaleFactor); - - // 垂直带宽:以 handleSize 为基准,同时保证像素级的最小高度(提高可点击性) - float band = Math.max(handleSize * 1.2f, 8.0f / Math.max(1e-6f, scaleFactor)); - - // 水平方向:允许沿着选择框上方一段横向区域触发(比仅中心点更宽松) - float horizontalPadding = Math.max(handleSize * 2.0f, 6.0f / Math.max(1e-6f, scaleFactor)); - float left = minX - horizontalPadding; - float right = maxX + horizontalPadding; - - boolean inVerticalBand = (y >= rotationHandleY - band) && (y <= rotationHandleY + band); - boolean inHorizontalRange = (x >= left) && (x <= right); - - return inVerticalBand && inHorizontalRange; - } - - private boolean isPointInCorner(float x, float y, float cornerX, float cornerY, float cornerSize) { - return x >= cornerX - cornerSize && x <= cornerX + cornerSize && - y >= cornerY - cornerSize && y <= cornerY + cornerSize; - } - - /** - * 根据拖拽模式更新光标 - */ - private void updateCursorForDragMode(ModelRenderPanel.DragMode dragMode) { - Cursor newCursor = getCursorForDragMode(dragMode); - renderPanel.setCursor(newCursor); - } - - /** - * 根据拖拽模式获取对应的光标 - */ - private Cursor getCursorForDragMode(ModelRenderPanel.DragMode dragMode) { - switch (dragMode) { - case MOVE: - return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR); - case RESIZE_LEFT: - case RESIZE_RIGHT: - return Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR); - case RESIZE_TOP: - case RESIZE_BOTTOM: - return Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR); - case RESIZE_TOP_LEFT: - case RESIZE_BOTTOM_RIGHT: - return Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR); - case RESIZE_TOP_RIGHT: - case RESIZE_BOTTOM_LEFT: - return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR); - case ROTATE: - return (rotationCursor != null) ? rotationCursor : Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); - case MOVE_PIVOT: - return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); - case MOVE_PRIMARY_VERTEX: - case MOVE_PUPPET_PIN: - return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); - case NONE: - default: - return Cursor.getDefaultCursor(); - } - } - - /** - * 根据悬停状态更新光标 - */ - private void updateCursorForHoverState(float modelX, float modelY, Mesh2D hoveredMesh) { - // 如果正在拖拽,不更新光标 - if (currentDragMode != ModelRenderPanel.DragMode.NONE) { - return; - } - - Cursor newCursor = Cursor.getDefaultCursor(); - boolean isOverSelection = false; - - // 检查是否在选中的网格上 - if (!selectedMeshes.isEmpty()) { - Mesh2D targetMeshForHandle = lastSelectedMesh; - if (targetMeshForHandle != null) { - ModelRenderPanel.DragMode hoverMode = checkResizeHandleHit(modelX, modelY, targetMeshForHandle); - if (hoverMode != ModelRenderPanel.DragMode.NONE) { - newCursor = getCursorForDragMode(hoverMode); - isOverSelection = true; - } else { - // 检查是否在选中网格的边界框内 - BoundingBox bounds; - if (targetMeshForHandle.isInMultiSelection()) { - bounds = targetMeshForHandle.getMultiSelectionBounds(); - } else { - bounds = targetMeshForHandle.getBounds(); - } - - if (modelX >= bounds.getMinX() && modelX <= bounds.getMaxX() && - modelY >= bounds.getMinY() && modelY <= bounds.getMaxY()) { - newCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR); - isOverSelection = true; - } - } - } - } - - // 如果没有在选中的网格上,检查是否在可悬停的网格上 - if (!isOverSelection && hoveredMesh != null) { - newCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); - } - - // 更新光标 - renderPanel.setCursor(newCursor); - } - - private void updateCursorForHoverState(float modelX, float modelY) { - updateCursorForHoverState(modelX, modelY, hoveredMesh); - } - - // ================== 选择管理方法 ================== - - /** - * 获取当前选中的网格 - */ - public Mesh2D getSelectedMesh() { - return selectedMeshes.isEmpty() ? null : selectedMeshes.iterator().next(); - } - - /** - * 获取当前选中的所有网格 - */ - public Set getSelectedMeshes() { - return Collections.unmodifiableSet(selectedMeshes); - } - - /** - * 设置选中的网格(单选) - */ - public void setSelectedMesh(Mesh2D mesh) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - for (Mesh2D selectedMesh : selectedMeshes) { - selectedMesh.setSelected(false); - selectedMesh.clearMultiSelection(); - } - selectedMeshes.clear(); - if (mesh != null) { - mesh.setSelected(true); - runCall(List.of(mesh)); - selectedMeshes.add(mesh); - lastSelectedMesh = mesh; - updateMultiSelectionInMeshes(); - } else { - lastSelectedMesh = null; - } - }); - } - - /** - * 添加选中的网格(多选) - */ - public void addSelectedMesh(Mesh2D mesh) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - if (mesh != null && !selectedMeshes.contains(mesh)) { - mesh.setSelected(true); - selectedMeshes.add(mesh); - runCall(new ArrayList<>(selectedMeshes)); - lastSelectedMesh = mesh; - ModelPart part = findPartByMesh(mesh); - if (part != null) { - part.updateMeshVertices(); - } - updateMultiSelectionInMeshes(); - for (ModelPart selectedPart : getSelectedParts()) { - selectedPart.updateMeshVertices(); - } - } - }); - } - - /** - * 移除选中的网格 - */ - public void removeSelectedMesh(Mesh2D mesh) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - if (mesh != null && selectedMeshes.contains(mesh)) { - mesh.setSelected(false); - selectedMeshes.remove(mesh); - updateMultiSelectionInMeshes(); - - if (mesh == lastSelectedMesh) { - lastSelectedMesh = selectedMeshes.isEmpty() ? null : selectedMeshes.iterator().next(); - } - } - }); - } - - /** - * 清空所有选中的网格 - */ - public void clearSelectedMeshes() { - renderPanel.getGlContextManager().executeInGLContext(() -> { - // 记录所有受影响的 ModelPart,以便在清除选中状态后更新它们的网格顶点 - // Use a Set to collect unique ModelParts - Set affectedParts = new HashSet<>(); - - // 1. 清除网格的选中状态并收集父 ModelPart - for (Mesh2D mesh : selectedMeshes) { - mesh.setSelected(false); - mesh.setSuspension(false); - mesh.clearMultiSelection(); - - // 查找并记录父 ModelPart - ModelPart part = findPartByMesh(mesh); - if (part != null) { - affectedParts.add(part); - } - } - - // 2. 清除选择集 - selectedMeshes.clear(); - lastSelectedMesh = null; - - // 3. 强制更新所有受影响 ModelPart 的网格顶点。 - // 这将确保网格的渲染顶点(renderVertices)从 ModelPart 的世界变换中重新同步, - // 从而修复多选结束后位置重置的错误。 - for (ModelPart part : affectedParts) { - // 关键的修复:强制 ModelPart 重新同步其网格顶点,恢复正确的世界位置 - part.updateMeshVertices(); - } - }); - } - - /** - * 全选所有网格 - */ - public void selectAllMeshes() { - renderPanel.getGlContextManager().executeInGLContext(() -> { - Model2D model = renderPanel.getModel(); - if (model == null) return; - - // 1. 清除之前的选择 - for (Mesh2D mesh : selectedMeshes) { - mesh.setSelected(false); - mesh.clearMultiSelection(); - // 在清除前获取并更新 ModelPart 也是一个好习惯,确保状态一致性 - ModelPart part = findPartByMesh(mesh); - if (part != null) { - part.updateMeshVertices(); - } - } - selectedMeshes.clear(); - - // 2. 获取所有网格并选中 - List allMeshes = getAllMeshesFromModel(model); - for (Mesh2D mesh : allMeshes) { - if (mesh.isVisible()) { - mesh.setSelected(true); - selectedMeshes.add(mesh); - } - } - - // 3. 设置最后选中的网格 - if (!selectedMeshes.isEmpty()) { - lastSelectedMesh = selectedMeshes.iterator().next(); - } - - updateMultiSelectionInMeshes(); - - for (ModelPart selectedPart : getSelectedParts()) { - selectedPart.updateMeshVertices(); - } - }); - } - - /** - * 选择两个网格之间的所有网格 - */ - private void selectRange(Mesh2D fromMesh, Mesh2D toMesh) { - Model2D model = renderPanel.getModel(); - if (model == null) return; - - List allMeshes = getAllMeshesFromModel(model); - int fromIndex = allMeshes.indexOf(fromMesh); - int toIndex = allMeshes.indexOf(toMesh); - - if (fromIndex == -1 || toIndex == -1) { - setSelectedMesh(toMesh); - return; - } - - int start = Math.min(fromIndex, toIndex); - int end = Math.max(fromIndex, toIndex); - - for (int i = start; i <= end; i++) { - Mesh2D mesh = allMeshes.get(i); - if (mesh.isVisible()) { - mesh.setSelected(true); - selectedMeshes.add(mesh); - } - } - - lastSelectedMesh = toMesh; - updateMultiSelectionInMeshes(); - } - - /** - * 更新所有选中网格的多选列表 - */ - private void updateMultiSelectionInMeshes() { - if (selectedMeshes.size() <= 1) { - for (Mesh2D mesh : getAllMeshesFromModel(renderPanel.getModel())) { - mesh.clearMultiSelection(); - } - return; - } - - for (Mesh2D selectedMesh : selectedMeshes) { - selectedMesh.clearMultiSelection(); - for (Mesh2D otherMesh : selectedMeshes) { - if (otherMesh != selectedMesh) { - selectedMesh.addToMultiSelection(otherMesh); - } - } - } - } - - /** - * 检查是否处于多选状态 - */ - private boolean isInMultiSelection() { - return selectedMeshes.size() > 1; - } - - // ================== 辅助方法 ================== - - /** - * 在指定位置查找网格 - */ - private Mesh2D findMeshAtPosition(float modelX, float modelY) { - Model2D model = renderPanel.getModel(); - if (model == null) return null; - - try { - List parts = model.getParts(); - if (parts == null || parts.isEmpty()) return null; - - // 遍历所有部件和网格(从上到下) - for (int i = parts.size() - 1; i >= 0; i--) { - ModelPart part = parts.get(i); - if (part == null || !part.isVisible()) continue; - - List meshes = part.getMeshes(); - if (meshes == null || meshes.isEmpty()) continue; - - for (int m = meshes.size() - 1; m >= 0; m--) { - Mesh2D mesh = meshes.get(m); - if (mesh == null || !mesh.isVisible()) continue; - - if (mesh.isDirty()) { - mesh.updateBounds(); - } - - boolean contains = false; - try { - contains = mesh.containsPoint(modelX, modelY); - } catch (Exception ex) { - logger.warn("mesh.containsPoint 抛出异常: {}", ex.getMessage()); - } - - if (contains) { - return mesh; - } - } - } - return null; - } catch (Exception e) { - logger.error("检测网格时出错", e); - return null; - } - } - - /** - * 通过网格查找对应的 ModelPart - */ - private ModelPart findPartByMesh(Mesh2D mesh) { - Model2D model = renderPanel.getModel(); - if (model == null) return null; - for (ModelPart part : model.getParts()) { - ModelPart found = findPartByMeshRecursive(part, mesh); - if (found != null) { - return found; - } - } - return null; - } - - /** - * 递归查找包含指定网格的部件 - */ - private ModelPart findPartByMeshRecursive(ModelPart part, Mesh2D targetMesh) { - if (part == null || targetMesh == null) return null; - - // 检查当前部件的网格 - for (Mesh2D mesh : part.getMeshes()) { - if (mesh == targetMesh) { - return part; - } - } - - // 递归检查子部件 - for (ModelPart child : part.getChildren()) { - ModelPart found = findPartByMeshRecursive(child, targetMesh); - if (found != null) { - return found; - } - } - - return null; - } - - /** - * 获取模型中的所有网格 - */ - private List getAllMeshesFromModel(Model2D model) { - List allMeshes = new ArrayList<>(); - if (model == null) return allMeshes; - - try { - List parts = model.getParts(); - if (parts == null) return allMeshes; - - for (ModelPart part : parts) { - if (part != null && part.isVisible()) { - addMeshesFromPart(part, allMeshes); - } - } - } catch (Exception e) { - logger.error("获取模型网格时出错", e); - } - - return allMeshes; - } - - /** - * 递归从部件中获取所有网格 - */ - private void addMeshesFromPart(ModelPart part, List meshList) { - if (part == null) return; - - // 添加当前部件的网格 - List meshes = part.getMeshes(); - if (meshes != null) { - for (Mesh2D mesh : meshes) { - if (mesh != null && mesh.isVisible()) { - meshList.add(mesh); - } - } - } - - // 递归处理子部件 - for (ModelPart child : part.getChildren()) { - addMeshesFromPart(child, meshList); - } - } - - /** - * 获取所有选中的部件 - */ - public List getSelectedParts() { - List selectedParts = new ArrayList<>(); - for (Mesh2D mesh : selectedMeshes) { - ModelPart part = findPartByMesh(mesh); - if (part != null && !selectedParts.contains(part)) { - selectedParts.add(part); - } - } - return selectedParts; - } - - /** - * 进入液化模式 - */ - private void enterLiquifyMode(Mesh2D targetMesh) { - if (targetMesh == null) { - logger.warn("无法进入液化模式:目标网格为空"); - return; - } - setSelectedMesh(targetMesh); - renderPanel.switchToLiquifyTool(); - logger.info("选择工具请求进入液化模式: {}", targetMesh.getName()); - } - - public void updateMeshVertices() {} - - /** - * 获取鼠标悬停的网格 - */ - public Mesh2D getHoveredMesh() { - return hoveredMesh; - } - - public interface Call { - void call(List mesh); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/Tool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/Tool.java deleted file mode 100644 index 4bb19ed..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/Tool.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.tools; - -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.model.util.tools.RanderTools; - -import java.awt.*; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; - -/** - * 工具抽象基类 - * 所有编辑工具都应继承此类 - */ -public abstract class Tool { - protected ModelRenderPanel renderPanel; - protected String toolName; - protected String toolDescription; - protected boolean isActive = false; - - // 关联的渲染工具对象 - protected RanderTools associatedRanderTools; - - public Tool(ModelRenderPanel renderPanel, String toolName, String toolDescription) { - this.renderPanel = renderPanel; - this.toolName = toolName; - this.toolDescription = toolDescription; - } - - // ================== 生命周期方法 ================== - - /** - * 激活工具 - */ - public abstract void activate(); - - /** - * 停用工具 - */ - public abstract void deactivate(); - - /** - * 工具是否处于激活状态 - */ - public boolean isActive() { - return isActive; - } - - // ================== 事件处理方法 ================== - - /** - * 处理鼠标按下事件 - */ - public abstract void onMousePressed(MouseEvent e, float modelX, float modelY); - - /** - * 处理鼠标释放事件 - */ - public abstract void onMouseReleased(MouseEvent e, float modelX, float modelY); - - /** - * 处理鼠标拖拽事件 - */ - public abstract void onMouseDragged(MouseEvent e, float modelX, float modelY); - - /** - * 处理鼠标移动事件 - */ - public abstract void onMouseMoved(MouseEvent e, float modelX, float modelY); - - /** - * 处理鼠标点击事件 - */ - public abstract void onMouseClicked(MouseEvent e, float modelX, float modelY); - - /** - * 处理鼠标双击事件 - */ - public abstract void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY); - - // ================== 工具状态方法 ================== - - /** - * 获取工具名称 - */ - public String getToolName() { - return toolName; - } - - /** - * 获取工具描述 - */ - public String getToolDescription() { - return toolDescription; - } - - public void onKeyPressed(KeyEvent e){}; - - /** - * 获取工具光标 - */ - public abstract Cursor getToolCursor(); - - /** - * 工具是否可用 - */ - public boolean isAvailable() { - return true; - } - - /** - * 清理工具资源 - */ - public void dispose() { - // 子类可重写此方法清理资源 - if (associatedRanderTools != null) { - associatedRanderTools = null; - } - } - - // ================== 新增方法:与RanderToolsManager集成 ================== - - /** - * 设置关联的渲染工具 - * @param randerTools 渲染工具对象 - */ - public void setAssociatedRanderTools(RanderTools randerTools) { - this.associatedRanderTools = randerTools; - } - - /** - * 获取关联的渲染工具 - * @return 关联的渲染工具对象,可能为null - */ - public RanderTools getAssociatedRanderTools() { - return associatedRanderTools; - } - - /** - * 检查是否有关联的渲染工具 - * @return true如果有关联的渲染工具 - */ - public boolean hasAssociatedRanderTools() { - return associatedRanderTools != null; - } - - @Override - public String toString() { - return toolName; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java deleted file mode 100644 index 5daea78..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java +++ /dev/null @@ -1,334 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.tools; - -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.util.Vertex; -import com.chuangzhou.vivid2D.render.model.util.VertexTag; -import org.joml.Vector2f; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class VertexDeformationTool extends Tool { - private static final Logger logger = LoggerFactory.getLogger(VertexDeformationTool.class); - private Mesh2D targetMesh = null; - private final List selectedVertices = new ArrayList<>(); - private Vertex hoveredVertex = null; - private static final float VERTEX_TOLERANCE = 8.0f; - private ModelRenderPanel.DragMode currentDragMode = ModelRenderPanel.DragMode.NONE; - private final List orderedControlVertices = new ArrayList<>(); - private boolean isPushPullMode = false; - private Vector2f dragStartPoint = null; - private List dragBaseState = null; - - public VertexDeformationTool(ModelRenderPanel renderPanel) { - super(renderPanel, "顶点变形工具", "直接对网格顶点进行精细变形操作"); - } - - @Override - public void activate() { - if (isActive) return; - isActive = true; - selectedVertices.clear(); - hoveredVertex = null; - if (!renderPanel.getSelectedMeshes().isEmpty()) { - targetMesh = renderPanel.getSelectedMesh(); - } else { - targetMesh = findFirstVisibleMesh(); - } - if (targetMesh != null) { - orderedControlVertices.clear(); - orderedControlVertices.addAll(targetMesh.getDeformationControlVertices()); - targetMesh.setStates("showDeformationVertices", true); - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - targetMesh.setRenderVertices(true); - } catch (Throwable t) { logger.debug("激活顶点显示失败: {}", t.getMessage()); } - }); - logger.info("激活顶点变形工具: {},已加载 {} 个控制点。", targetMesh.getName(), orderedControlVertices.size()); - } else { - logger.warn("没有找到可用的网格用于顶点变形"); - } - renderPanel.repaint(); - } - - @Override - public void deactivate() { - if (!isActive) return; - isActive = false; - if (targetMesh != null) { - for (Vertex v : orderedControlVertices) { - v.setTag(VertexTag.DEFORMATION); - } - targetMesh.setStates("showDeformationVertices", false); - try { - targetMesh.setRenderVertices(false); - if (targetMesh.getModelPart() != null) { - targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); - targetMesh.getModelPart().updateMeshVertices(); - targetMesh.saveAsOriginal(); - } - } catch (Throwable t) { logger.debug("停用时清理失败: {}", t.getMessage()); } - } - targetMesh = null; - selectedVertices.clear(); - orderedControlVertices.clear(); - hoveredVertex = null; - currentDragMode = ModelRenderPanel.DragMode.NONE; - logger.info("停用顶点变形工具"); - } - - /** - * [已修正] onMousePressed 现在会检查 Alt 键来决定进入“推/拉”模式还是“控制点选择”模式。 - */ - @Override - public void onMousePressed(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null) return; - - // [核心修正] 检查 Alt 键是否被按下 - if (e.isAltDown()) { - isPushPullMode = true; - dragStartPoint = new Vector2f(modelX, modelY); - dragBaseState = new ArrayList<>(targetMesh.getActiveVertexList().size()); - for(Vertex v : targetMesh.getActiveVertexList()){ - dragBaseState.add(v.copy()); - } - currentDragMode = ModelRenderPanel.DragMode.NONE; - logger.debug("进入推/拉模式,起点: ({}, {})", modelX, modelY); - - } else { - // --- 默认的“控制点选择”模式 --- - isPushPullMode = false; - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - Vertex clickedVertex = findDeformationVertexAtPosition(modelX, modelY); - if (clickedVertex != null) { - if (e.isControlDown()) { - if (selectedVertices.contains(clickedVertex)) { - selectedVertices.remove(clickedVertex); - } else { - selectedVertices.add(clickedVertex); - } - } else { - if (!selectedVertices.contains(clickedVertex)) { - selectedVertices.clear(); - selectedVertices.add(clickedVertex); - } - } - currentDragMode = ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX; - } else { - if (!e.isControlDown()) { - selectedVertices.clear(); - } - currentDragMode = ModelRenderPanel.DragMode.NONE; - } - } catch (Throwable t) { - logger.error("onMousePressed (控制点模式) 处理失败", t); - } finally { - renderPanel.repaint(); - } - }); - } - } - - /** - * [已修正] onMouseDragged 现在会根据模式执行不同的拖动逻辑。 - */ - @Override - public void onMouseDragged(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null) return; - - if (isPushPullMode) { - // --- “推/拉”模式的逻辑 --- - if (dragStartPoint == null || dragBaseState == null) return; - - // 计算从按下鼠标开始的总位移 - Vector2f delta = new Vector2f(modelX, modelY).sub(dragStartPoint); - - // 定义一个画笔半径 (可以设为可配置的) - float radius = 50.0f; - - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - // 调用 Mesh2D 中已经存在的局部变形方法! - targetMesh.applyLocalizedPush(dragBaseState, dragStartPoint, delta, radius); - } catch (Throwable t) { - logger.error("onMouseDragged (推/拉模式) 处理失败", t); - } finally { - renderPanel.repaint(); - } - }); - - } else { - // --- 默认的“控制点拖动”模式的逻辑 --- - if (selectedVertices.isEmpty() || currentDragMode != ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX) return; - - Vertex primaryVertex = selectedVertices.get(selectedVertices.size() - 1); - - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - Map parameters = Map.of("id", primaryVertex.getName(), - "Vertex", primaryVertex); - renderPanel.getParametersManagement().broadcast( - targetMesh.getModelPart(), - "meshVertices", - parameters - ); - primaryVertex.position.set(modelX, modelY); - } catch (Throwable t) { - logger.error("onMouseDragged (控制点模式) 处理失败", t); - } finally { - renderPanel.repaint(); - } - }); - } - } - - /** - * [已修正] onMouseReleased 现在会在固化变形后,向 ParametersManagement 广播消息。 - */ - @Override - public void onMouseReleased(MouseEvent e, float modelX, float modelY) { - if (!isActive) return; - - if (isPushPullMode) { - // --- 清理“推/拉”模式的状态 --- - isPushPullMode = false; - dragStartPoint = null; - dragBaseState = null; - logger.debug("退出推/拉模式"); - } - - // 无论是哪种模式,都在松开鼠标时固化变形 - if (targetMesh != null) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - targetMesh.saveAsOriginal(); - if (targetMesh.getModelPart() != null) { - targetMesh.getModelPart().updateMeshVertices(); - } - } catch (Throwable t) { logger.error("onMouseReleased 保存基准或广播消息失败", t); } - }); - } - - currentDragMode = ModelRenderPanel.DragMode.NONE; - renderPanel.repaint(); - } - - @Override - public void onMouseMoved(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null) return; - Vertex newHoveredVertex = findDeformationVertexAtPosition(modelX, modelY); - if (newHoveredVertex != hoveredVertex) { - hoveredVertex = newHoveredVertex; - renderPanel.setCursor(hoveredVertex != null ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : createVertexCursor()); - } - } - - @Override - public void onMouseClicked(MouseEvent e, float modelX, float modelY) { } - - @Override - public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null || e.isAltDown()) return; // 在推拉模式下禁用双击 - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - Vertex clickedVertex = findDeformationVertexAtPosition(modelX, modelY); - if (clickedVertex != null) { - untagDeformationVertex(clickedVertex); - } else { - Vertex newVertex = targetMesh.addControlPointAt(modelX, modelY); - if (newVertex != null) { - orderedControlVertices.add(newVertex); - updateDeformationRegion(); - } else { - logger.warn("在 ({}, {}) 添加控制点失败,可能点击位置在网格外部。", modelX, modelY); - } - } - } catch (Throwable t) { - logger.error("onMouseDoubleClicked 处理失败", t); - } - }); - } - - @Override - public void onKeyPressed(KeyEvent e) { - if (!isActive || selectedVertices.isEmpty()) return; - int kc = e.getKeyCode(); - if (kc == KeyEvent.VK_BACK_SPACE || kc == KeyEvent.VK_DELETE) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - List toDelete = new ArrayList<>(selectedVertices); - for (Vertex v : toDelete) { - untagDeformationVertex(v); - } - selectedVertices.clear(); - }); - } - } - - @Override - public Cursor getToolCursor() { - return createVertexCursor(); - } - - private void untagDeformationVertex(Vertex vertex) { - if (targetMesh == null || vertex == null) return; - orderedControlVertices.remove(vertex); - selectedVertices.remove(vertex); - if (hoveredVertex == vertex) { - hoveredVertex = null; - } - vertex.setTag(VertexTag.DEFORMATION); - vertex.delete(); - updateDeformationRegion(); - } - - private void updateDeformationRegion() { - if (targetMesh == null) return; - targetMesh.setDeformationControlVertices(new ArrayList<>(orderedControlVertices)); - renderPanel.repaint(); - } - - private Vertex findDeformationVertexAtPosition(float x, float y) { - if (targetMesh == null) return null; - float tolerance = VERTEX_TOLERANCE / calculateScaleFactor(); - float toleranceSq = tolerance * tolerance; - for (Vertex v : orderedControlVertices) { - if (v.position.distanceSquared(x, y) < toleranceSq) { - return v; - } - } - return null; - } - - private float calculateScaleFactor() { - return renderPanel.getCameraManagement().calculateScaleFactor(); - } - - private Mesh2D findFirstVisibleMesh() { - Model2D model = renderPanel.getModel(); if (model == null) return null; - for (ModelPart part : model.getParts()) if (part != null && part.isVisible()) - for (Mesh2D mesh : part.getMeshes()) if (mesh != null && mesh.isVisible()) return mesh; - return null; - } - - private Cursor createVertexCursor() { - int size = 32; BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2 = img.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - int center = size / 2; g2.setColor(Color.ORANGE); g2.setStroke(new BasicStroke(2f)); - g2.drawRect(center - 5, center - 5, 10, 10); g2.dispose(); - return Toolkit.getDefaultToolkit().createCustomCursor(img, new Point(center, center), "VertexSelectCursor"); - } - - public Mesh2D getTargetMesh() {return targetMesh;} -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/FrameInterpolator.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/FrameInterpolator.java deleted file mode 100644 index 5532128..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/FrameInterpolator.java +++ /dev/null @@ -1,348 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.util; - -import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement; -import com.chuangzhou.vivid2D.render.model.AnimationParameter; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.util.Vertex; -import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; -import org.joml.Matrix3f; -import org.joml.Vector2f; -import org.slf4j.Logger; - -import java.util.*; - -/** - * [已修复] 关键帧插值器 - * 1. 优先处理 "meshVertices" 参数,对整个网格状态进行插值。 - * 2. 如果 "meshVertices" 不存在,则回退到处理独立的 "deformationVertex" 参数。 - * 3. 增加了在应用 "deformationVertex" 前的重置逻辑,防止顶点卡住。 - * 4. 修正了顶点 "删除" (取消变形) 的逻辑。 - */ -public class FrameInterpolator { - private FrameInterpolator() {} - - // ---- 辅助转换方法(统一处理 Number / String / List 等) ---- - private static float toFloat(Object o) { - if (o == null) return 0f; - if (o instanceof Number) return ((Number) o).floatValue(); - if (o instanceof String) { - try { return Float.parseFloat((String) o); } catch (NumberFormatException ignored) { } - } - return 0f; - } - - private static float[] readVec2(Object o) { - float[] out = new float[]{0f, 0f}; - if (o instanceof List) { - List l = (List) o; - if (l.size() > 0) out[0] = toFloat(l.get(0)); - if (l.size() > 1) out[1] = toFloat(l.get(1)); - } else if (o instanceof Vector2f) { - out[0] = ((Vector2f) o).x; - out[1] = ((Vector2f) o).y; - } else if (o != null && o.getClass().isArray()) { - try { - // 处理 float[] 的情况 - if (o instanceof float[] arr) { - if (arr.length > 0) out[0] = arr[0]; - if (arr.length > 1) out[1] = arr[1]; - } else { - Object[] arr = null; - if (o instanceof Object[]) { - arr = (Object[]) o; - } - if (arr != null && arr.length > 0) out[0] = toFloat(arr[0]); - if (arr != null && arr.length > 1) out[1] = toFloat(arr[1]); - } - } catch (Exception ignored) {} - } if (o instanceof Vertex vertex) { - out[0] = vertex.position.x; - out[1] = vertex.position.y; - } else if (o instanceof Number || o instanceof String) { - float v = toFloat(o); - out[0] = out[1] = v; - } - return out; - } - - private static float normalizeAngle(float a) { - while (a <= -Math.PI) a += 2 * Math.PI; - while (a > Math.PI) a -= 2 * Math.PI; - return a; - } - - private static float normalizeAnimAngleUnits(float a) { - // 假设大于 2*PI 的值是以角度为单位的 - if (Math.abs(a) > Math.PI * 2.1f) { - return (float) Math.toRadians(a); - } - return a; - } - - // ---- 查找与特定动画参数关联的 paramId 索引 ---- - private static List findIndicesForParam(ParametersManagement.Parameter fullParam, String paramId, AnimationParameter currentAnimationParameter) { - List indices = new ArrayList<>(); - if (fullParam == null || fullParam.paramId() == null || currentAnimationParameter == null) return indices; - List pids = fullParam.paramId(); - List animParams = fullParam.animationParameter(); - if (animParams == null || animParams.size() != pids.size()) return indices; - - for (int i = 0; i < pids.size(); i++) { - if (paramId.equals(pids.get(i)) && currentAnimationParameter.equals(animParams.get(i))) { - indices.add(i); - } - } - return indices; - } - - /** - * [新增] 查找与特定变形顶点ID关联的 "meshVertices" 参数索引。 - */ - private static List findIndicesForDeformationVertex(ParametersManagement.Parameter fullParam, String vertexId, AnimationParameter currentAnimationParameter) { - List indices = new ArrayList<>(); - if (fullParam == null || fullParam.paramId() == null || currentAnimationParameter == null || vertexId == null) return indices; - - List pids = fullParam.paramId(); - List values = fullParam.value(); - List animParams = fullParam.animationParameter(); - - if (animParams == null || animParams.size() != pids.size() || values.size() != pids.size()) return indices; - - for (int i = 0; i < pids.size(); i++) { - // 筛选出 "meshVertices" 参数,并且属于当前动画 - if ("meshVertices".equals(pids.get(i)) && currentAnimationParameter.equals(animParams.get(i))) { - Object val = values.get(i); - // 检查值是否为 Map,并且其 "id" 字段匹配我们正在寻找的 vertexId - if (val instanceof Map) { - Map mapValue = (Map) val; - if (vertexId.equals(mapValue.get("id"))) { - indices.add(i); - } - } - } - } - return indices; - } - - - // ---- 在指定索引集合中查找围绕 current 的前后关键帧 ---- - private static int[] findSurroundingKeyframesForIndices(List keyframes, List indices, float current) { - int prevIndex = -1, nextIndex = -1; - float prevVal = Float.NEGATIVE_INFINITY, nextVal = Float.POSITIVE_INFINITY; - if (keyframes == null || indices == null) return new int[]{-1, -1}; - for (int idx : indices) { - if (idx >= 0 && idx < keyframes.size()) { - float val = keyframes.get(idx); - if (val <= current && (prevIndex == -1 || val >= prevVal)) { - prevIndex = idx; - prevVal = val; - } - if (val >= current && (nextIndex == -1 || val <= nextVal)) { - nextIndex = idx; - nextVal = val; - } - } - } - return new int[] { prevIndex, nextIndex }; - } - - private static float computeT(float prevVal, float nextVal, float current) { - if (Float.compare(nextVal, prevVal) == 0) return 0f; - return Math.max(0f, Math.min(1f, (current - prevVal) / (nextVal - prevVal))); - } - - // ---- 计算 position/scale/pivot 的目标值 ---- - private static boolean computeVec2Target(ParametersManagement.Parameter fullParam, String paramId, float current, float[] out, AnimationParameter animParam) { - List idxs = findIndicesForParam(fullParam, paramId, animParam); - if (idxs.isEmpty()) return false; - - int[] surrounding = findSurroundingKeyframesForIndices(fullParam.keyframe(), idxs, current); - int prevIndex = surrounding[0], nextIndex = surrounding[1]; - List values = fullParam.value(); - - if (prevIndex != -1 && nextIndex != -1) { - if (prevIndex == nextIndex) { - float[] v = readVec2(values.get(prevIndex)); - out[0] = v[0]; out[1] = v[1]; - return true; - } - float[] prev = readVec2(values.get(prevIndex)); - float[] next = readVec2(values.get(nextIndex)); - float t = computeT(fullParam.keyframe().get(prevIndex), fullParam.keyframe().get(nextIndex), current); - out[0] = prev[0] + t * (next[0] - prev[0]); - out[1] = prev[1] + t * (next[1] - prev[1]); - return true; - } else if (prevIndex != -1) { - float[] v = readVec2(values.get(prevIndex)); - out[0] = v[0]; out[1] = v[1]; - return true; - } else if (nextIndex != -1) { - float[] v = readVec2(values.get(nextIndex)); - out[0] = v[0]; out[1] = v[1]; - return true; - } - return false; - } - - // ---- 计算 rotation 的目标值 ---- - private static boolean computeRotationTarget(ParametersManagement.Parameter fullParam, String paramId, float current, float[] out, AnimationParameter animParam) { - List idxs = findIndicesForParam(fullParam, paramId, animParam); - if (idxs.isEmpty()) return false; - - int[] surrounding = findSurroundingKeyframesForIndices(fullParam.keyframe(), idxs, current); - int prevIndex = surrounding[0], nextIndex = surrounding[1]; - List values = fullParam.value(); - - float target; - if (prevIndex != -1 && nextIndex != -1) { - if (prevIndex == nextIndex) { - target = toFloat(values.get(prevIndex)); - } else { - float p = normalizeAnimAngleUnits(toFloat(values.get(prevIndex))); - float q = normalizeAnimAngleUnits(toFloat(values.get(nextIndex))); - float t = computeT(fullParam.keyframe().get(prevIndex), fullParam.keyframe().get(nextIndex), current); - // 正确处理角度插值,避免“绕远路” - target = p + t * normalizeAngle(q - p); - } - } else if (prevIndex != -1) { - target = toFloat(values.get(prevIndex)); - } else if (nextIndex != -1) { - target = toFloat(values.get(nextIndex)); - } else { - return false; - } - out[0] = target; - return true; - } - - /** - * [新增] 计算所有变形顶点的目标状态。 - */ - private static Map computeMeshVerticesTarget(ParametersManagement.Parameter fullParam, float current, AnimationParameter animParam) { - Map targetDeformations = new HashMap<>(); - if (fullParam == null) return targetDeformations; - Set uniqueVertexIds = new HashSet<>(); - List pids = fullParam.paramId(); - List values = fullParam.value(); - List animParams = fullParam.animationParameter(); - for (int i = 0; i < pids.size(); i++) { - if ("meshVertices".equals(pids.get(i)) && animParam.equals(animParams.get(i))) { - Object val = values.get(i); - if (val instanceof Map) { - Object id = ((Map) val).get("id"); - if (id instanceof String) { - uniqueVertexIds.add((String) id); - } - } - } - } - if (uniqueVertexIds.isEmpty()) return targetDeformations; - for (String vertexId : uniqueVertexIds) { - List idxs = findIndicesForDeformationVertex(fullParam, vertexId, animParam); - if (idxs.isEmpty()) continue; - int[] surrounding = findSurroundingKeyframesForIndices(fullParam.keyframe(), idxs, current); - int prevIndex = surrounding[0]; - int nextIndex = surrounding[1]; - float[] finalPos = new float[2]; - boolean posCalculated = false; - if (prevIndex != -1 && nextIndex != -1) { - Map prevData = (Map) values.get(prevIndex); - float[] prevPos = readVec2(prevData.get("Vertex")); - if (prevIndex == nextIndex) { - finalPos = prevPos; - posCalculated = true; - } else { - Map nextData = (Map) values.get(nextIndex); - float[] nextPos = readVec2(nextData.get("Vertex")); - float t = computeT(fullParam.keyframe().get(prevIndex), fullParam.keyframe().get(nextIndex), current); - finalPos[0] = prevPos[0] + t * (nextPos[0] - prevPos[0]); - finalPos[1] = prevPos[1] + t * (nextPos[1] - prevPos[1]); - posCalculated = true; - } - } else if (prevIndex != -1) { - Map prevData = (Map) values.get(prevIndex); - finalPos = readVec2(prevData.get("Vertex")); - posCalculated = true; - } else if (nextIndex != -1) { - Map nextData = (Map) values.get(nextIndex); - finalPos = readVec2(nextData.get("Vertex")); - posCalculated = true; - } - - if (posCalculated) { - targetDeformations.put(vertexId, finalPos); - } - } - return targetDeformations; - } - - - /** - * 将变换操作按当前关键帧插值并应用到 parts。 - * 应在 GL 上下文线程中调用。 - */ - public static void applyFrameInterpolations(ParametersManagement pm, List parts, AnimationParameter currentAnimationParameter, Logger logger) { - if (pm == null || parts == null || parts.isEmpty() || currentAnimationParameter == null || pm.getParametersPanel().getSelectParameter() == null) return; - - float current = toFloat(currentAnimationParameter.getValue()); - - for (ModelPart part : parts) { - if (!Objects.equals(pm.getParametersPanel().getSelectParameter().getId(), currentAnimationParameter.getId())) continue; - - ParametersManagement.Parameter fullParam = pm.getModelPartParameters(part); - if (fullParam == null) continue; - - try { - float[] targetPivot = null, targetScale = null, targetPosition = null; - Float targetRotation = null; - - float[] tmp2 = new float[2]; - if (computeVec2Target(fullParam, "pivot", current, tmp2, currentAnimationParameter)) targetPivot = tmp2.clone(); - if (computeVec2Target(fullParam, "scale", current, tmp2, currentAnimationParameter)) targetScale = tmp2.clone(); - if (computeVec2Target(fullParam, "position", current, tmp2, currentAnimationParameter)) targetPosition = tmp2.clone(); - - float[] tmp1 = new float[1]; - if (computeRotationTarget(fullParam, "rotate", current, tmp1, currentAnimationParameter)) targetRotation = tmp1[0]; - - Map targetDeformations = computeMeshVerticesTarget(fullParam, current, currentAnimationParameter); - if (targetPivot != null) part.setPivot(targetPivot[0], targetPivot[1]); - if (targetScale != null) part.setScale(targetScale[0], targetScale[1]); - if (targetPosition != null) part.setPosition(targetPosition[0], targetPosition[1]); - if (targetRotation != null) part.setRotation(targetRotation); - if (targetDeformations.isEmpty() || part.getMeshes().isEmpty()) { - part.updateMeshVertices(); - } else { - Mesh2D targetMesh = part.getMeshes().get(0); - if (targetMesh != null && targetMesh.getActiveVertexList() != null) { - List allVerticesInMesh = targetMesh.getActiveVertexList().getVertices(); - for (Map.Entry deformationEntry : targetDeformations.entrySet()) { - String vertexIdToFind = deformationEntry.getKey(); - float[] worldPos = deformationEntry.getValue(); - for (Vertex vertex : allVerticesInMesh) { - if (vertexIdToFind.equals(vertex.getName())) { - vertex.position.set(worldPos[0], worldPos[1]); - break; - } - } - targetMesh.saveAsOriginal(); - } - for (Vertex vertex : targetMesh.getDeformationControlVertices()){ - for (Map.Entry deformationEntry : targetDeformations.entrySet()) { - String vertexIdToFind = deformationEntry.getKey(); - float[] worldPos = deformationEntry.getValue(); - if (vertexIdToFind.equals(vertex.getName())) { - vertex.position.set(worldPos[0], worldPos[1]); - break; - } - } - } - part.updateMeshVertices(); - } - } - } catch (Exception e) { - logger.error("在对部件 '{}' 应用插值时发生异常", part.getName(), e); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java deleted file mode 100644 index 6aea984..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.util; - -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.Texture; -import org.joml.Vector2f; -import org.lwjgl.system.MemoryUtil; - -import java.awt.image.BufferedImage; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.ArrayList; -import java.util.List; - -public class MeshTextureUtil { - public static Mesh2D createQuadForImage(BufferedImage img, String meshName) { - float w = img.getWidth(); - float h = img.getHeight(); - - try { - return Mesh2D.createQuad(meshName, w, h); - } catch (Exception ignored) {} - - throw new RuntimeException("无法创建 Mesh2D"); - } - - - public static Texture tryCreateTextureFromImageMemory(BufferedImage img, String texName) { - try { - int w = img.getWidth(); - int h = img.getHeight(); - ByteBuffer buf = imageToRGBAByteBuffer(img); - - Constructor suit = null; - for (Constructor c : Texture.class.getDeclaredConstructors()) { - Class[] ps = c.getParameterTypes(); - if (ps.length >= 4 && ps[0] == String.class) { - suit = c; - break; - } - } - if (suit != null) { - suit.setAccessible(true); - Object texObj = null; - Class[] ps = suit.getParameterTypes(); - if (ps.length >= 5 && ps[3].getSimpleName().toLowerCase().contains("format")) { - Object formatEnum = null; - try { - Class formatCls = null; - for (Class inner : Texture.class.getDeclaredClasses()) { - if (inner.getSimpleName().toLowerCase().contains("format")) { - formatCls = inner; - break; - } - } - if (formatCls != null) { - for (Field f : formatCls.getFields()) { - if (f.getName().toUpperCase().contains("RGBA")) { - formatEnum = f.get(null); - break; - } - } - } - } catch (Throwable ignored) { - } - if (formatEnum != null) { - try { - texObj = suit.newInstance(texName, w, h, formatEnum, buf); - } catch (Throwable ignored) { - } - } - } - if (texObj == null) { - try { - texObj = suit.newInstance(texName, w, h, buf); - } catch (Throwable ignored) { - } - } - if (texObj instanceof Texture) return (Texture) texObj; - } - } catch (Throwable t) { - t.printStackTrace(); - } - return null; - } - - private static ByteBuffer imageToRGBAByteBuffer(BufferedImage img) { - final int w = img.getWidth(); - final int h = img.getHeight(); - final int[] pixels = new int[w * h]; - img.getRGB(0, 0, w, h, pixels, 0, w); - ByteBuffer buffer = MemoryUtil.memAlloc(w * h * 4).order(ByteOrder.nativeOrder()); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - int argb = pixels[y * w + x]; - int a = (argb >> 24) & 0xFF; - int r = (argb >> 16) & 0xFF; - int g = (argb >> 8) & 0xFF; - int b = (argb) & 0xFF; - buffer.put((byte) r); - buffer.put((byte) g); - buffer.put((byte) b); - buffer.put((byte) a); - } - } - buffer.flip(); - return buffer; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java deleted file mode 100644 index af66065..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java +++ /dev/null @@ -1,1462 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.util; - -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import org.joml.Vector2f; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.*; -import java.util.concurrent.CopyOnWriteArrayList; - -/** - * 全局操作历史管理器 - * 负责统一注册、管理和发布所有操作记录 - * - * @author tzdwindows 7 - */ -public class OperationHistoryGlobal { - - private static final Logger LOGGER = LoggerFactory.getLogger(OperationHistoryGlobal.class); - - // 单例实例 - private static final OperationHistoryGlobal INSTANCE = new OperationHistoryGlobal(); - - // 操作记录管理器 - private final OperationHistoryManager historyManager; - - // 操作监听器列表 - private final List listeners; - - // 已注册的操作类型 - private final Set registeredOperations; - - // 操作记录器映射 - private final Map recorderMap; - - // 默认监听器 - private final DefaultOperationListener defaultListener; - - private OperationHistoryGlobal() { - this.historyManager = OperationHistoryManager.getInstance(); - this.listeners = new CopyOnWriteArrayList<>(); - this.registeredOperations = new HashSet<>(); - this.recorderMap = new HashMap<>(); - this.defaultListener = new DefaultOperationListener(); - - // 添加默认监听器 - addOperationListener(defaultListener); - - // 初始化基础操作记录器 - initializeBasicRecorders(); - } - - /** - * 获取全局实例 - */ - public static OperationHistoryGlobal getInstance() { - return INSTANCE; - } - - /** - * 初始化基础操作记录器 - */ - private void initializeBasicRecorders() { - LOGGER.debug("开始初始化基础操作记录器..."); - - // 基础变换操作 - registerOperationRecorder("SET_POSITION", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("SET_POSITION", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("SET_POSITION", "undo", params); - } - - @Override - public String getDescription() { - return "移动图层"; - } - }); - - registerOperationRecorder("DRAG_PART_END", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("DRAG_PART_END", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("DRAG_PART_END", "undo", params); - } - - @Override - public String getDescription() { - return "拖拽图层结束"; - } - }); - - registerOperationRecorder("RESIZE_PART_END", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("RESIZE_PART_END", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("RESIZE_PART_END", "undo", params); - } - - @Override - public String getDescription() { - return "调整大小结束"; - } - }); - - registerOperationRecorder("ROTATE_PART_END", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("ROTATE_PART_END", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("ROTATE_PART_END", "undo", params); - } - - @Override - public String getDescription() { - return "旋转图层结束"; - } - }); - - registerOperationRecorder("MOVE_PIVOT_END", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("MOVE_PIVOT_END", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("MOVE_PIVOT_END", "undo", params); - } - - @Override - public String getDescription() { - return "移动中心点结束"; - } - }); - - registerOperationRecorder("SET_ROTATION", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("SET_ROTATION", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("SET_ROTATION", "undo", params); - } - - @Override - public String getDescription() { - return "旋转图层"; - } - }); - - registerOperationRecorder("BATCH_TRANSFORM", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("BATCH_TRANSFORM", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("BATCH_TRANSFORM", "undo", params); - } - - @Override - public String getDescription() { - return "批量变换"; - } - }); - - registerOperationRecorder("SET_SCALE", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("SET_SCALE", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("SET_SCALE", "undo", params); - } - - @Override - public String getDescription() { - return "缩放图层"; - } - }); - - registerOperationRecorder("SET_OPACITY", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("SET_OPACITY", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("SET_OPACITY", "undo", params); - } - - @Override - public String getDescription() { - return "调整不透明度"; - } - }); - - registerOperationRecorder("SET_VISIBLE", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("SET_VISIBLE", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("SET_VISIBLE", "undo", params); - } - - @Override - public String getDescription() { - return "显示/隐藏图层"; - } - }); - - registerOperationRecorder("SET_PIVOT", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("SET_PIVOT", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("SET_PIVOT", "undo", params); - } - - @Override - public String getDescription() { - return "设置中心点"; - } - }); - - // 图层操作 - registerOperationRecorder("ADD_PART", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("ADD_PART", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("ADD_PART", "undo", params); - } - - @Override - public String getDescription() { - return "添加图层"; - } - }); - - registerOperationRecorder("REMOVE_PART", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("REMOVE_PART", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("REMOVE_PART", "undo", params); - } - - @Override - public String getDescription() { - return "删除图层"; - } - }); - - registerOperationRecorder("RENAME_PART", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("RENAME_PART", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("RENAME_PART", "undo", params); - } - - @Override - public String getDescription() { - return "重命名图层"; - } - }); - - // 拖拽操作 - registerOperationRecorder("DRAG_PART", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("DRAG_PART", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("DRAG_PART", "undo", params); - } - - @Override - public String getDescription() { - return "拖拽图层"; - } - }); - - // 网格操作 - registerOperationRecorder("ADD_MESH", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("ADD_MESH", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("ADD_MESH", "undo", params); - } - - @Override - public String getDescription() { - return "添加网格"; - } - }); - - registerOperationRecorder("REMOVE_MESH", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("REMOVE_MESH", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("REMOVE_MESH", "undo", params); - } - - @Override - public String getDescription() { - return "移除网格"; - } - }); - - // 液化操作 - registerOperationRecorder("LIQUIFY", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("LIQUIFY", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("LIQUIFY", "undo", params); - } - - @Override - public String getDescription() { - return "液化操作"; - } - }); - - // 层级操作 - registerOperationRecorder("ADD_CHILD", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("ADD_CHILD", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("ADD_CHILD", "undo", params); - } - - @Override - public String getDescription() { - return "添加子部件"; - } - }); - - registerOperationRecorder("REMOVE_CHILD", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("REMOVE_CHILD", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("REMOVE_CHILD", "undo", params); - } - - @Override - public String getDescription() { - return "移除子部件"; - } - }); - - // 纹理操作 - registerOperationRecorder("BIND_TEXTURE", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("BIND_TEXTURE", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("BIND_TEXTURE", "undo", params); - } - - @Override - public String getDescription() { - return "绑定纹理"; - } - }); - - // PSD导入操作 - registerOperationRecorder("IMPORT_PSD", new OperationRecorder() { - @Override - public void execute(Object... params) { - notifyListeners("IMPORT_PSD", "execute", params); - } - - @Override - public void undo(Object... params) { - notifyListeners("IMPORT_PSD", "undo", params); - } - - @Override - public String getDescription() { - return "导入PSD"; - } - }); - - LOGGER.info("基础操作记录器初始化完成,共注册 {} 个操作类型", registeredOperations.size()); - } - - /** - * 默认操作监听器 - * 用于处理基础操作记录器的初始化和基本事件处理 - */ - private static class DefaultOperationListener implements OperationListener { - - private final Map operationCounts = new HashMap<>(); - private final Set handledOperations = new HashSet<>(); - private final Map operationContext = new HashMap<>(); - - @Override - public void onOperationEvent(String operationType, String action, Object... params) { - // 记录操作统计 - operationCounts.put(operationType, operationCounts.getOrDefault(operationType, 0) + 1); - - // 根据操作类型和动作进行处理 - switch (action) { - case "record": - handleRecordEvent(operationType, params); - break; - case "execute": - handleExecuteEvent(operationType, params); - break; - case "undo": - handleUndoEvent(operationType, params); - break; - case "redo": - handleRedoEvent(operationType, params); - break; - case "clear": - handleClearEvent(operationType, params); - break; - default: - handleUnknownEvent(operationType, action, params); - break; - } - - // 标记已处理的操作类型 - handledOperations.add(operationType); - } - - private void handleRecordEvent(String operationType, Object... params) { - // 处理记录操作事件 - 保存操作状态用于撤回 - switch (operationType) { - case "SET_POSITION": - handlePositionRecord(params); - break; - case "SET_ROTATION": - handleRotationRecord(params); - break; - case "SET_SCALE": - handleScaleRecord(params); - break; - case "SET_OPACITY": - handleOpacityRecord(params); - break; - case "SET_VISIBLE": - handleVisibleRecord(params); - break; - case "SET_PIVOT": - handlePivotRecord(params); - break; - case "ADD_PART": - handleAddPartRecord(params); - break; - case "REMOVE_PART": - handleRemovePartRecord(params); - break; - case "RENAME_PART": - handleRenamePartRecord(params); - break; - case "DRAG_PART": - handleDragPartRecord(params); - break; - case "DRAG_PART_END": - handleDragPartEndRecord(params); - break; - case "RESIZE_PART_END": - handleResizePartEndRecord(params); - break; - case "ROTATE_PART_END": - handleRotatePartEndRecord(params); - break; - case "MOVE_PIVOT_END": - handleMovePivotEndRecord(params); - break; - case "ADD_MESH": - handleAddMeshRecord(params); - break; - case "REMOVE_MESH": - handleRemoveMeshRecord(params); - break; - case "BIND_TEXTURE": - handleBindTextureRecord(params); - break; - case "BATCH_TRANSFORM": - handleBatchTransformRecord(params); - break; - default: - LOGGER.debug("记录操作: {}", operationType); - break; - } - } - - private void handleExecuteEvent(String operationType, Object... params) { - // 处理执行操作事件(重做) - switch (operationType) { - case "SET_POSITION": - executePositionChange(params); - break; - case "SET_ROTATION": - executeRotationChange(params); - break; - case "SET_SCALE": - executeScaleChange(params); - break; - case "SET_OPACITY": - executeOpacityChange(params); - break; - case "SET_VISIBLE": - executeVisibleChange(params); - break; - case "SET_PIVOT": - executePivotChange(params); - break; - case "DRAG_PART_END": - executeDragPartEnd(params); - break; - case "RESIZE_PART_END": - executeResizePartEnd(params); - break; - case "ROTATE_PART_END": - executeRotatePartEnd(params); - break; - case "MOVE_PIVOT_END": - executeMovePivotEnd(params); - break; - case "BATCH_TRANSFORM": - executeBatchTransform(params); - break; - case "LIQUIFY": - executeLiquify(params); - break; - default: - LOGGER.debug("执行操作: {}", operationType); - break; - } - } - - private void handleUndoEvent(String operationType, Object... params) { - // 处理撤回操作事件 - switch (operationType) { - case "SET_POSITION": - undoPositionChange(params); - break; - case "SET_ROTATION": - undoRotationChange(params); - break; - case "SET_SCALE": - undoScaleChange(params); - break; - case "SET_OPACITY": - undoOpacityChange(params); - break; - case "SET_VISIBLE": - undoVisibleChange(params); - break; - case "SET_PIVOT": - undoPivotChange(params); - break; - case "DRAG_PART_END": - undoDragPartEnd(params); - break; - case "RESIZE_PART_END": - undoResizePartEnd(params); - break; - case "ROTATE_PART_END": - undoRotatePartEnd(params); - break; - case "MOVE_PIVOT_END": - undoMovePivotEnd(params); - break; - case "BATCH_TRANSFORM": - undoBatchTransform(params); - break; - case "LIQUIFY": - undoLiquify(params); - break; - default: - //System.out.println("撤回操作: " + operationType); - break; - } - } - - // ============ 批量变换操作方法 ============ - - private void executeLiquify(Object... params) { - // 重做液化操作:应用液化后的状态 - if (params.length >= 2) { - @SuppressWarnings("unchecked") - Map afterStates = - (Map) params[1]; - applyLiquifyStates(afterStates); - } - } - - private void undoLiquify(Object... params) { - // 撤回液化操作:恢复液化前的状态 - if (params.length >= 1) { - @SuppressWarnings("unchecked") - Map beforeStates = - (Map) params[0]; - applyLiquifyStates(beforeStates); - } - } - - private void applyLiquifyStates(Map states) { - if (states == null || states.isEmpty()) { - return; - } - - for (Map.Entry entry : states.entrySet()) { - Mesh2D mesh = entry.getKey(); - OperationHistoryGlobal.MeshState state = entry.getValue(); - - if (mesh == null) { - continue; - } - - try { - // 恢复顶点数据 - if (state.vertices != null && state.vertices.length > 0) { - mesh.setVertices(state.vertices, true); // true表示同时更新originalVertices - } else { - } - - // 恢复原始顶点数据(作为备份) - if (state.originalVertices != null && state.originalVertices.length > 0) { - mesh.setOriginalVertices(state.originalVertices); - } - - // 恢复原始中心点 - if (state.originalPivot != null) { - mesh.setOriginalPivot(state.originalPivot); - } - - // 强制更新边界 - mesh.updateBounds(); - - } catch (Exception e) { - } - } - } - - private void handleBatchTransformRecord(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - Object[] oldValues = (Object[]) params[1]; - Object[] newValues = (Object[]) params[2]; - } - } - - private void executeBatchTransform(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - Object[] newValues = (Object[]) params[2]; - - // 应用新的变换值 - if (newValues.length >= 4) { - Vector2f newPosition = (Vector2f) newValues[0]; - Float newRotation = (Float) newValues[1]; - Vector2f newScale = (Vector2f) newValues[2]; - Vector2f newPivot = (Vector2f) newValues[3]; - - part.setPosition(newPosition.x, newPosition.y); - part.setRotation(newRotation); - part.setScale(newScale.x, newScale.y); - part.setPivot(newPivot.x, newPivot.y); - } - - LOGGER.debug("重做批量变换: {}", part.getName()); - } - } - - private void undoBatchTransform(Object... params) { - if (params.length >= 2 && params[0] instanceof ModelPart part) { - Object[] oldValues = (Object[]) params[1]; - - // 恢复旧的变换值 - if (oldValues.length >= 4) { - Vector2f oldPosition = (Vector2f) oldValues[0]; - Float oldRotation = (Float) oldValues[1]; - Vector2f oldScale = (Vector2f) oldValues[2]; - Vector2f oldPivot = (Vector2f) oldValues[3]; - - part.setPosition(oldPosition.x, oldPosition.y); - part.setRotation(oldRotation); - part.setScale(oldScale.x, oldScale.y); - part.setPivot(oldPivot.x, oldPivot.y); - } - - LOGGER.debug("撤回批量变换: {}", part.getName()); - } - } - - private void handleRedoEvent(String operationType, Object... params) { - // 处理重做操作事件 - 与 execute 相同 - handleExecuteEvent(operationType, params); - } - - private void handleClearEvent(String operationType, Object... params) { - // 处理清空历史事件 - operationCounts.clear(); - handledOperations.clear(); - operationContext.clear(); - LOGGER.debug("操作历史已清空"); - } - - private void handleUnknownEvent(String operationType, String action, Object... params) { - LOGGER.debug("未知操作事件: {} - {}", operationType, action); - } - - // ============ 新增的拖拽结束操作方法 ============ - - private void handleDragPartEndRecord(Object... params) { - //if (params.length >= 2) { - // List parts = (List) params[0]; - // Map startPositions = (Map) params[1]; -// - // System.out.printf("记录拖拽结束: %d 个部件从起始位置拖拽%n", parts.size()); - // for (ModelPart part : parts) { - // Vector2f startPos = startPositions.get(part); - // Vector2f currentPos = part.getPosition(); - // System.out.printf(" 部件 %s: (%.1f,%.1f) -> (%.1f,%.1f)%n", - // part.getName(), startPos.x, startPos.y, currentPos.x, currentPos.y); - // } - //} - } - - private void handleResizePartEndRecord(Object... params) { - if (params.length >= 2) { - List parts = (List) params[0]; - Map startScales = (Map) params[1]; - for (ModelPart part : parts) { - Vector2f startScale = startScales.get(part); - Vector2f currentScale = part.getScale(); - LOGGER.debug(" 部件 %s: (%.2f,%.2f) -> (%.2f,%.2f)%n", - part.getName(), startScale.x, startScale.y, currentScale.x, currentScale.y); - } - } - } - - private void handleRotatePartEndRecord(Object... params) { - } - - private void handleMovePivotEndRecord(Object... params) { - } - - private void executeDragPartEnd(Object... params) { - if (params.length >= 2) { - List parts = (List) params[0]; - // 从参数中获取当前位置(参数索引从2开始) - int paramIndex = 2; - for (ModelPart part : parts) { - if (paramIndex < params.length && params[paramIndex] instanceof Vector2f targetPosition) { - part.setPosition(targetPosition.x, targetPosition.y); - paramIndex++; - } - } - LOGGER.debug("重做拖拽结束操作"); - } - } - - private void executeResizePartEnd(Object... params) { - if (params.length >= 2) { - List parts = (List) params[0]; - // 从参数中获取当前缩放(参数索引从2开始) - int paramIndex = 2; - for (ModelPart part : parts) { - if (paramIndex < params.length && params[paramIndex] instanceof Vector2f targetScale) { - part.setScale(targetScale.x, targetScale.y); - paramIndex++; - } - } - LOGGER.debug("重做调整大小结束操作"); - } - } - - private void executeRotatePartEnd(Object... params) { - if (params.length >= 2) { - List parts = (List) params[0]; - // 从参数中获取当前旋转(参数索引从2开始) - int paramIndex = 2; - for (ModelPart part : parts) { - if (paramIndex < params.length && params[paramIndex] instanceof Float) { - float targetRotation = (Float) params[paramIndex]; - part.setRotation(targetRotation); - paramIndex++; - } - } - LOGGER.debug("重做旋转结束操作"); - } - } - - private void executeMovePivotEnd(Object... params) { - if (params.length >= 2) { - List parts = (List) params[0]; - // 从参数中获取当前中心点(参数索引从2开始) - int paramIndex = 2; - for (ModelPart part : parts) { - if (paramIndex < params.length && params[paramIndex] instanceof Vector2f targetPivot) { - part.setPivot(targetPivot.x, targetPivot.y); - paramIndex++; - } - } - LOGGER.debug("重做移动中心点结束操作"); - } - } - - private void undoDragPartEnd(Object... params) { - if (params.length >= 2) { - List parts = (List) params[0]; - Map startPositions = (Map) params[1]; - - for (ModelPart part : parts) { - Vector2f startPosition = startPositions.get(part); - if (startPosition != null) { - part.setPosition(startPosition.x, startPosition.y); - } - } - // System.out.println("撤回拖拽结束操作"); - } - } - - private void undoResizePartEnd(Object... params) { - if (params.length >= 2) { - List parts = (List) params[0]; - Map startScales = (Map) params[1]; - - for (ModelPart part : parts) { - Vector2f startScale = startScales.get(part); - if (startScale != null) { - part.setScale(startScale.x, startScale.y); - } - } - LOGGER.debug("撤回调整大小结束操作"); - } - } - - private void undoRotatePartEnd(Object... params) { - if (params.length >= 2) { - List parts = (List) params[0]; - Map startRotations = (Map) params[1]; - - for (ModelPart part : parts) { - Float startRotation = startRotations.get(part); - if (startRotation != null) { - part.setRotation(startRotation); - } - } - LOGGER.debug("撤回旋转结束操作"); - } - } - - private void undoMovePivotEnd(Object... params) { - if (params.length >= 2) { - List parts = (List) params[0]; - Map startPivots = (Map) params[1]; - - for (ModelPart part : parts) { - Vector2f startPivot = startPivots.get(part); - if (startPivot != null) { - part.setPivot(startPivot.x, startPivot.y); - } - } - LOGGER.debug("撤回移动中心点结束操作"); - } - } - - - // ============ 具体操作处理方法 ============ - - private void handlePositionRecord(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - Vector2f oldPosition = (Vector2f) params[1]; - Vector2f newPosition = (Vector2f) params[2]; - - // 保存撤回信息 - String key = "position_undo_" + part.getName(); - operationContext.put(key, oldPosition); - } - } - - private void executePositionChange(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - Vector2f newPosition = (Vector2f) params[2]; - part.setPosition(newPosition.x, newPosition.y); - } - } - - private void undoPositionChange(Object... params) { - if (params.length >= 2 && params[0] instanceof ModelPart part) { - Vector2f oldPosition = (Vector2f) params[1]; - part.setPosition(oldPosition.x, oldPosition.y); - } - } - - private void handleRotationRecord(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - float oldRotation = (Float) params[1]; - float newRotation = (Float) params[2]; - - String key = "rotation_undo_" + part.getName(); - operationContext.put(key, oldRotation); - } - } - - private void executeRotationChange(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - float newRotation = (Float) params[2]; - part.setRotation(newRotation); - } - } - - private void undoRotationChange(Object... params) { - if (params.length >= 2 && params[0] instanceof ModelPart part) { - float oldRotation = (Float) params[1]; - part.setRotation(oldRotation); - } - } - - private void handleScaleRecord(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - Vector2f oldScale = (Vector2f) params[1]; - Vector2f newScale = (Vector2f) params[2]; - - String key = "scale_undo_" + part.getName(); - operationContext.put(key, oldScale); - } - } - - private void executeScaleChange(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - Vector2f newScale = (Vector2f) params[2]; - part.setScale(newScale.x, newScale.y); - } - } - - private void undoScaleChange(Object... params) { - if (params.length >= 2 && params[0] instanceof ModelPart part) { - Vector2f oldScale = (Vector2f) params[1]; - part.setScale(oldScale.x, oldScale.y); - } - } - - private void handleOpacityRecord(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - float oldOpacity = (Float) params[1]; - float newOpacity = (Float) params[2]; - - String key = "opacity_undo_" + part.getName(); - operationContext.put(key, oldOpacity); - } - } - - private void executeOpacityChange(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - float newOpacity = (Float) params[2]; - part.setOpacity(newOpacity); - } - } - - private void undoOpacityChange(Object... params) { - if (params.length >= 2 && params[0] instanceof ModelPart part) { - float oldOpacity = (Float) params[1]; - part.setOpacity(oldOpacity); - } - } - - private void handleVisibleRecord(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - boolean oldVisible = (Boolean) params[1]; - boolean newVisible = (Boolean) params[2]; - - String key = "visible_undo_" + part.getName(); - operationContext.put(key, oldVisible); - } - } - - private void executeVisibleChange(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - boolean newVisible = (Boolean) params[2]; - part.setVisible(newVisible); - } - } - - private void undoVisibleChange(Object... params) { - if (params.length >= 2 && params[0] instanceof ModelPart part) { - boolean oldVisible = (Boolean) params[1]; - part.setVisible(oldVisible); - } - } - - private void handlePivotRecord(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - Vector2f oldPivot = (Vector2f) params[1]; - Vector2f newPivot = (Vector2f) params[2]; - - String key = "pivot_undo_" + part.getName(); - operationContext.put(key, oldPivot); - } - } - - private void executePivotChange(Object... params) { - if (params.length >= 3 && params[0] instanceof ModelPart part) { - Vector2f newPivot = (Vector2f) params[2]; - part.setPivot(newPivot.x, newPivot.y); - } - } - - private void undoPivotChange(Object... params) { - if (params.length >= 2 && params[0] instanceof ModelPart part) { - Vector2f oldPivot = (Vector2f) params[1]; - part.setPivot(oldPivot.x, oldPivot.y); - } - } - - private void handleAddPartRecord(Object... params) { - } - - private void handleRemovePartRecord(Object... params) { - } - - private void handleRenamePartRecord(Object... params) { - } - - private void handleDragPartRecord(Object... params) { - } - - private void handleAddMeshRecord(Object... params) { - } - - private void handleRemoveMeshRecord(Object... params) { - } - - private void handleBindTextureRecord(Object... params) { - } - - /** - * 获取操作统计信息 - */ - public Map getOperationStatistics() { - return new HashMap<>(operationCounts); - } - - /** - * 获取详细统计报告 - */ - public String getDetailedStatistics() { - StringBuilder sb = new StringBuilder(); - sb.append("操作统计报告:\n"); - sb.append("==============\n"); - - int totalOperations = operationCounts.values().stream().mapToInt(Integer::intValue).sum(); - sb.append("总操作次数: ").append(totalOperations).append("\n"); - sb.append("处理的操作类型数: ").append(handledOperations.size()).append("\n\n"); - - sb.append("各操作类型统计:\n"); - operationCounts.entrySet().stream() - .sorted(Map.Entry.comparingByValue().reversed()) - .forEach(entry -> { - sb.append(String.format(" %-20s: %d 次\n", entry.getKey(), entry.getValue())); - }); - - return sb.toString(); - } - - /** - * 获取已处理的操作类型 - */ - public Set getHandledOperations() { - return new HashSet<>(handledOperations); - } - - /** - * 重置统计信息 - */ - public void resetStatistics() { - operationCounts.clear(); - handledOperations.clear(); - operationContext.clear(); - } - - /** - * 获取操作上下文(用于调试) - */ - public Map getOperationContext() { - return new HashMap<>(operationContext); - } - - /** - * 检查特定操作类型的统计 - */ - public int getOperationCount(String operationType) { - return operationCounts.getOrDefault(operationType, 0); - } - - /** - * 获取最频繁的操作类型 - */ - public String getMostFrequentOperation() { - return operationCounts.entrySet().stream() - .max(Map.Entry.comparingByValue()) - .map(Map.Entry::getKey) - .orElse("无操作"); - } - } - - public static class MeshState { - public String name; - public float[] vertices; - public float[] originalVertices; - public Vector2f originalPivot; - Object texture; - - public MeshState(String name, float[] vertices, float[] originalVertices, - Vector2f originalPivot, Object texture) { - this.name = name; - this.vertices = vertices != null ? vertices.clone() : null; - this.originalVertices = originalVertices != null ? originalVertices.clone() : null; - this.originalPivot = originalPivot != null ? new Vector2f(originalPivot) : null; - this.texture = texture; - } - } - - /** - * 注册操作记录器 - */ - public void registerOperationRecorder(String operationType, OperationRecorder recorder) { - if (operationType == null || recorder == null) { - throw new IllegalArgumentException("Operation type and recorder cannot be null"); - } - - registeredOperations.add(operationType); - recorderMap.put(operationType, recorder); - - // 同时注册到历史管理器 - if (historyManager != null) { - historyManager.registerRecorder(operationType, recorder); - } - - LOGGER.debug("已注册操作类型: {}", operationType); - } - - /** - * 注销操作记录器 - */ - public void unregisterOperationRecorder(String operationType) { - if (operationType != null) { - registeredOperations.remove(operationType); - recorderMap.remove(operationType); - LOGGER.debug("已注销操作类型: {}", operationType); - } - } - - /** - * 记录操作 - */ - public void recordOperation(String operationType, Object... params) { - if (!isOperationRegistered(operationType)) { - LOGGER.warn("未注册的操作类型: {}", operationType); - return; - } - - if (historyManager != null) { - historyManager.recordOperation(operationType, params); - notifyListeners(operationType, "record", params); - } - } - - /** - * 执行撤回操作 - */ - public boolean undo() { - if (historyManager != null && historyManager.canUndo()) { - boolean success = historyManager.undo(); - if (success) { - String description = historyManager.getUndoDescription(); - notifyListeners("SYSTEM", "undo", description); - //System.out.println("撤回操作: " + description); - } - return success; - } - return false; - } - - /** - * 执行重做操作 - */ - public boolean redo() { - if (historyManager != null && historyManager.canRedo()) { - boolean success = historyManager.redo(); - if (success) { - String description = historyManager.getRedoDescription(); - notifyListeners("SYSTEM", "redo", description); - LOGGER.debug("重做操作: {}", description); - } - return success; - } - return false; - } - - /** - * 添加操作监听器 - */ - public void addOperationListener(OperationListener listener) { - if (listener != null && !listeners.contains(listener)) { - listeners.add(listener); - LOGGER.debug("已添加操作监听器: {}", listener.getClass().getSimpleName()); - } - } - - /** - * 移除操作监听器 - */ - public void removeOperationListener(OperationListener listener) { - if (listeners.remove(listener)) { - LOGGER.debug("已移除操作监听器: {}", listener.getClass().getSimpleName()); - } - } - - /** - * 移除默认监听器(谨慎使用) - */ - public void removeDefaultListener() { - removeOperationListener(defaultListener); - } - - /** - * 通知所有监听器 - */ - private void notifyListeners(String operationType, String action, Object... params) { - for (OperationListener listener : listeners) { - try { - listener.onOperationEvent(operationType, action, params); - } catch (Exception e) { - LOGGER.error("操作监听器执行失败: {}", e.getMessage()); - e.printStackTrace(); - } - } - } - - /** - * 检查操作类型是否已注册 - */ - public boolean isOperationRegistered(String operationType) { - return registeredOperations.contains(operationType); - } - - /** - * 获取所有已注册的操作类型 - */ - public Set getRegisteredOperations() { - return new HashSet<>(registeredOperations); - } - - /** - * 获取操作记录器 - */ - public OperationRecorder getOperationRecorder(String operationType) { - return recorderMap.get(operationType); - } - - /** - * 清空操作历史 - */ - public void clearHistory() { - if (historyManager != null) { - historyManager.clear(); - notifyListeners("SYSTEM", "clear", "操作历史已清空"); - } - } - - /** - * 获取操作历史管理器 - */ - public OperationHistoryManager getHistoryManager() { - return historyManager; - } - - /** - * 检查是否可以撤回 - */ - public boolean canUndo() { - return historyManager != null && historyManager.canUndo(); - } - - /** - * 检查是否可以重做 - */ - public boolean canRedo() { - return historyManager != null && historyManager.canRedo(); - } - - /** - * 获取撤回操作描述 - */ - public String getUndoDescription() { - return historyManager != null ? historyManager.getUndoDescription() : ""; - } - - /** - * 获取重做操作描述 - */ - public String getRedoDescription() { - return historyManager != null ? historyManager.getRedoDescription() : ""; - } - - /** - * 批量注册操作记录器 - */ - public void registerOperationRecorders(Map recorders) { - if (recorders != null) { - for (Map.Entry entry : recorders.entrySet()) { - registerOperationRecorder(entry.getKey(), entry.getValue()); - } - } - } - - /** - * 获取默认监听器的统计信息 - */ - public Map getDefaultListenerStatistics() { - return defaultListener.getOperationStatistics(); - } - - /** - * 获取默认监听器处理的操作用户 - */ - public Set getDefaultListenerHandledOperations() { - return defaultListener.getHandledOperations(); - } - - /** - * 重置默认监听器统计信息 - */ - public void resetDefaultListenerStatistics() { - defaultListener.resetStatistics(); - } - - /** - * 获取操作统计信息 - */ - public OperationStatistics getStatistics() { - return new OperationStatistics( - registeredOperations.size(), - historyManager != null ? getHistorySize() : 0, - canUndo(), - canRedo() - ); - } - - /** - * 获取历史记录大小(估算) - */ - private int getHistorySize() { - // 这里可以通过反射或其他方式获取历史记录的实际大小 - // 暂时返回估算值 - return 0; - } - - /** - * 操作统计信息类 - */ - public static class OperationStatistics { - private final int registeredOperationCount; - private final int historySize; - private final boolean canUndo; - private final boolean canRedo; - - public OperationStatistics(int registeredOperationCount, int historySize, - boolean canUndo, boolean canRedo) { - this.registeredOperationCount = registeredOperationCount; - this.historySize = historySize; - this.canUndo = canUndo; - this.canRedo = canRedo; - } - - public int getRegisteredOperationCount() { - return registeredOperationCount; - } - - public int getHistorySize() { - return historySize; - } - - public boolean canUndo() { - return canUndo; - } - - public boolean canRedo() { - return canRedo; - } - - @Override - public String toString() { - return String.format( - "OperationStatistics{注册操作数=%d, 历史记录=%d, 可撤回=%s, 可重做=%s}", - registeredOperationCount, historySize, canUndo, canRedo - ); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryManager.java deleted file mode 100644 index 0ab99f7..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryManager.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.util; - -import java.util.HashMap; -import java.util.LinkedList; -import java.util.Map; - -/** - * 操作记录管理器 - * 负责管理操作的撤回和重做 - * - * @author tzdwindows 7 - */ -public class OperationHistoryManager { - private static final OperationHistoryManager instance = new OperationHistoryManager(); - // 操作记录栈 - private final LinkedList undoStack; - private final LinkedList redoStack; - - // 最大记录数量 - private final int maxHistorySize; - - // 操作记录器映射 - private final Map recorderMap; - - // 是否启用记录 - private boolean enabled = true; - - public OperationHistoryManager() { - this(1000); - } - - public OperationHistoryManager(int maxHistorySize) { - this.maxHistorySize = maxHistorySize; - this.undoStack = new LinkedList<>(); - this.redoStack = new LinkedList<>(); - this.recorderMap = new HashMap<>(); - } - - /** - * 获取操作记录管理器实例 - * - * @return 操作记录管理器实例 - */ - public static OperationHistoryManager getInstance() { - return instance; - } - - /** - * 注册操作记录器 - * - * @param operationType 操作类型标识 - * @param recorder 操作记录器 - */ - public void registerRecorder(String operationType, OperationRecorder recorder) { - recorderMap.put(operationType, recorder); - } - - /** - * 记录操作 - * - * @param operationType 操作类型 - * @param params 操作参数 - */ - public void recordOperation(String operationType, Object... params) { - if (!enabled) return; - - OperationRecorder recorder = recorderMap.get(operationType); - if (recorder == null) { - System.err.println("未注册的操作类型: " + operationType); - return; - } - - // 创建操作记录 - OperationRecord record = new OperationRecord(operationType, params, recorder.getDescription()); - - // 添加到撤回栈 - undoStack.push(record); - - // 限制栈大小 - if (undoStack.size() > maxHistorySize) { - undoStack.removeLast(); - } - - // 清空重做栈(新操作后重做栈无效) - redoStack.clear(); - - //System.out.println("记录操作: " + record.getDescription()); - } - - /** - * 撤回操作 - */ - public boolean undo() { - if (undoStack.isEmpty()) { - System.out.println("没有可撤回的操作"); - return false; - } - - OperationRecord record = undoStack.pop(); - OperationRecorder recorder = recorderMap.get(record.getOperationType()); - - if (recorder != null) { - try { - // 禁用记录,避免撤回操作被记录 - enabled = false; - recorder.undo(record.getParams()); - // 添加到重做栈 - redoStack.push(record); - //System.out.println("撤回操作: " + record.getDescription()); - return true; - } catch (Exception e) { - System.err.println("撤回操作失败: " + record.getDescription()); - e.printStackTrace(); - // 操作失败,放回撤回栈 - undoStack.push(record); - return false; - } finally { - enabled = true; - } - } - - return false; - } - - /** - * 重做操作 - */ - public boolean redo() { - if (redoStack.isEmpty()) { - System.out.println("没有可重做的操作"); - return false; - } - - OperationRecord record = redoStack.pop(); - OperationRecorder recorder = recorderMap.get(record.getOperationType()); - - if (recorder != null) { - try { - // 禁用记录,避免重做操作被记录 - enabled = false; - recorder.execute(record.getParams()); - // 放回撤回栈 - undoStack.push(record); - System.out.println("重做操作: " + record.getDescription()); - return true; - } catch (Exception e) { - System.err.println("重做操作失败: " + record.getDescription()); - e.printStackTrace(); - // 操作失败,放回重做栈 - redoStack.push(record); - return false; - } finally { - enabled = true; - } - } - - return false; - } - - /** - * 清空所有记录 - */ - public void clear() { - undoStack.clear(); - redoStack.clear(); - } - - /** - * 是否可以撤回 - */ - public boolean canUndo() { - return !undoStack.isEmpty(); - } - - /** - * 是否可以重做 - */ - public boolean canRedo() { - return !redoStack.isEmpty(); - } - - /** - * 获取撤回操作描述 - */ - public String getUndoDescription() { - return undoStack.isEmpty() ? "" : undoStack.peek().getDescription(); - } - - /** - * 获取重做操作描述 - */ - public String getRedoDescription() { - return redoStack.isEmpty() ? "" : redoStack.peek().getDescription(); - } - - /** - * 操作记录内部类 - */ - private static class OperationRecord { - private final String operationType; - private final Object[] params; - private final String description; - private final long timestamp; - - public OperationRecord(String operationType, Object[] params, String description) { - this.operationType = operationType; - this.params = params != null ? params.clone() : new Object[0]; - this.description = description; - this.timestamp = System.currentTimeMillis(); - } - - public String getOperationType() { - return operationType; - } - - public Object[] getParams() { - return params; - } - - public String getDescription() { - return description; - } - - public long getTimestamp() { - return timestamp; - } - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationListener.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationListener.java deleted file mode 100644 index 9242d8c..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationListener.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.util; - -/** - * 操作监听器接口 - * 用于监听操作历史事件 - */ -public interface OperationListener { - - /** - * 操作事件回调 - * - * @param operationType 操作类型 - * @param action 动作类型(record, execute, undo, redo, clear) - * @param params 操作参数 - */ - void onOperationEvent(String operationType, String action, Object... params); -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationRecorder.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationRecorder.java deleted file mode 100644 index 0580109..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationRecorder.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.util; - -/** - * 操作记录接口 - * 用于注册需要支持撤回/重做的操作 - * - * @author tzdwindows 7 - */ -public interface OperationRecorder { - - /** - * 执行操作(用于重做) - * - * @param params 操作参数 - */ - void execute(Object... params); - - /** - * 撤销操作 - * - * @param params 操作参数 - */ - void undo(Object... params); - - /** - * 获取操作描述(用于UI显示) - * - * @return 操作描述 - */ - String getDescription(); -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSDImporter.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSDImporter.java deleted file mode 100644 index 431fcdf..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSDImporter.java +++ /dev/null @@ -1,185 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.util; - -import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel; -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.Texture; - -import javax.swing.*; -import java.io.File; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class PSDImporter { - private final Model2D model; - private final ModelRenderPanel renderPanel; - private final ModelLayerPanel layerPanel; - - public PSDImporter(Model2D model, ModelRenderPanel renderPanel, ModelLayerPanel layerPanel) { - this.model = model; - this.renderPanel = renderPanel; - this.layerPanel = layerPanel; - } - - public void importPSDFile(File psdFile) { - try { - PsdParser.PSDImportResult result = PsdParser.parsePSDFile(psdFile); - if (result != null && !result.layers.isEmpty()) { - int choice = JOptionPane.showConfirmDialog(null, - String.format("PSD文件包含 %d 个图层,是否全部导入?", result.layers.size()), - "导入PSD图层", JOptionPane.YES_NO_OPTION); - - if (choice == JOptionPane.YES_OPTION) { - importPSDLayers(result); - } - } - } catch (Exception ex) { - JOptionPane.showMessageDialog(null, - "解析PSD文件失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - } - } - - private void importPSDLayers(PsdParser.PSDImportResult result) { - if (renderPanel != null) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - List createdParts = createPartsFromPSDLayers(result.layers); - SwingUtilities.invokeLater(() -> notifyImportComplete(createdParts)); - } catch (Exception e) { - SwingUtilities.invokeLater(() -> - showError("导入PSD图层失败: " + e.getMessage())); - } - }); - } else { - List createdParts = createPartsFromPSDLayers(result.layers); - notifyImportComplete(createdParts); - } - } - - private List createPartsFromPSDLayers(List layers) { - List createdParts = new ArrayList<>(); - for (PsdParser.PSDLayerInfo layerInfo : layers) { - ModelPart part = createPartFromPSDLayer(layerInfo); - if (part != null) { - createdParts.add(part); - } - } - return createdParts; - } - - private ModelPart createPartFromPSDLayer(PsdParser.PSDLayerInfo layerInfo) { - try { - System.out.println("正在创建PSD图层: " + layerInfo.name + " [" + - layerInfo.width + "x" + layerInfo.height + "]" + "[x=" + layerInfo.x + ",y=" + layerInfo.y + "]"); - - // 确保部件名唯一,避免覆盖已有部件导致"合并成一个图层"的问题 - String uniqueName = ensureUniquePartName(layerInfo.name); - - // 创建部件 - ModelPart part = model.createPart(uniqueName); - if (part == null) { - System.err.println("创建部件失败: " + uniqueName); - return null; - } - - // 如果 model 有 partMap,更新映射(防止老实现以 name 为 key 覆盖或冲突) - try { - Map partMap = layerPanel.getModelPartMap(); - if (partMap != null) { - partMap.put(uniqueName, part); - } - } catch (Exception ignored) { - } - - part.setVisible(layerInfo.visible); - - // 设置不透明度(优先使用公开方法) - try { - part.setOpacity(layerInfo.opacity); - } catch (Throwable t) { - // 如果没有公开方法,尝试通过反射备用(保持兼容) - try { - Field f = part.getClass().getDeclaredField("opacity"); - f.setAccessible(true); - f.setFloat(part, layerInfo.opacity); - } catch (Throwable ignored) { - System.err.println("设置不透明度失败: " + uniqueName); - } - } - part.setPosition(layerInfo.x, layerInfo.y); - - // 创建网格(使用唯一 mesh 名避免工厂复用同一实例) - long uniq = System.nanoTime(); - Mesh2D mesh = MeshTextureUtil.createQuadForImage(layerInfo.image, uniqueName + "_mesh_" + uniq); - - // 把 mesh 加入 part(注意部分实现可能复制或包装 mesh) - part.addMesh(mesh); - - // 创建纹理(使用唯一名称,防止按 name 在内部被复用或覆盖) - String texName = uniqueName + "_tex_" + uniq; - Texture texture = layerPanel.createTextureFromBufferedImage(layerInfo.image, texName); - try { - List partMeshes = part.getMeshes(); - Mesh2D actualMesh = null; - if (partMeshes != null && !partMeshes.isEmpty()) { - actualMesh = partMeshes.get(partMeshes.size() - 1); - } - - if (actualMesh != null) { - actualMesh.setTexture(texture); - } else { - mesh.setTexture(texture); - } - model.addTexture(texture); - model.markNeedsUpdate(); - } catch (Throwable e) { - System.err.println("在绑定纹理到 mesh 时出错: " + uniqueName + " - " + e.getMessage()); - e.printStackTrace(); - } - SwingUtilities.invokeLater(() -> { - try { - layerPanel.reloadFromModel(); - } catch (Throwable ignored) { - } - try { - if (renderPanel != null) renderPanel.repaint(); - } catch (Throwable ignored) { - } - }); - - return part; - - } catch (Exception e) { - System.err.println("创建PSD图层部件失败: " + layerInfo.name + " - " + e.getMessage()); - e.printStackTrace(); - return null; - } - } - - private String ensureUniquePartName(String baseName) { - if (model == null) return baseName; - Map partMap = layerPanel.getModelPartMap(); - if (partMap == null) return baseName; - String name = baseName; - int idx = 1; - while (partMap.containsKey(name)) { - name = baseName + "_" + idx++; - } - return name; - } - - private void notifyImportComplete(List createdParts) { - if (model != null) { - model.markNeedsUpdate(); - } - // 通知监听器导入完成 - } - - private void showError(String message) { - JOptionPane.showMessageDialog(null, message, "错误", JOptionPane.ERROR_MESSAGE); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSD_Structure_Dumper.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSD_Structure_Dumper.java deleted file mode 100644 index 3153203..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSD_Structure_Dumper.java +++ /dev/null @@ -1,167 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.util; - -import java.io.*; -import java.nio.charset.StandardCharsets; - -/** - * PSD文件结构诊断工具 - * 目的:打印出“图层和蒙版信息区段”的详细结构,用于分析非标准PSD文件。 - */ -public class PSD_Structure_Dumper { - - private static final int PREVIEW_BYTES = 16; // 预览的字节数 - - public static void dump(File file) { - System.out.println("=========================================================="); - System.out.println("开始诊断PSD文件: " + file.getName()); - System.out.println("=========================================================="); - - try (FileInputStream fis = new FileInputStream(file); - DataInputStream dis = new DataInputStream(new BufferedInputStream(fis))) { - - // 1. 跳过文件头、颜色模式区、图像资源区 - if (!"8BPS".equals(readString(dis, 4))) throw new IOException("非法的PSD文件签名"); - skipFully(dis, 22); - skipFully(dis, readUInt32(dis)); // Color mode data - skipFully(dis, readUInt32(dis)); // Image resources - - // 2. 进入“图层和蒙版信息区段” - long layerAndMaskLength = readUInt32(dis); - if (layerAndMaskLength == 0) { - System.out.println("文件不包含“图层和蒙版信息区段”。"); - return; - } - System.out.printf("发现“图层和蒙版信息区段”,总长度: %d%n", layerAndMaskLength); - long sectionEndPos = fis.getChannel().position() + layerAndMaskLength; - - long layerInfoLength = readUInt32(dis); - System.out.printf(" - 图层信息块长度: %d%n", layerInfoLength); - if (layerInfoLength == 0) return; - - int layerCount = dis.readShort(); - System.out.printf(" - 文件报告的图层数量: %d%n", layerCount); - if (layerCount < 0) layerCount = -layerCount; - - // 3. 逐一打印每个图层记录的结构 - for (int i = 0; i < layerCount; i++) { - System.out.println("\n--- 开始解析图层记录 " + i + " ---"); - long layerRecordStartPos = fis.getChannel().position(); - System.out.printf("[偏移: %d] 图层坐标 (Top, Left, Bottom, Right): %d, %d, %d, %d%n", - layerRecordStartPos, dis.readInt(), dis.readInt(), dis.readInt(), dis.readInt()); - - int channels = dis.readShort(); - System.out.printf("[偏移: %d] 通道数量: %d. 跳过 %d 字节的通道信息.%n", fis.getChannel().position(), channels, channels * 6); - skipFully(dis, (long) channels * 6); - - String blendSig = readString(dis, 4); - System.out.printf("[偏移: %d] 混合模式签名: '%s'%n", fis.getChannel().position() - 4, blendSig); - if (!"8BIM".equals(blendSig)) { - System.out.println("!!! 错误: 此处签名不是 '8BIM',解析可能已出错。"); - } - - String blendMode = readString(dis, 4); - System.out.printf("[偏移: %d] 混合模式Key: '%s'%n", fis.getChannel().position() - 4, blendMode); - skipFully(dis, 4); // Opacity, Clipping, Flags - - int extraDataLen = dis.readInt(); - System.out.printf("[偏移: %d] 额外数据总长度: %d%n", fis.getChannel().position() - 4, extraDataLen); - long extraDataEndPos = fis.getChannel().position() + extraDataLen; - - // 4. 遍历额外数据中的所有附加信息块 (这是关键) - System.out.println(" --- 遍历额外数据块 ---"); - while (fis.getChannel().position() < extraDataEndPos) { - long blockStartPos = fis.getChannel().position(); - String sig = readString(dis, 4); - if (!"8BIM".equals(sig) && !"8B64".equals(sig)) { - System.out.printf("[偏移: %d] !!! 发现未知签名 '%s',可能已错位,停止解析此图层。%n", blockStartPos, sig); - break; - } - - String key = readString(dis, 4); - long len = readUInt32(dis); - - System.out.printf(" [偏移: %d] 发现数据块: 签名='%s', Key='%s', 长度=%d%n", blockStartPos, sig, key, len); - - // 特别关注图层名称块 'luni' - if ("luni".equals(key)) { - int nameLen = dis.readInt(); - byte[] nameBytes = new byte[nameLen * 2]; - dis.readFully(nameBytes); - String name = new String(nameBytes, StandardCharsets.UTF_16BE); - System.out.printf(" >> 解码为 'luni' (Unicode图层名称): '%s'%n", name); - // 跳过剩余部分 - long alreadyRead = 4 + nameBytes.length; - if (len - alreadyRead > 0) skipFully(dis, len - alreadyRead); - } else { - // 打印其他块的少量预览数据 - byte[] preview = new byte[(int) Math.min(len, PREVIEW_BYTES)]; - dis.readFully(preview); - System.out.printf(" 预览数据: %s ...%n", bytesToHex(preview)); - if (len - preview.length > 0) { - skipFully(dis, len - preview.length); - } - } - // 确保长度是偶数,Photoshop有时会填充一个字节 - if (len % 2 != 0) { - System.out.println(" 检测到奇数长度,跳过1个填充字节。"); - skipFully(dis, 1); - } - } - System.out.println(" --- 额外数据块遍历结束 ---"); - // 确保指针移动到下一个图层记录的开始 - if (fis.getChannel().position() != extraDataEndPos) { - long diff = extraDataEndPos - fis.getChannel().position(); - System.out.printf("!!! 指针与预期不符,强制跳过 %d 字节以对齐下一个图层%n", diff); - skipFully(dis, diff); - } - } - System.out.println("\n--- 所有图层记录解析完毕 ---"); - - - } catch (Exception e) { - System.out.println("\n!!!!!! 在诊断过程中发生严重错误 !!!!!!"); - e.printStackTrace(); - } finally { - System.out.println("=========================================================="); - System.out.println("诊断结束"); - System.out.println("=========================================================="); - } - } - - // --- 辅助方法 --- - private static String readString(DataInputStream dis, int len) throws IOException { - byte[] bytes = new byte[len]; - dis.readFully(bytes); - return new String(bytes, StandardCharsets.US_ASCII); - } - - private static long readUInt32(DataInputStream dis) throws IOException { - return dis.readInt() & 0xFFFFFFFFL; - } - - private static void skipFully(DataInputStream dis, long bytes) throws IOException { - if (bytes <= 0) return; - long remaining = bytes; - while (remaining > 0) { - long skipped = dis.skip(remaining); - if (skipped <= 0) throw new IOException("Skip failed"); - remaining -= skipped; - } - } - - private static String bytesToHex(byte[] bytes) { - StringBuilder sb = new StringBuilder(); - for (byte b : bytes) { - sb.append(String.format("%02X ", b)); - } - return sb.toString(); - } - - public static void main(String[] args) { - File fileToDiagnose = new File("G:\\鬼畜素材\\工作间\\川普-风催雨\\川普-风催雨.psd"); - System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); - System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); - PSD_Structure_Dumper.dump(fileToDiagnose); - } -} - diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PsdParser.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PsdParser.java deleted file mode 100644 index 45200cf..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PsdParser.java +++ /dev/null @@ -1,451 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.util; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.imageio.ImageIO; -import javax.imageio.ImageReader; -import javax.imageio.metadata.IIOMetadata; -import javax.imageio.stream.ImageInputStream; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.FileInputStream; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -/** - * PSD文件解析工具类 - 基于 TwelveMonkeys PSDMetadata(修复版) - */ -public class PsdParser { - private static final Logger logger = LoggerFactory.getLogger(PsdParser.class); - - public static class PSDLayerInfo { - public String name; - public BufferedImage image; - public float opacity; - public boolean visible; - public int x, y; - public int width, height; - public int left, top, right, bottom; - - public PSDLayerInfo(String name, BufferedImage image, float opacity, boolean visible, - int left, int top, int right, int bottom) { - this.name = name; - this.image = image; - this.opacity = opacity; - this.visible = visible; - this.left = left; - this.top = top; - this.right = right; - this.bottom = bottom; - this.x = left; - this.y = top; - this.width = right - left; - this.height = bottom - top; - } - } - - public static class PSDImportResult { - public List layers = new ArrayList<>(); - public int documentWidth = -1; - public int documentHeight = -1; - public BufferedImage mergedImage; - } - - /** - * 主解析方法 - */ - public static PSDImportResult parsePSDFile(File psdFile) throws Exception { - PSDImportResult result = new PSDImportResult(); - - ImageReader reader = findPSDImageReader(); - if (reader == null) { - throw new RuntimeException("系统不支持PSD文件格式,请安装 TwelveMonkeys imageio-psd 插件"); - } - - try (FileInputStream fis = new FileInputStream(psdFile); - ImageInputStream iis = ImageIO.createImageInputStream(fis)) { - - reader.setInput(iis); - - // 读取文档尺寸 - result.documentWidth = reader.getWidth(0); - result.documentHeight = reader.getHeight(0); - logger.info("文档尺寸: {}x{}", result.documentWidth, result.documentHeight); - - // 获取元数据 - IIOMetadata metadata = reader.getImageMetadata(0); - - // 尝试从元数据中提取图层信息 - if (metadata != null) { - extractLayerInfoFromMetadata(metadata, result, reader); - } else { - logger.warn("无法获取元数据,使用备用解析方法"); - parseUsingImageIndices(reader, result); - } - - // 读取合并图像 - try { - result.mergedImage = reader.read(0); - } catch (Exception e) { - logger.warn("无法读取合并图像: {}", e.getMessage()); - } - - } finally { - try { - reader.dispose(); - } catch (Exception ignored) { - } - } - - return result; - } - - /** - * 从元数据中提取图层信息 - */ - private static void extractLayerInfoFromMetadata(IIOMetadata metadata, - PSDImportResult result, - ImageReader reader) { - try { - // 尝试访问 TwelveMonkeys 的 PSDMetadata - if (metadata.getClass().getName().equals("com.twelvemonkeys.imageio.plugins.psd.PSDMetadata")) { - extractFromPSDMetadata(metadata, result, reader); - } else { - // 尝试从标准元数据格式中提取 - extractFromStandardMetadata(metadata, result, reader); - } - } catch (Exception e) { - logger.error("从元数据提取图层信息失败: {}", e.getMessage()); - parseUsingImageIndices(reader, result); - } - } - - /** - * 从 TwelveMonkeys 的 PSDMetadata 中提取图层信息 - */ - private static void extractFromPSDMetadata(IIOMetadata metadata, - PSDImportResult result, - ImageReader reader) { - try { - // 使用反射访问 PSDMetadata 的私有字段 - Class psdMetadataClass = metadata.getClass(); - - // 获取 layerInfo 字段 - Field layerInfoField = psdMetadataClass.getDeclaredField("layerInfo"); - layerInfoField.setAccessible(true); - - @SuppressWarnings("unchecked") - List layerInfos = (List) layerInfoField.get(metadata); - - if (layerInfos != null && !layerInfos.isEmpty()) { - logger.info("从 PSDMetadata 中找到 {} 个图层", layerInfos.size()); - - for (int i = 0; i < layerInfos.size(); i++) { - try { - Object twelveMonkeysLayer = layerInfos.get(i); - PSDLayerInfo layer = createLayerInfoFromTwelveMonkeys(twelveMonkeysLayer, reader, i); - if (layer != null) { - result.layers.add(layer); - } - } catch (Exception e) { - logger.error("处理图层 {} 失败: {}", i, e.getMessage()); - } - } - } else { - logger.info("PSDMetadata 中没有图层信息,使用图像索引方式"); - parseUsingImageIndices(reader, result); - } - - } catch (Exception e) { - logger.error("访问 PSDMetadata 失败: {}", e.getMessage()); - parseUsingImageIndices(reader, result); - } - } - - /** - * 从 TwelveMonkeys 的图层对象创建我们的图层信息 - */ - private static PSDLayerInfo createLayerInfoFromTwelveMonkeys(Object twelveMonkeysLayer, - ImageReader reader, - int layerIndex) { - try { - Class layerClass = twelveMonkeysLayer.getClass(); - - // 提取基本几何信息 - int top = getIntField(layerClass, twelveMonkeysLayer, "top"); - int left = getIntField(layerClass, twelveMonkeysLayer, "left"); - int bottom = getIntField(layerClass, twelveMonkeysLayer, "bottom"); - int right = getIntField(layerClass, twelveMonkeysLayer, "right"); - - // 提取图层名称 - String layerName = extractLayerName(layerClass, twelveMonkeysLayer); - - // 提取可见性和不透明度 - boolean visible = extractVisibility(layerClass, twelveMonkeysLayer); - float opacity = extractOpacity(layerClass, twelveMonkeysLayer); - - // 读取图层图像 - BufferedImage layerImage = readLayerImage(reader, layerIndex); - - // 如果无法读取图像,创建占位符 - if (layerImage == null) { - int width = right - left; - int height = bottom - top; - if (width > 0 && height > 0) { - layerImage = createPlaceholderImage(width, height); - } - } - - return new PSDLayerInfo(layerName, layerImage, opacity, visible, left, top, right, bottom); - - } catch (Exception e) { - logger.error("创建图层信息失败: {}", e.getMessage()); - return null; - } - } - - /** - * 提取图层名称 - */ - private static String extractLayerName(Class layerClass, Object layer) { - try { - // 先尝试获取 unicodeLayerName - Field unicodeNameField = layerClass.getDeclaredField("unicodeLayerName"); - unicodeNameField.setAccessible(true); - String unicodeName = (String) unicodeNameField.get(layer); - if (unicodeName != null && !unicodeName.trim().isEmpty()) { - return unicodeName.trim(); - } - - // 然后尝试获取 layerName - Field nameField = layerClass.getDeclaredField("layerName"); - nameField.setAccessible(true); - String name = (String) nameField.get(layer); - if (name != null && !name.trim().isEmpty()) { - return name.trim(); - } - } catch (Exception e) { - logger.debug("无法提取图层名称: {}", e.getMessage()); - } - - return "Layer_" + System.identityHashCode(layer); - } - - /** - * 提取可见性 - */ - private static boolean extractVisibility(Class layerClass, Object layer) { - try { - Field blendModeField = layerClass.getDeclaredField("blendMode"); - blendModeField.setAccessible(true); - Object blendMode = blendModeField.get(layer); - - if (blendMode != null) { - Class blendModeClass = blendMode.getClass(); - Field flagsField = blendModeClass.getDeclaredField("flags"); - flagsField.setAccessible(true); - int flags = flagsField.getInt(blendMode); - // 第2位为0表示可见 - return (flags & 0x02) == 0; - } - } catch (Exception e) { - logger.debug("无法提取可见性: {}", e.getMessage()); - } - return true; // 默认可见 - } - - /** - * 提取不透明度 - */ - private static float extractOpacity(Class layerClass, Object layer) { - try { - Field blendModeField = layerClass.getDeclaredField("blendMode"); - blendModeField.setAccessible(true); - Object blendMode = blendModeField.get(layer); - - if (blendMode != null) { - Class blendModeClass = blendMode.getClass(); - Field opacityField = blendModeClass.getDeclaredField("opacity"); - opacityField.setAccessible(true); - int opacity = opacityField.getInt(blendMode); - return opacity / 255.0f; // 转换为 0.0-1.0 范围 - } - } catch (Exception e) { - logger.debug("无法提取不透明度: {}", e.getMessage()); - } - return 1.0f; // 默认不透明度 - } - - /** - * 获取整数字段值 - */ - private static int getIntField(Class clazz, Object obj, String fieldName) { - try { - Field field = clazz.getDeclaredField(fieldName); - field.setAccessible(true); - return field.getInt(obj); - } catch (Exception e) { - logger.debug("无法获取字段 {}: {}", fieldName, e.getMessage()); - return 0; - } - } - - /** - * 读取图层图像 - */ - private static BufferedImage readLayerImage(ImageReader reader, int layerIndex) { - try { - // 图层索引从1开始(0是合并图像) - int imageIndex = layerIndex + 1; - if (imageIndex < reader.getNumImages(true)) { - return reader.read(imageIndex); - } - } catch (Exception e) { - logger.debug("无法读取图层 {} 的图像: {}", layerIndex, e.getMessage()); - } - return null; - } - - /** - * 从标准元数据格式中提取图层信息 - */ - private static void extractFromStandardMetadata(IIOMetadata metadata, - PSDImportResult result, - ImageReader reader) { - try { - // 尝试从标准元数据节点中提取图层信息 - org.w3c.dom.Node tree = metadata.getAsTree("com_twelvemonkeys_imageio_psd_image_1.0"); - if (tree != null) { - extractFromMetadataTree(tree, result, reader); - } else { - parseUsingImageIndices(reader, result); - } - } catch (Exception e) { - logger.error("从标准元数据提取失败: {}", e.getMessage()); - parseUsingImageIndices(reader, result); - } - } - - /** - * 从元数据树中提取图层信息 - */ - private static void extractFromMetadataTree(org.w3c.dom.Node tree, - PSDImportResult result, - ImageReader reader) { - // 这里可以添加从 DOM 树中解析图层信息的逻辑 - // 由于比较复杂,暂时使用备用方法 - parseUsingImageIndices(reader, result); - } - - /** - * 备用方法:使用图像索引解析图层 - */ - private static void parseUsingImageIndices(ImageReader reader, PSDImportResult result) { - try { - int numImages = reader.getNumImages(true); - logger.info("使用图像索引方式,找到 {} 个图像", numImages); - - // 从索引1开始读取图层(索引0是合并图像) - for (int i = 1; i < numImages; i++) { - try { - BufferedImage layerImage = reader.read(i); - if (layerImage != null) { - String layerName = "Layer_" + i; - PSDLayerInfo layer = new PSDLayerInfo( - layerName, layerImage, 1.0f, true, - 0, 0, layerImage.getWidth(), layerImage.getHeight() - ); - result.layers.add(layer); - - logger.info("读取图层 {}: '{}' 尺寸 {}x{}", i, layerName, layerImage.getWidth(), layerImage.getHeight()); - } - } catch (Exception e) { - logger.error("读取图层 {} 失败: {}", i, e.getMessage()); - } - } - } catch (Exception e) { - logger.error("使用图像索引方式解析失败: {}", e.getMessage()); - } - } - - /** - * 创建占位符图像 - */ - private static BufferedImage createPlaceholderImage(int width, int height) { - if (width <= 0 || height <= 0) { - return null; - } - - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int alpha = 128; - int red = (x * 255 / width) & 0xFF; - int green = (y * 255 / height) & 0xFF; - int blue = 128; - - int rgb = (alpha << 24) | (red << 16) | (green << 8) | blue; - image.setRGB(x, y, rgb); - } - } - - return image; - } - - /** - * 查找 PSD ImageReader - */ - private static ImageReader findPSDImageReader() { - try { - Iterator it = ImageIO.getImageReadersByFormatName("psd"); - if (it.hasNext()) return it.next(); - it = ImageIO.getImageReadersByMIMEType("image/vnd.adobe.photoshop"); - if (it.hasNext()) return it.next(); - it = ImageIO.getImageReadersBySuffix("psd"); - if (it.hasNext()) return it.next(); - } catch (Exception e) { - logger.debug("查找 PSD ImageReader 失败: {}", e.getMessage()); - } - return null; - } - - public static boolean isPSDSupported() { - return findPSDImageReader() != null; - } - - public static String getPSDSupportInfo() { - ImageReader r = findPSDImageReader(); - return (r != null) ? ("PSD 支持: " + r.getClass().getName()) : "PSD 不支持(请安装 TwelveMonkeys imageio-psd 插件)"; - } - - /** - * 简单测试方法 - */ - public static void main(String[] args) { - try { - File psdFile = new File("test.psd"); - if (!psdFile.exists()) { - System.out.println("测试文件不存在: " + psdFile.getAbsolutePath()); - return; - } - - PSDImportResult result = parsePSDFile(psdFile); - - System.out.println("文档尺寸: " + result.documentWidth + "x" + result.documentHeight); - System.out.println("图层数量: " + result.layers.size()); - - for (PSDLayerInfo layer : result.layers) { - System.out.printf("图层: %s, 位置: (%d, %d), 尺寸: %dx%d, 可见: %s, 不透明度: %.2f%n", - layer.name, layer.x, layer.y, layer.width, layer.height, - layer.visible, layer.opacity); - } - - } catch (Exception e) { - e.printStackTrace(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerCellRenderer.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerCellRenderer.java deleted file mode 100644 index 94d3a08..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerCellRenderer.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.util.renderer; - -import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel; -import com.chuangzhou.vivid2D.render.awt.manager.ThumbnailManager; -import com.chuangzhou.vivid2D.render.model.ModelPart; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; - -public class LayerCellRenderer extends JPanel implements ListCellRenderer { - private static final int THUMBNAIL_WIDTH = 48; - private static final int THUMBNAIL_HEIGHT = 48; - - private final JCheckBox visibleBox = new JCheckBox(); - private final JLabel nameLabel = new JLabel(); - private final JLabel opacityLabel = new JLabel(); - private final JLabel thumbnailLabel = new JLabel(); - - private final ModelLayerPanel layerPanel; - private final ThumbnailManager thumbnailManager; - - public LayerCellRenderer(ModelLayerPanel layerPanel, ThumbnailManager thumbnailManager) { - this.layerPanel = layerPanel; - this.thumbnailManager = thumbnailManager; - initComponents(); - } - - private void initComponents() { - setLayout(new BorderLayout(6, 6)); - - // 左侧:缩略图 - thumbnailLabel.setPreferredSize(new Dimension(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)); - thumbnailLabel.setOpaque(true); - thumbnailLabel.setBackground(new Color(60, 60, 60)); - thumbnailLabel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); - - // 中间:可见性复选框和名称 - JPanel centerPanel = new JPanel(new BorderLayout(4, 0)); - centerPanel.setOpaque(false); - - JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); - leftPanel.setOpaque(false); - visibleBox.setOpaque(false); - leftPanel.add(visibleBox); - leftPanel.add(nameLabel); - - centerPanel.add(leftPanel, BorderLayout.CENTER); - centerPanel.add(opacityLabel, BorderLayout.EAST); - - add(thumbnailLabel, BorderLayout.WEST); - add(centerPanel, BorderLayout.CENTER); - } - - public void attachMouseListener(JList layerList, javax.swing.ListModel listModel) { - addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - int idx = layerList.locationToIndex(e.getPoint()); - if (idx >= 0) { - ModelPart part = listModel.getElementAt(idx); - Rectangle cbBounds = visibleBox.getBounds(); - // 调整点击区域检测,考虑缩略图的存在 - cbBounds.x += thumbnailLabel.getWidth() + 6; // 缩略图宽度 + 间距 - if (cbBounds.contains(e.getPoint())) { - boolean newVis = !part.isVisible(); - part.setVisible(newVis); - if (layerPanel.getModel() != null) { - layerPanel.getModel().markNeedsUpdate(); - } - layerPanel.reloadFromModel(); - layerPanel.refreshCurrentThumbnail(); - } else { - layerList.setSelectedIndex(idx); - } - } - } - }); - } - - @Override - public Component getListCellRendererComponent(JList list, ModelPart value, - int index, boolean isSelected, boolean cellHasFocus) { - nameLabel.setText(value.getName()); - opacityLabel.setText(((int) (value.getOpacity() * 100)) + "%"); - visibleBox.setSelected(value.isVisible()); - - // 设置缩略图 - BufferedImage thumbnail = thumbnailManager.getThumbnail(value); - if (thumbnail != null) { - thumbnailLabel.setIcon(new ImageIcon(thumbnail)); - } else { - thumbnailLabel.setIcon(null); - // 如果没有缩略图,生成一个 - SwingUtilities.invokeLater(() -> { - thumbnailManager.generateThumbnail(value); - list.repaint(); - }); - } - - if (isSelected) { - setBackground(list.getSelectionBackground()); - setForeground(list.getSelectionForeground()); - nameLabel.setForeground(list.getSelectionForeground()); - opacityLabel.setForeground(list.getSelectionForeground()); - thumbnailLabel.setBorder(BorderFactory.createLineBorder(list.getSelectionForeground(), 2)); - } else { - setBackground(list.getBackground()); - setForeground(list.getForeground()); - nameLabel.setForeground(list.getForeground()); - opacityLabel.setForeground(list.getForeground()); - thumbnailLabel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); - } - setOpaque(true); - setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); - return this; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerReorderTransferHandler.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerReorderTransferHandler.java deleted file mode 100644 index 72d6a0f..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerReorderTransferHandler.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.util.renderer; - -import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel; - -import javax.swing.*; -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.StringSelection; -import java.awt.datatransfer.Transferable; -import java.util.Arrays; -import java.util.stream.Collectors; - -public class LayerReorderTransferHandler extends TransferHandler { - private final ModelLayerPanel layerPanel; - - public LayerReorderTransferHandler(ModelLayerPanel layerPanel) { - this.layerPanel = layerPanel; - } - - @Override - public Transferable createTransferable(JComponent c) { - if (!(c instanceof JList)) return null; - - JList list = (JList) c; - // 【修正 1:获取所有选中索引】 - int[] srcIndices = list.getSelectedIndices(); - if (srcIndices.length == 0) return null; - - // 将所有选中索引打包成一个逗号分隔的字符串 - String indexString = Arrays.stream(srcIndices) - .mapToObj(String::valueOf) - .collect(Collectors.joining(",")); - - return new StringSelection(indexString); - } - - @Override - public int getSourceActions(JComponent c) { - return MOVE; - } - - @Override - public boolean canImport(TransferSupport support) { - return support.isDrop() && support.isDataFlavorSupported(DataFlavor.stringFlavor); - } - - @Override - public boolean importData(TransferSupport support) { - if (!canImport(support)) return false; - - try { - if (!(support.getComponent() instanceof JList)) return false; - - JList.DropLocation dl = (JList.DropLocation) support.getDropLocation(); - int dropIndex = dl.getIndex(); - - // 【修正 2:解析索引字符串,获取所有被拖拽的源索引】 - String s = (String) support.getTransferable().getTransferData(DataFlavor.stringFlavor); - int[] srcIndices = Arrays.stream(s.split(",")) - .mapToInt(Integer::parseInt) - .toArray(); - - if (srcIndices.length == 0) return false; - - // 检查目标位置是否在拖拽的块内 (minSrc < dropIndex <= maxSrc) - int minSrc = srcIndices[0]; - int maxSrc = srcIndices[srcIndices.length - 1]; - - // 如果 dropIndex 落在 (minSrc, maxSrc] 区间内,则阻止拖拽到自身或内部 - if (dropIndex > minSrc && dropIndex <= maxSrc) { - return false; - } - - // 【修正 3:调用 ModelLayerPanel 中的块重排方法】 - layerPanel.performBlockReorder(srcIndices, dropIndex); - - layerPanel.endDragOperation(); - return true; - - } catch (Exception ex) { - ex.printStackTrace(); - } - return false; - } - - @Override - protected void exportDone(JComponent source, Transferable data, int action) { - if (action == TransferHandler.NONE) { - layerPanel.endDragOperation(); - } - super.exportDone(source, data, action); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/AnimationParameter.java b/src/main/java/com/chuangzhou/vivid2D/render/model/AnimationParameter.java deleted file mode 100644 index 6e5dc22..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/AnimationParameter.java +++ /dev/null @@ -1,220 +0,0 @@ -package com.chuangzhou.vivid2D.render.model; - -import java.util.Collections; -import java.util.Objects; -import java.util.SortedSet; -import java.util.TreeSet; - -public class AnimationParameter { - private final String id; - private float value; - private final float defaultValue; - private final float minValue; - private final float maxValue; - private boolean changed = false; - - private final TreeSet keyframes = new TreeSet<>(); - - public AnimationParameter(String id, float min, float max, float defaultValue) { - this.id = id; - this.minValue = min; - this.maxValue = max; - this.defaultValue = defaultValue; - this.value = defaultValue; - } - - public void setValue(float value) { - float clamped = Math.max(minValue, Math.min(maxValue, value)); - if (this.value != clamped) { - this.value = clamped; - this.changed = true; - } - } - - /** - * @return 一个新的 AnimationParameter 实例,包含相同的配置、值、状态和关键帧。 - */ - public AnimationParameter copy() { - AnimationParameter copy = new AnimationParameter(this.id, this.minValue, this.maxValue, this.defaultValue); - copy.value = this.value; - copy.changed = this.changed; - copy.keyframes.addAll(this.keyframes); - return copy; - } - - public boolean hasChanged() { - return changed; - } - - public void markClean() { - this.changed = false; - } - - public float getValue() { - return value; - } - - public String getId() { - return id; - } - - public float getMinValue() { - return minValue; - } - - public float getMaxValue() { - return maxValue; - } - - public float getDefaultValue() { - return defaultValue; - } - - public void reset() { - setValue(defaultValue); - } - - /** - * 获取归一化值 [0, 1] - */ - public float getNormalizedValue() { - float range = maxValue - minValue; - if (range == 0) return 0; - return (value - minValue) / range; - } - - /** - * 设置归一化值 - */ - public void setNormalizedValue(float normalized) { - float newValue = minValue + normalized * (maxValue - minValue); - setValue(newValue); // 使用 setValue 来确保钳位和 'changed' 标记 - } - - - /** - * 添加一个关键帧。值会被自动钳位(clamp)到 min/max 范围内。 - * @param frameValue 参数值 - * @return 如果成功添加了新帧,返回 true;如果帧已存在,返回 false。 - */ - public boolean addKeyframe(float frameValue) { - float clampedValue = Math.max(minValue, Math.min(maxValue, frameValue)); - return keyframes.add(clampedValue); - } - - /** - * 移除一个关键帧。 - * @param frameValue 参数值 - * @return 如果成功移除了该帧,返回 true;如果帧不存在,返回 false。 - */ - public boolean removeKeyframe(float frameValue) { - return keyframes.remove(frameValue); - } - - /** - * 检查某个值是否是关键帧。 - * @param frameValue 参数值 - * @return 如果是,返回 true。 - */ - public boolean isKeyframe(float frameValue) { - // 使用 epsilon 进行浮点数比较可能更稳健,但 TreeSet 存储的是精确值 - // 为了简单起见,我们假设我们操作的是精确的 float - return keyframes.contains(frameValue); - } - - /** - * 获取所有关键帧的只读、排序视图。 - * @return 排序后的关键帧集合 - */ - public SortedSet getKeyframes() { - return Collections.unmodifiableSortedSet(keyframes); - } - - /** - * 清除所有关键帧。 - */ - public void clearKeyframes() { - keyframes.clear(); - } - - /** - * 查找在给定阈值(threshold)内最接近指定值的关键帧。 - * - * @param value 要查找的值 - * @param snapThreshold 绝对吸附阈值 (例如 0.05) - * @return 如果找到,返回最近的帧值;否则返回 null。 - */ - public Float getNearestKeyframe(float value, float snapThreshold) { - if (snapThreshold <= 0) return null; - - // 查找 value 附近的关键帧 - SortedSet head = keyframes.headSet(value); - SortedSet tail = keyframes.tailSet(value); - - Float prev = head.isEmpty() ? null : head.last(); - Float next = tail.isEmpty() ? null : tail.first(); - - float distToPrev = prev != null ? Math.abs(value - prev) : Float.MAX_VALUE; - float distToNext = next != null ? Math.abs(value - next) : Float.MAX_VALUE; - - if (distToPrev < snapThreshold && distToPrev <= distToNext) { - return prev; - } - if (distToNext < snapThreshold && distToNext < distToPrev) { - return next; - } - - return null; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - AnimationParameter that = (AnimationParameter) obj; - // 比较所有定义参数的 final 字段和关键帧集合 - return Float.compare(that.defaultValue, defaultValue) == 0 && - Float.compare(that.minValue, minValue) == 0 && - Float.compare(that.maxValue, maxValue) == 0 && - Objects.equals(id, that.id) && - Objects.equals(keyframes, that.keyframes); - } - - @Override - public String toString() { - String idStr = Objects.requireNonNullElse(id, "[null id]"); - String valStr = String.format("%.3f", value); - String minStr = String.format("%.3f", minValue); - String maxStr = String.format("%.3f", maxValue); - String defStr = String.format("%.3f", defaultValue); - - StringBuilder sb = new StringBuilder(); - - sb.append("AnimationParameter[ID=").append(idStr); - sb.append(", Value=").append(valStr); - sb.append(changed ? " (Changed)" : ""); - sb.append(", Range=[").append(minStr).append(", ").append(maxStr).append("]"); - sb.append(", Default=").append(defStr); - if (keyframes.isEmpty()) { - sb.append(", Keyframes=[]"); - } else { - sb.append(", Keyframes=["); - boolean first = true; - for (Float kf : keyframes) { - if (!first) { - sb.append(", "); - } - sb.append(String.format("%.3f", kf)); - first = false; - } - sb.append("]"); - } - - sb.append("]"); - return sb.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java deleted file mode 100644 index 6329e23..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java +++ /dev/null @@ -1,2313 +0,0 @@ -package com.chuangzhou.vivid2D.render.model; - -import com.chuangzhou.vivid2D.events.GlobalEventBus; -import com.chuangzhou.vivid2D.events.render.Mesh2DRender; -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.MultiSelectionBoxRenderer; -import com.chuangzhou.vivid2D.render.TextRenderer; -import com.chuangzhou.vivid2D.render.model.util.*; -import com.chuangzhou.vivid2D.render.model.util.manager.RanderToolsManager; -import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; -import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; -import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement; -import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram; -import org.joml.Matrix3f; -import org.joml.Vector2f; - -import java.nio.FloatBuffer; -import java.nio.IntBuffer; -import java.util.*; - -import org.joml.Vector4f; -import org.lwjgl.opengl.*; -import org.lwjgl.system.MemoryUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * 2D网格类,用于存储和管理2D模型的几何数据 - * 支持顶点、UV坐标、索引和变形操作 - * - * @author tzdwindows 7 - */ -public class Mesh2D { - private static final Logger logger = LoggerFactory.getLogger(Mesh2D.class); - // ==================== 网格数据 ==================== - private String name; - //private float[] vertices; // 顶点数据 [x0, y0, x1, y1, ...] - //private float[] uvs; // UV坐标 [u0, v0, u1, v1, ...] - //private float[] originalVertices; // 原始顶点数据(用于变形恢复) - private VertexList activeVertexList; - private ModelPart modelPart; - - // ==================== 渲染属性 ==================== - private Texture texture; - private boolean visible = true; - private int drawMode = TRIANGLES; // 绘制模式 - private int vaoId = -1; - private int vboId = -1; - private int eboId = -1; - private int indexCount = 0; - private boolean uploaded = false; - - // ==================== 状态管理 ==================== - private boolean dirty = true; // 数据是否已修改 - private BoundingBox bounds; - private boolean boundsDirty = true; - private boolean bakedToWorld = false; - private volatile boolean selected = false; - private Vector2f pivot = new Vector2f(0, 0); - private Vector2f originalPivot = new Vector2f(0, 0); - private boolean isSuspension = false; - private boolean isRenderVertices = false; - private final Map statesList = new LinkedHashMap<>(); - - // [NEW] ==================== 变形引擎字段 ==================== - private final List deformationControlVertices = new ArrayList<>(); - private final List deformationControlCage = new ArrayList<>(); - - // ==================== 多选支持 ==================== - private final List multiSelectedParts = new ArrayList<>(); - private final BoundingBox multiSelectionBounds = new BoundingBox(); - private boolean multiSelectionDirty = true; - - // ==================== 液化状态渲染 ==================== - private boolean showLiquifyOverlay = false; - private Vector4f liquifyOverlayColor = new Vector4f(1.0f, 0.5f, 0.0f, 0.3f); // 半透明橙色 - - // ==================== 常量 ==================== - public static final int POINTS = 0; - public static final int LINES = 1; - public static final int LINE_STRIP = 2; - public static final int TRIANGLES = 3; - public static final int TRIANGLE_STRIP = 4; - public static final int TRIANGLE_FAN = 5; - private static final float ROTATION_HANDLE_DISTANCE = 30.0f; - // ==================== 构造器 ==================== - - public Mesh2D() { - this("unnamed"); - } - - public Mesh2D(String name) { - this.name = name; - this.activeVertexList = new VertexList("KongFuZi"); - this.bounds = new BoundingBox(); - statesList.put("showDeformationVertices",false); - } - - public Mesh2D(String name, float[] vertices, float[] uvs, int[] indices) { - this(name); - setMeshData(vertices, uvs, indices); - } - - - public void setStates(String key, boolean value){ - statesList.put(key,value); - } - - public boolean getStates(String key){ - return statesList.get(key); - } - - // ==================== 变形引擎方法 ==================== - - /** - * 在指定的 (x, y) 坐标处,直接创建一个新的控制点。 - * - * 这个方法采用【三角形分割】的逻辑,完美实现了用户的最终要求: - * 1. 它不再关心“最近的边”,而是直接定位到 (x, y) 所在的【三角形】。 - * 2. 它在 (x, y) 处创建一个新顶点。 - * 3. 它将包含该点的旧三角形移除,并替换为三个连接到新顶点的新三角形。 - * 4. 它立即将新顶点设为控制点,确保其位置绝对精确。 - * - * @param x 用户想要添加控制点的精确 x 坐标 - * @param y 用户想要添加控制点的精确 y 坐标 - * @return 成功创建并添加为控制点的新顶点;如果失败(例如,点在网格之外)则返回 null。 - */ - public Vertex addControlPointAt(float x, float y) { - if (activeVertexList.getIndices() == null || activeVertexList.getIndices().length == 0 || this.modelPart == null) { - logger.warn("无法添加控制点:索引为空或未关联 ModelPart。"); - return null; - } - - Vector2f worldPoint = new Vector2f(x, y); - - // --- 步骤 1: 找到包含该点的【视觉】三角形 (此逻辑已正确) --- - TriangleInfo containingTriangle = findTriangleContainingPoint(worldPoint); - if (containingTriangle == null) { - return null; - } - - // --- [核心修正] 步骤 2: 在正确的、统一的坐标系下计算所有属性 --- - Vector2f barycentricCoords = barycentric(worldPoint, - containingTriangle.v1.position, - containingTriangle.v2.position, - containingTriangle.v3.position); - - float w = barycentricCoords.x; - float v = barycentricCoords.y; - float u = 1.0f - w - v; - - if (u < -1e-6f || v < -1e-6f || w < -1e-6f) { - logger.warn("计算出的重心坐标无效,添加顶点失败。 u={}, v={}, w={}", u, v, w); - return null; - } - - Vector2f newUv = new Vector2f(0, 0); - newUv.add(new Vector2f(containingTriangle.v1.uv).mul(u)); - newUv.add(new Vector2f(containingTriangle.v2.uv).mul(w)); - newUv.add(new Vector2f(containingTriangle.v3.uv).mul(v)); - - Vector2f localPoint = new Vector2f(0, 0); - localPoint.add(new Vector2f(containingTriangle.v1.originalPosition).mul(u)); - localPoint.add(new Vector2f(containingTriangle.v2.originalPosition).mul(w)); - localPoint.add(new Vector2f(containingTriangle.v3.originalPosition).mul(v)); - - // --- 步骤 3: 创建新顶点,并为其赋予正确的“公民身份” --- - Vertex newVertex = new Vertex(worldPoint, newUv, VertexTag.DEFORMATION); - newVertex.originalPosition.set(localPoint); - newVertex.setName(String.valueOf(UUID.randomUUID())); - - // 添加到 active 列表 - activeVertexList.add(newVertex); - final int newVertexIndex = activeVertexList.size() - 1; - - // 重建索引(把所在三角形拆成三个小三角形) - List newIndices = new ArrayList<>(); - for (int i = 0; i < this.activeVertexList.getIndices().length; i += 3) { - int i1 = this.activeVertexList.getIndices()[i], i2 = this.activeVertexList.getIndices()[i + 1], i3 = this.activeVertexList.getIndices()[i + 2]; - if (i1 == containingTriangle.i1 && i2 == containingTriangle.i2 && i3 == containingTriangle.i3) { - newIndices.add(i1); newIndices.add(i2); newIndices.add(newVertexIndex); - newIndices.add(i2); newIndices.add(i3); newIndices.add(newVertexIndex); - newIndices.add(i3); newIndices.add(i1); newIndices.add(newVertexIndex); - } else { - newIndices.add(i1); newIndices.add(i2); newIndices.add(i3); - } - } - this.activeVertexList.setIndices(newIndices.stream().mapToInt(Integer::intValue).toArray()); - - // --- 动态分配三角形区域(改进) --- - // 规则:三角形分配到离质心最近的 DEFORMATION 顶点; - // 但如果该控制顶点是当前三角形的 apex(APEX),且质心位于 AB 底边的“另一侧”,则禁止该 apex 成为 owner。 - // 使用基于 AB 直线的“同侧测试”(叉乘符号)来判断质心是否与 apex 在同一侧,避免受屏幕坐标系方向影响。 - - int[] indicesArr = activeVertexList.getIndices(); - List vertices = activeVertexList.getVertices(); - List deformationVertices = this.deformationControlVertices; // 已包含新的控制点 - - // 初始化每个 deformation 顶点的控制三角形列表 - for (Vertex dv : deformationVertices) { - dv.setControlledTriangles(new ArrayList<>()); - } - - final float EPS = 1e-6f; - int triCount = indicesArr.length / 3; - for (int t = 0; t < triCount; t++) { - int idx0 = indicesArr[t * 3]; - int idx1 = indicesArr[t * 3 + 1]; - int idx2 = indicesArr[t * 3 + 2]; - - Vector2f p0 = vertices.get(idx0).originalPosition; - Vector2f p1 = vertices.get(idx1).originalPosition; - Vector2f p2 = vertices.get(idx2).originalPosition; - - // 质心(使用原始位置) - Vector2f centroid = new Vector2f( - (p0.x + p1.x + p2.x) / 3.0f, - (p0.y + p1.y + p2.y) / 3.0f - ); - - // 找出 apex(y 最大的顶点)及 base 两点(用索引表示) - int apexIndexLocal = idx0; - Vector2f apexP = p0; - int baseIdxA = idx1, baseIdxB = idx2; - Vector2f baseA = p1, baseB = p2; - - if (p1.y > apexP.y) { apexIndexLocal = idx1; apexP = p1; baseIdxA = idx0; baseA = p0; baseIdxB = idx2; baseB = p2; } - if (p2.y > apexP.y) { apexIndexLocal = idx2; apexP = p2; baseIdxA = idx0; baseA = p0; baseIdxB = idx1; baseB = p1; } - - // 计算 AB 向量和叉积符号函数(用于同侧测试) - float abx = baseB.x - baseA.x; - float aby = baseB.y - baseA.y; - - // cross = (B-A) x (P-A) = abx*(py - baseA.y) - aby*(px - baseA.x) - float crossApex = abx * (apexP.y - baseA.y) - aby * (apexP.x - baseA.x); - float crossCentroid = abx * (centroid.y - baseA.y) - aby * (centroid.x - baseA.x); - - boolean apexForbidden; - if (Math.abs(crossApex) < EPS) { - // apex 在 AB 线上,保守处理为不禁止(因为没有明确“对侧”) - apexForbidden = false; - } else { - // 若叉积符号不同则表示质心和 apex 在 AB 的两侧 -> 禁止该 apex 控制此三角形 - apexForbidden = (crossApex * crossCentroid) < 0.0f; - } - - // 在 deformationVertices 中选择最近的顶点(平方距离比较),若最近是 apex 且被禁止则取下一个最近 - Vertex chosen = null; - float bestDist = Float.MAX_VALUE; - Vertex second = null; - float secondDist = Float.MAX_VALUE; - - for (Vertex dv : deformationVertices) { - Vector2f dvPos = dv.originalPosition; - float dx = dvPos.x - centroid.x; - float dy = dvPos.y - centroid.y; - float dist2 = dx * dx + dy * dy; - - if (dist2 < bestDist) { - second = chosen; secondDist = bestDist; - chosen = dv; bestDist = dist2; - } else if (dist2 < secondDist) { - second = dv; secondDist = dist2; - } - } - - Vertex owner = chosen; - // 如果最近的是三角形的 apex 且禁止,则尝试用 second;如果 second 为 null 或仍不合适则保留 chosen(降级处理) - if (owner != null && owner.getIndex() == apexIndexLocal && apexForbidden) { - if (second != null && second.getIndex() != apexIndexLocal) { - owner = second; - } - } - - // 最终将该三角形 t 分配给 owner - if (owner != null) { - List list = owner.getControlledTriangles(); - if (list == null) { - list = new ArrayList<>(); - owner.setControlledTriangles(list); - } - list.add(t); // 使用三角形全局索引 t - } - } - List allPoints = new ArrayList<>(this.deformationControlVertices); - allPoints.add(newVertex); - setDeformationControlVertices(allPoints); - // 返回新创建的控制顶点 - return newVertex; - } - - // [辅助内部类] 用于方便地传递找到的三角形信息 - private static class TriangleInfo { - int i1, i2, i3; - Vertex v1, v2, v3; - TriangleInfo(int i1, int i2, int i3, Vertex v1, Vertex v2, Vertex v3) { - this.i1 = i1; this.i2 = i2; this.i3 = i3; - this.v1 = v1; this.v2 = v2; this.v3 = v3; - } - } - - // [辅助方法] 查找包含给定点的三角形 - private TriangleInfo findTriangleContainingPoint(Vector2f point) { - for (int i = 0; i < this.activeVertexList.getIndices().length; i += 3) { - int i1 = this.activeVertexList.getIndices()[i]; - int i2 = this.activeVertexList.getIndices()[i + 1]; - int i3 = this.activeVertexList.getIndices()[i + 2]; - - if (i1 >= activeVertexList.size() || i2 >= activeVertexList.size() || i3 >= activeVertexList.size()) { - continue; - } - - Vertex v1 = activeVertexList.get(i1); - Vertex v2 = activeVertexList.get(i2); - Vertex v3 = activeVertexList.get(i3); - - // [核心修正] - // 直接使用 vertex.position,它已经是我们需要的世界坐标了。 - if (isPointInTriangle(point, v1.position, v2.position, v3.position)) { - return new TriangleInfo(i1, i2, i3, v1, v2, v3); - } - } - return null; - } - - // [辅助方法] 判断点是否在三角形内(基于Barycentric坐标) - private boolean isPointInTriangle(Vector2f p, Vector2f a, Vector2f b, Vector2f c) { - Vector2f coords = barycentric(p, a, b, c); - // 如果所有权重都在0和1之间,则点在三角形内 - return coords.x >= 0 && coords.y >= 0 && (coords.x + coords.y) <= 1; - } - - // [辅助方法] 计算Barycentric坐标 - private Vector2f barycentric(Vector2f p, Vector2f a, Vector2f b, Vector2f c) { - Vector2f v0 = new Vector2f(b).sub(a); - Vector2f v1 = new Vector2f(c).sub(a); - Vector2f v2 = new Vector2f(p).sub(a); - float d00 = v0.dot(v0); - float d01 = v0.dot(v1); - float d11 = v1.dot(v1); - float d20 = v2.dot(v0); - float d21 = v2.dot(v1); - float denom = d00 * d11 - d01 * d01; - if (Math.abs(denom) < 1e-9) return new Vector2f(-1, -1); // 退化三角形 - float w = (d11 * d20 - d01 * d21) / denom; - float v = (d00 * d21 - d01 * d20) / denom; - return new Vector2f(w, v); - } - - /** - * [重构] 设置内部控制点,并激活“内部点模式”。 - * 调用此方法会自动禁用“笼模式”。 - */ - public void setDeformationControlVertices(List controlVertices) { - this.deformationControlVertices.clear(); - this.deformationControlCage.clear(); - - if (controlVertices != null && !controlVertices.isEmpty()) { - this.deformationControlVertices.addAll(controlVertices); - } - } - - /** - * 尝试将当前顶点列表中的顶点进行“ localized push”,即基于当前顶点列表的状态,将当前顶点列表中的顶点进行“ localized push”。 - * @param baseVertexState 拖动开始时的顶点列表 - * @param pushCenterWorld 推送中心点 - * @param delta 推送向量 - * @param radius 推送半径 - */ - public void applyLocalizedPush(List baseVertexState, Vector2f pushCenterWorld, Vector2f delta, float radius) { - if (activeVertexList == null || activeVertexList.isEmpty() || baseVertexState.size() != activeVertexList.size()) { - return; - } - - float radiusSq = radius * radius; - - for (int i = 0; i < activeVertexList.size(); i++) { - Vertex currentVertex = activeVertexList.get(i); - Vertex baseStateVertex = baseVertexState.get(i); // 获取这个顶点在拖动开始时的状态 - - // 为了保证影响范围计算的一致性,我们仍然基于顶点的原始位置来计算距离 - float distSq = currentVertex.originalPosition.distanceSquared(pushCenterWorld); - - if (distSq < radiusSq) { - // 影响因子计算不变 - float normalizedDist = distSq / radiusSq; - float influence = (float) Math.exp(-normalizedDist * 4.0); - - // [核心逻辑变更] - // 新的位置 = 拖动开始时的位置 + 本次拖动带来的位移 - currentVertex.position.set(baseStateVertex.position).add(delta.x * influence, delta.y * influence); - - } else { - // 如果顶点在影响范围之外,它的位置应该恢复到本次拖动开始前的状态 - currentVertex.position.set(baseStateVertex.position); - } - } - - // 标记网格为脏,以便GPU缓冲更新 - markDirty(); - } - - // ==================== 网格数据设置 ==================== - - /** - * 设置网格数据 - */ - public void setMeshData(float[] vertices, float[] uvs, int[] indices) { - if (vertices.length % 2 != 0) { - throw new IllegalArgumentException("Vertices array must have even length (x,y pairs)"); - } - if (uvs.length % 2 != 0) { - throw new IllegalArgumentException("UVs array must have even length (u,v pairs)"); - } - if (vertices.length / 2 != uvs.length / 2) { - throw new IllegalArgumentException("Vertices and UVs must have same number of points"); - } - if (this.activeVertexList == null) { - this.activeVertexList = new VertexList("KongFuZi"); - } - this.activeVertexList.clear(); - int vertexCount = vertices.length / 2; - for (int i = 0; i < vertexCount; i++) { - float x = vertices[i * 2]; - float y = vertices[i * 2 + 1]; - float u = uvs[i * 2]; - float v = uvs[i * 2 + 1]; - this.activeVertexList.add(new Vertex(x, y, u, v)); - } - activeVertexList.setIndices(indices.clone()); - this.originalPivot.set(this.pivot); - markDirty(); - } - - /** - * 直接设置此网格的活动顶点列表。 - * 这是从序列化数据恢复网格状态的首选方法,因为它可以保留每个顶点的完整信息。 - * - * @param vertexList 包含完整顶点信息的新顶点列表。 - */ - public void setActiveVertexList(VertexList vertexList) { - if (vertexList != null) { - this.activeVertexList = vertexList; - markDirty(); // 标记网格需要更新 - } - } - - /** - * 设置是否为渲染顶点 - */ - public void setRenderVertices(boolean renderVertices) { - isRenderVertices = renderVertices; - } - - public void setModelPart(ModelPart modelPart) { - this.modelPart = modelPart; - } - - /** - * 设置是否显示液化覆盖层 - */ - public void setShowLiquifyOverlay(boolean show) { - this.showLiquifyOverlay = show; - markDirty(); - } - - /** - * 绘制液化状态指示器(在不显示顶点时) - */ - private void drawLiquifyStatusIndicator(BufferBuilder bb) { - BoundingBox bounds = getBounds(); - if (bounds == null || !bounds.isValid()) return; - - float centerX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f; - float centerY = (bounds.getMinY() + bounds.getMaxY()) / 2.0f; - float width = bounds.getWidth(); - float height = bounds.getHeight(); - - // 计算指示器位置(放在网格右上方,不遮挡内容) - float indicatorX = bounds.getMaxX() + Math.max(width, height) * 0.2f; - float indicatorY = bounds.getMaxY() + Math.max(width, height) * 0.1f; - - // 1. 绘制简洁的液化状态圆点 - float dotRadius = Math.max(width, height) * 0.08f; - - // 外圈圆点 - bb.begin(GL11.GL_TRIANGLE_FAN, 16); - bb.setColor(new Vector4f(1.0f, 0.6f, 0.0f, 0.8f)); // 橙色圆点 - bb.vertex(indicatorX, indicatorY, 0f, 0f); // 中心点 - for (int i = 0; i <= 16; i++) { - float angle = (float) (i * 2 * Math.PI / 16); - float x = indicatorX + (float) Math.cos(angle) * dotRadius; - float y = indicatorY + (float) Math.sin(angle) * dotRadius; - bb.vertex(x, y, 0f, 0f); - } - bb.endImmediate(); - - // 内圈白色圆点 - float innerRadius = dotRadius * 0.5f; - bb.begin(GL11.GL_TRIANGLE_FAN, 12); - bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 0.9f)); // 白色内圆 - bb.vertex(indicatorX, indicatorY, 0f, 0f); // 中心点 - for (int i = 0; i <= 12; i++) { - float angle = (float) (i * 2 * Math.PI / 12); - float x = indicatorX + (float) Math.cos(angle) * innerRadius; - float y = indicatorY + (float) Math.sin(angle) * innerRadius; - bb.vertex(x, y, 0f, 0f); - } - bb.endImmediate(); - - // 2. 绘制简洁的画笔图标 - drawSimpleBrushIcon(bb, indicatorX, indicatorY, dotRadius); - - // 3. 绘制简洁的提示文字 - String liquifyText = "Liquify"; - String hintText = "Ctrl: Show Vertices"; - - TextRenderer textRenderer = ModelRender.getTextRenderer(); - if (textRenderer != null) { - float textY = indicatorY + dotRadius + 15f; - - // 主标题 - float titleWidth = textRenderer.getTextWidth(liquifyText); - float titleX = indicatorX - titleWidth / 2.0f; - - // 绘制主标题背景(简洁的圆角效果) - bb.begin(GL11.GL_TRIANGLES, 6); - bb.setColor(new Vector4f(0.0f, 0.0f, 0.0f, 0.6f)); // 半透明黑色背景 - bb.vertex(titleX - 6, textY - 12, 0f, 0f); - bb.vertex(titleX + titleWidth + 6, textY - 12, 0f, 0f); - bb.vertex(titleX + titleWidth + 6, textY + 2, 0f, 0f); - bb.vertex(titleX + titleWidth + 6, textY + 2, 0f, 0f); - bb.vertex(titleX - 6, textY + 2, 0f, 0f); - bb.vertex(titleX - 6, textY - 12, 0f, 0f); - bb.endImmediate(); - - // 绘制主标题 - ModelRender.renderText(liquifyText, titleX, textY, new Vector4f(1.0f, 0.8f, 0.0f, 1.0f)); - - // 提示文字(小字号) - float hintY = textY + 15f; - float hintWidth = textRenderer.getTextWidth(hintText); - float hintX = indicatorX - hintWidth / 2.0f; - - ModelRender.renderText(hintText, hintX, hintY, new Vector4f(0.8f, 0.8f, 0.8f, 0.7f)); - } - } - - /** - * 绘制简洁的画笔图标 - */ - private void drawSimpleBrushIcon(BufferBuilder bb, float centerX, float centerY, float size) { - float iconSize = size * 0.6f; - - // 画笔柄(简单的线条) - bb.begin(GL11.GL_LINES, 2); - bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 0.9f)); - bb.vertex(centerX - iconSize * 0.3f, centerY - iconSize * 0.5f, 0f, 0f); - bb.vertex(centerX, centerY + iconSize * 0.3f, 0f, 0f); - bb.endImmediate(); - - // 画笔头(小三角形) - bb.begin(GL11.GL_TRIANGLES, 3); - bb.setColor(new Vector4f(1.0f, 0.3f, 0.0f, 0.9f)); - bb.vertex(centerX, centerY + iconSize * 0.3f, 0f, 0f); - bb.vertex(centerX - iconSize * 0.2f, centerY + iconSize * 0.6f, 0f, 0f); - bb.vertex(centerX + iconSize * 0.2f, centerY + iconSize * 0.6f, 0f, 0f); - bb.endImmediate(); - } - - /** - * 设置中心点 - */ - public boolean setPivot(float x, float y) { - BoundingBox bounds = getBounds(); - if (x >= bounds.getMinX() && x <= bounds.getMaxX() && - y >= bounds.getMinY() && y <= bounds.getMaxY()) { - this.pivot.set(x, y); - return true; - } - return false; - } - - public Vector2f getOriginalPivot() { - return new Vector2f(originalPivot); - } - - public boolean setOriginalPivot(Vector2f p) { - if (p != null) { - BoundingBox bounds = getBounds(); - if (bounds != null && - p.x >= bounds.getMinX() && p.x <= bounds.getMaxX() && - p.y >= bounds.getMinY() && p.y <= bounds.getMaxY()) { - this.originalPivot.set(p); - markDirty(); - return true; - } - } - return false; - } - - - public boolean setPivot(Vector2f pivot) { - BoundingBox bounds = getBounds(); - if (pivot.x >= bounds.getMinX() && pivot.x <= bounds.getMaxX() && - pivot.y >= bounds.getMinY() && pivot.y <= bounds.getMaxY()) { - this.pivot.set(pivot); - return true; - } - return false; - } - - /** - * 获取中心点 - */ - public Vector2f getPivot() { - return new Vector2f(pivot); - } - - /** - * 移动中心点 - */ - public void movePivot(float dx, float dy) { - float newX = pivot.x + dx; - float newY = pivot.y + dy; - - BoundingBox b = getBounds(); - - if (b != null && newX >= b.getMinX() && newX <= b.getMaxX() - && newY >= b.getMinY() && newY <= b.getMaxY()) { - this.pivot.add(dx, dy); - // 同步原始局部 pivot —— 这里假设 originalPivot 与 pivot 的坐标系一致(多数场景下是这样) - this.originalPivot.add(dx, dy); - markDirty(); - } - } - - - /** - * 创建一个四边形网格。 - * 这个版本不仅包含四个角点,还在上、下、左、右四条边的中点,以及整个图形的中心,都添加了顶点。 - * 最终形成的网格由9个顶点和8个三角形构成,这种“钻石”拓扑结构非常适合平滑的变形。 - * - * @param name 网格的名称 - * @param width 宽度 - * @param height 高度 - * @return 一个新的、经过优化的 Mesh2D 对象 - */ - public static Mesh2D createQuad(String name, float width, float height) { - float hw = width / 2.0f; // half-width - float hh = height / 2.0f; // half-height - - // 定义9个顶点: 4个角点, 4个边中点, 1个中心点 - float[] vertices = { - // 角点 - -hw, -hh, // 0: 左下 - hw, -hh, // 1: 右下 - hw, hh, // 2: 右上 - -hw, hh, // 3: 左上 - // 边中点 - 0, -hh, // 4: 下中 - hw, 0, // 5: 右中 - 0, hh, // 6: 上中 - -hw, 0, // 7: 左中 - // 中心点 - 0, 0 // 8: 中心 - }; - - // 对应的9个UV坐标 - float[] uvs = { - // 角点 - 0.0f, 1.0f, // 0: 左下 - 1.0f, 1.0f, // 1: 右下 - 1.0f, 0.0f, // 2: 右上 - 0.0f, 0.0f, // 3: 左上 - // 边中点 - 0.5f, 1.0f, // 4: 下中 - 1.0f, 0.5f, // 5: 右中 - 0.5f, 0.0f, // 6: 上中 - 0.0f, 0.5f, // 7: 左中 - // 中心点 - 0.5f, 0.5f // 8: 中心 - }; - - // 使用中心点(索引8)作为公共点,构建8个三角形 (Triangle Fan) - int[] indices = { - 8, 0, 4, - 8, 4, 1, - 8, 1, 5, - 8, 5, 2, - 8, 2, 6, - 8, 6, 3, - 8, 3, 7, - 8, 7, 0 - }; - - return new Mesh2D(name, vertices, uvs, indices); - } - - /** - * 获取原始顶点数据的 float 数组 - * (来自当前激活的列表) - */ - public float[] getOriginalVertices() { - if (activeVertexList == null) return new float[0]; - float[] origVerts = new float[getVertexCount() * 2]; - for (int i = 0; i < getVertexCount(); i++) { - Vertex v = activeVertexList.get(i); // <-- MODIFIED - origVerts[i * 2] = v.originalPosition.x; - origVerts[i * 2 + 1] = v.originalPosition.y; - } - return origVerts; - } - - /** - * 设置原始顶点数据 - * (到当前激活的列表) - */ - public void setOriginalVertices(float[] originalVertices) { - // --- MODIFIED --- - if (activeVertexList == null) return; - if (originalVertices != null && originalVertices.length == getVertexCount() * 2) { - for (int i = 0; i < getVertexCount(); i++) { - Vertex v = activeVertexList.get(i); // <-- MODIFIED - v.originalPosition.set(originalVertices[i * 2], originalVertices[i * 2 + 1]); - } - } - // --- END MODIFIED --- - } - - /** - * 设置顶点数据(支持顶点数量变化) - * (到当前激活的列表) - */ - public void setVertices(float[] vertices) { - if (vertices == null) { - throw new IllegalArgumentException("Vertices array cannot be null"); - } - if (vertices.length % 2 != 0) { - throw new IllegalArgumentException("Vertices array must have even length (x,y pairs)"); - } - if (activeVertexList == null) return; // <-- MODIFIED - - // --- MODIFIED --- - int newVertexCount = vertices.length / 2; - int oldVertexCount = getVertexCount(); - - // 如果顶点数量变化 - if (newVertexCount != oldVertexCount) { - List newVertexCollection = new ArrayList<>(newVertexCount); - for (int i = 0; i < newVertexCount; i++) { - float x = vertices[i * 2]; - float y = vertices[i * 2 + 1]; - - if (i < oldVertexCount) { - // 保留旧UV - newVertexCollection.add(new Vertex(x, y, activeVertexList.get(i).uv.x, activeVertexList.get(i).uv.y)); - } else { - // 估算新UV (简单取 0.5, 0.5) - newVertexCollection.add(new Vertex(x, y, 0.5f, 0.5f)); - } - } - // 完全替换活动列表中的顶点 - activeVertexList.clear(); - for(Vertex v : newVertexCollection) { - activeVertexList.add(v); - } - - // 索引也需要重新生成 - regenerateIndicesForNewVertexCount(newVertexCount); - } else { - // 顶点数量不变,仅更新位置 - for (int i = 0; i < newVertexCount; i++) { - activeVertexList.get(i).position.set(vertices[i * 2], vertices[i * 2 + 1]); - } - } - // --- END MODIFIED --- - - markDirty(); - } - - /** - * 为新的顶点数量重新生成索引 - */ - private void regenerateIndicesForNewVertexCount(int newVertexCount) { - // 简单的三角形扇形索引生成 - if (newVertexCount >= 3) { - List newIndices = new ArrayList<>(); - - // 使用三角形扇形(适用于凸多边形) - for (int i = 1; i < newVertexCount - 1; i++) { - newIndices.add(0); - newIndices.add(i); - newIndices.add(i + 1); - } - - activeVertexList.setIndices(new int[newIndices.size()]); - for (int i = 0; i < newIndices.size(); i++) { - this.activeVertexList.getIndices()[i] = newIndices.get(i); - } - } else { - this.activeVertexList.setIndices(new int[0]); - } - } - - /** - * 设置顶点数据(带原始顶点同步) - * (到当前激活的列表) - */ - public void setVertices(float[] vertices, boolean updateOriginal) { - setVertices(vertices); - if (updateOriginal) { - this.saveAsOriginal(); - } - } - - /** - * 创建圆形网格 - */ - public static Mesh2D createCircle(String name, float radius, int segments) { - if (segments < 3) { - segments = 3; - } - - int vertexCount = segments + 1; // 中心点 + 边缘点 - float[] vertices = new float[vertexCount * 2]; - float[] uvs = new float[vertexCount * 2]; - int[] indices = new int[segments * 3]; - - // 中心点 (索引0) - vertices[0] = 0.0f; - vertices[1] = 0.0f; - uvs[0] = 0.5f; - uvs[1] = 0.5f; - - // 边缘点 - float angleStep = (float) (2.0f * Math.PI / segments); - for (int i = 0; i < segments; i++) { - float angle = i * angleStep; - int vertexIndex = (i + 1) * 2; - - vertices[vertexIndex] = (float) Math.cos(angle) * radius; - vertices[vertexIndex + 1] = (float) Math.sin(angle) * radius; - - uvs[vertexIndex] = (float) (Math.cos(angle) * 0.5f + 0.5f); - uvs[vertexIndex + 1] = (float) (Math.sin(angle) * 0.5f + 0.5f); - - // 三角形索引 - int triangleIndex = i * 3; - indices[triangleIndex] = 0; // 中心点 - indices[triangleIndex + 1] = i + 1; - indices[triangleIndex + 2] = (i + 1) % segments + 1; - } - - return new Mesh2D(name, vertices, uvs, indices); - } - - // ==================== 顶点操作 ==================== - - /** - * 添加新顶点到网格末尾 - */ - public void addVertex(float x, float y, float u, float v) { - if (activeVertexList != null) { - activeVertexList.add(new Vertex(x, y, u, v)); - markDirty(); - } - } - - public void addVertex(Vertex vertex){ - if (activeVertexList != null) { - activeVertexList.add(vertex); - markDirty(); - } - } - - /** - * 在指定位置插入顶点 - */ - public void insertVertex(int index, float x, float y, float u, float v) { - if (activeVertexList == null) return; // <-- MODIFIED - if (index < 0 || index > getVertexCount()) { - throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); - } - - // --- MODIFIED --- - activeVertexList.vertices.add(index, new Vertex(x, y, u, v)); - // --- END MODIFIED --- - - updateIndicesAfterVertexInsertion(index); - markDirty(); - } - - /** - * 移除指定顶点 - */ - public void removeVertex(int index) { - if (activeVertexList == null) return; // <-- MODIFIED - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); - } - - // --- MODIFIED --- - activeVertexList.remove(index); - // --- END MODIFIED --- - - updateIndicesAfterVertexRemoval(index); - markDirty(); - } - - /** - * 在插入顶点后更新索引数组 - */ - private void updateIndicesAfterVertexInsertion(int insertedIndex) { - if (activeVertexList.getIndices() == null) return; - - for (int i = 0; i < activeVertexList.getIndices().length; i++) { - if (activeVertexList.getIndices()[i] >= insertedIndex) { - activeVertexList.getIndices()[i]++; - } - } - } - - /** - * 在移除顶点后更新索引数组 - */ - private void updateIndicesAfterVertexRemoval(int removedIndex) { - if (activeVertexList.getIndices() == null) return; - - // 创建新索引数组,移除引用被删除顶点的三角形 - List newIndicesList = new ArrayList<>(); - for (int i = 0; i < activeVertexList.getIndices().length; i += 3) { - int i1 = activeVertexList.getIndices()[i]; - int i2 = activeVertexList.getIndices()[i + 1]; - int i3 = activeVertexList.getIndices()[i + 2]; - - // 如果三角形包含被删除的顶点,跳过这个三角形 - if (i1 == removedIndex || i2 == removedIndex || i3 == removedIndex) { - continue; - } - - // 调整索引编号 - if (i1 > removedIndex) i1--; - if (i2 > removedIndex) i2--; - if (i3 > removedIndex) i3--; - - newIndicesList.add(i1); - newIndicesList.add(i2); - newIndicesList.add(i3); - } - - // 转换回数组 - activeVertexList.setIndices(new int[newIndicesList.size()]); - for (int i = 0; i < newIndicesList.size(); i++) { - activeVertexList.getIndices()[i] = newIndicesList.get(i); - } - } - - /** - * 获取顶点数量 - */ - public int getVertexCount() { - return (activeVertexList != null) ? activeVertexList.size() : 0; - } - - /** - * 获取顶点位置 - */ - public Vector2f getVertex(int index, Vector2f dest) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); - } - return dest.set(activeVertexList.get(index).position); - } - - public Vector2f getVertex(int index) { - return getVertex(index, new Vector2f()); - } - - /** - * 设置顶点位置 - */ - public void setVertex(int index, float x, float y) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); - } - activeVertexList.get(index).position.set(x, y); - markDirty(); - } - - public void setVertex(int index, Vector2f position) { - setVertex(index, position.x, position.y); - } - - public void setVertex(int index, Vertex vertex){ - activeVertexList.set(index, vertex); - } - - public Vertex getVertexInstance(int index){ - return activeVertexList.get(index); - } - - /** - * 设置该 Mesh 的选中状态(线程安全) - */ - public void setSelected(boolean sel) { - this.selected = sel; - } - - /** - * 查询选中状态 - */ - public boolean isSelected() { - return this.selected; - } - - /** - * 获取UV坐标 - */ - public Vector2f getUV(int index, Vector2f dest) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("UV index out of bounds: " + index); - } - return dest.set(activeVertexList.get(index).uv); - } - - /** - * 设置UV坐标 - */ - public void setUV(int index, float u, float v) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("UV index out of bounds: " + index); - } - activeVertexList.get(index).uv.set(u, v); - markDirty(); - } - - // ==================== 变形支持 ==================== - - /** - * 重置为原始顶点数据 - */ - public void resetToOriginal() { - if (activeVertexList != null) { - for (Vertex v : activeVertexList) { - v.resetToOriginal(); - } - markDirty(); - } - } - - /** - * 保存当前顶点为原始数据 - */ - public void saveAsOriginal() { - if (activeVertexList == null || modelPart == null) return; - Matrix3f inverseWorldTransform = modelPart.getWorldTransform().invert(new Matrix3f()); - for (Vertex v : activeVertexList) { - Vector2f currentWorldPos = v.position; - Vector2f newLocalPos = Matrix3fUtils.transformPoint(inverseWorldTransform, currentWorldPos); - v.originalPosition.set(newLocalPos); - } - for (Vertex controlV : deformationControlVertices) { - Vector2f currentWorldPos = controlV.position; - Vector2f newLocalPos = Matrix3fUtils.transformPoint(inverseWorldTransform, currentWorldPos); - controlV.originalPosition.set(newLocalPos); - } - } - - - /** - * 应用变形到所有顶点 - */ - public void transformVertices(VertexTransformer transformer) { - if (activeVertexList != null) { - for (int i = 0; i < getVertexCount(); i++) { - Vertex v = activeVertexList.get(i); - transformer.transform(v.position, i); - } - markDirty(); - } - } - - public ModelPart getModelPart() { - return modelPart; - } - - public VertexList getActiveVertexList() { - return activeVertexList; - } - - public List getDeformationControlCage() { - return deformationControlCage; - } - - public List getDeformationControlVertices() { - return deformationControlVertices; - } - - /** - * 顶点变换器接口 - */ - public interface VertexTransformer { - void transform(Vector2f vertex, int index); - } - - // ==================== 边界计算 ==================== - - /** - * 更新边界框 - */ - public void updateBounds() { - bounds.reset(); - if (activeVertexList != null) { - for (Vertex v : activeVertexList) { - bounds.expand(v.position.x, v.position.y); - } - } - boundsDirty = false; - } - - /** - * 获取边界框 - */ - public BoundingBox getBounds() { - if (boundsDirty) { - updateBounds(); - } - return bounds; - } - - /** - * 检查点是否在网格内(使用边界框近似) - */ - public boolean containsPoint(float x, float y) { - return containsPoint(x, y, false); - } - - /** - * 检查点是否在网格内(可选择精确检测) - */ - public boolean containsPoint(float x, float y, boolean precise) { - //if (isInMultiSelection()) { - // BoundingBox multiBounds = getMultiSelectionBounds(); - // boolean inBounds = x >= multiBounds.getMinX() && x <= multiBounds.getMaxX() && - // y >= multiBounds.getMinY() && y <= multiBounds.getMaxY(); -// - // if (precise && inBounds) { - // // 在多选边界框内时,进一步检查是否在任意选中的网格几何形状内 - // return isPointInAnySelectedMesh(x, y); - // } - // return inBounds; - //} - - BoundingBox b = getBounds(); - boolean inBounds = x >= b.getMinX() && x <= b.getMaxX() && y >= b.getMinY() && y <= b.getMaxY(); - - if (precise && inBounds) { - // 精确检测点是否在网格几何形状内 - return isPointInMeshGeometry(x, y); - } - return inBounds; - } - - /** - * 检查点是否在任意选中的网格几何形状内 - */ - private boolean isPointInAnySelectedMesh(float x, float y) { - // 检查自己 - if (isPointInMeshGeometry(x, y)) { - return true; - } - - // 检查多选列表中的其他网格 - for (Mesh2D mesh : multiSelectedParts) { - if (mesh.isPointInMeshGeometry(x, y)) { - return true; - } - } - return false; - } - - /** - * 精确检测点是否在网格几何形状内(使用射线法) - * (基于当前激活的列表) - */ - private boolean isPointInMeshGeometry(float x, float y) { - // 简单的边界框检测先过滤掉明显不在的 - BoundingBox b = getBounds(); - if (x < b.getMinX() || x > b.getMaxX() || y < b.getMinY() || y > b.getMaxY()) { - return false; - } - - // 使用射线法进行精确检测 - return isPointInPolygon(x, y); // <-- MODIFIED - } - - /** - * 使用射线法判断点是否在多边形内 (新版本) - * (基于当前激活的列表) - */ - private boolean isPointInPolygon(float x, float y) { - // --- MODIFIED --- - if (getVertexCount() < 3) { // 至少需要3个点组成三角形 - return false; - } - - boolean inside = false; - int n = getVertexCount(); - - for (int i = 0, j = n - 1; i < n; j = i++) { - Vertex vi = activeVertexList.get(i); // <-- MODIFIED - Vertex vj = activeVertexList.get(j); // <-- MODIFIED - - float xi = vi.position.x; - float yi = vi.position.y; - float xj = vj.position.x; - float yj = vj.position.y; - if (isPointOnLineSegment(x, y, xi, yi, xj, yj)) { - return true; - } - if (((yi > y) != (yj > y)) && - (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) { - inside = !inside; - } - } - return inside; - } - - /** - * 检查点是否在线段上 - */ - private boolean isPointOnLineSegment(float x, float y, float x1, float y1, float x2, float y2) { - float cross = (x - x1) * (y2 - y1) - (y - y1) * (x2 - x1); - if (Math.abs(cross) > 1e-6) { - return false; // 不在直线上 - } - - float dot = (x - x1) * (x2 - x1) + (y - y1) * (y2 - y1); - if (dot < 0) { - return false; // 在线段起点之前 - } - - float squaredLength = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1); - if (dot > squaredLength) { - return false; // 在线段终点之后 - } - - return true; - } - - public boolean containsPoint(Vector2f point) { - return containsPoint(point.x, point.y); - } - - // ==================== 缓冲区支持 ==================== - - /** - * 获取顶点缓冲区数据 - * (基于当前激活的列表) - */ - public FloatBuffer getVertexBuffer(FloatBuffer buffer) { - int floatCount = getVertexCount() * 2; - if (buffer == null || buffer.remaining() < floatCount) { - throw new IllegalArgumentException("Buffer is null or too small"); - } - buffer.clear(); - if (activeVertexList != null) { - for (Vertex v : activeVertexList) { - buffer.put(v.position.x); - buffer.put(v.position.y); - } - } - buffer.flip(); - return buffer; - } - - public float getX(int index) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); - } - return activeVertexList.get(index).position.x; - } - - public float getY(int index) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); - } - return activeVertexList.get(index).position.y; - } - - /** - * 获取索引缓冲区数据 - */ - public IntBuffer getIndexBuffer(IntBuffer buffer) { - int idxCount = (activeVertexList != null && activeVertexList.getIndices() != null) ? activeVertexList.getIndices().length : 0; - if (buffer == null || buffer.capacity() < idxCount) { - throw new IllegalArgumentException("Buffer is null or too小/capacity不足"); - } - buffer.clear(); - if (idxCount > 0) buffer.put(activeVertexList.getIndices()); - buffer.flip(); - return buffer; - } - - /** - * 获取交错的顶点+UV数据(用于VBO) - * (基于当前激活的列表) - */ - public FloatBuffer getInterleavedBuffer(FloatBuffer buffer) { - int vertexCount = getVertexCount(); - int floatCount = vertexCount * 4; // 每个顶点:x, y, u, v - if (buffer == null || buffer.remaining() < floatCount) { - throw new IllegalArgumentException("Buffer is null or too small"); - } - buffer.clear(); - if (activeVertexList != null) { - for (int i = 0; i < vertexCount; i++) { - Vertex v = activeVertexList.get(i); - buffer.put(v.position.x); buffer.put(v.position.y); - buffer.put(v.uv.x); buffer.put(v.uv.y); - } - } - buffer.flip(); - return buffer; - } - - public boolean moveControlVertex(Vertex control, Vector2f targetPos) { - if (control == null) return false; - if (activeVertexList == null) return false; - - int[] indicesArr = activeVertexList.getIndices(); - List vertices = activeVertexList.getVertices(); - List controlled = control.getControlledTriangles(); - if (controlled == null || controlled.isEmpty()) { - // 没有控制的三角形,直接移动 - control.position.set(targetPos); - control.originalPosition.set(targetPos); - return true; - } - - final float EPS = 1e-6f; - // 先模拟检查所有被该控制点控制的三角形,任何一个三角形违反规则都拒绝移动 - for (Integer t : controlled) { - if (t == null || t < 0) continue; - int triIdx = t * 3; - if (triIdx + 2 >= indicesArr.length) continue; - int idx0 = indicesArr[triIdx]; - int idx1 = indicesArr[triIdx + 1]; - int idx2 = indicesArr[triIdx + 2]; - - // 找到这一三角形的三个点(使用 originalPosition 进行判断) - Vertex va = vertices.get(idx0); - Vertex vb = vertices.get(idx1); - Vertex vc = vertices.get(idx2); - - // 确定当前三角形哪个是 apex(y 最大) - int apexIndexLocal = idx0; - Vector2f apexP = va.originalPosition; - Vector2f baseA = vb.originalPosition, baseB = vc.originalPosition; - int baseIdxA = idx1, baseIdxB = idx2; - - if (vb.originalPosition.y > apexP.y) { - apexIndexLocal = idx1; apexP = vb.originalPosition; - baseA = va.originalPosition; baseIdxA = idx0; - baseB = vc.originalPosition; baseIdxB = idx2; - } - if (vc.originalPosition.y > apexP.y) { - apexIndexLocal = idx2; apexP = vc.originalPosition; - baseA = va.originalPosition; baseIdxA = idx0; - baseB = vb.originalPosition; baseIdxB = idx1; - } - - // 只有当 control 是这个三角形的 apex 时才应用 "不允许移动到 AB 以下" 的约束 - if (control.getIndex() != apexIndexLocal) { - continue; - } - - // 模拟将 apex 移动到 targetPos 后的质心 - Vector2f centroidAfter = new Vector2f( - (targetPos.x + baseA.x + baseB.x) / 3.0f, - (targetPos.y + baseA.y + baseB.y) / 3.0f - ); - - float baseMinY = Math.min(baseA.y, baseB.y); - // 如果质心低于底边的最小 y,则违反规则(即移动使得质心进入 AB 以下区域) - if (centroidAfter.y < baseMinY - EPS) { - // 被约束,拒绝整个移动 - return false; - } - - // 进一步增强:使用 AB 的同侧检测(可选冗余检查) - float abx = baseB.x - baseA.x; - float aby = baseB.y - baseA.y; - float crossApex = abx * (apexP.y - baseA.y) - aby * (apexP.x - baseA.x); - float crossCentroidAfter = abx * (centroidAfter.y - baseA.y) - aby * (centroidAfter.x - baseA.x); - if (Math.abs(crossApex) >= EPS && (crossApex * crossCentroidAfter) < 0.0f) { - // centroidAfter 与 apex 在 AB 两侧,视为违规 - return false; - } - } - - // 所有受控三角形都通过约束检查,执行移动并更新 originalPosition(或只更新 position,取决于你的设计) - control.position.set(targetPos); - control.originalPosition.set(targetPos); - return true; - } - - // ==================== 状态管理 ==================== - /** - * 标记数据已修改 - */ - public void markDirty() { - this.dirty = true; - this.boundsDirty = true; - this.multiSelectionDirty = true; // 新增:标记多选边界框需要更新 - } - - /** - * 设置当前的“悬停状态” - */ - public void setSuspension(boolean suspension) { - isSuspension = suspension; - } - - /** - * 清除脏标记 - */ - public void markClean() { - this.dirty = false; - } - - /** - * 检查数据是否已修改 - */ - public boolean isDirty() { - return dirty; - } - - /** - * 将网格数据上传到 GPU(生成 VAO/VBO/EBO) - */ - public void uploadToGPU() { - if (uploaded) return; - - for (int i = activeVertexList.size() - 1; i >= 0; i--) { - Vertex v = activeVertexList.get(i); - if (v != null && v.isDelete()) { - removeVertexAndRemapIndices(v); - } - } - RenderSystem.assertOnRenderThread(); - - int vertexCount = getVertexCount(); - if (vertexCount == 0 || activeVertexList.getIndices().length == 0) { - return; - } - - FloatBuffer interleaved = MemoryUtil.memAllocFloat(vertexCount * 4); - IntBuffer ib = MemoryUtil.memAllocInt(activeVertexList.getIndices().length); - try { - getInterleavedBuffer(interleaved); - getIndexBuffer(ib); - - vaoId = RenderSystem.glGenVertexArrays(); - RenderSystem.glBindVertexArray(() -> vaoId); - - vboId = RenderSystem.glGenBuffers(); - RenderSystem.glBindBuffer(RenderSystem.GL_ARRAY_BUFFER, () -> vboId); - RenderSystem.glBufferData(RenderSystem.GL_ARRAY_BUFFER, interleaved, RenderSystem.GL_STATIC_DRAW); - - eboId = RenderSystem.glGenBuffers(); - RenderSystem.glBindBuffer(RenderSystem.GL_ELEMENT_ARRAY_BUFFER, () -> eboId); - RenderSystem.glBufferData(RenderSystem.GL_ELEMENT_ARRAY_BUFFER, ib, RenderSystem.GL_STATIC_DRAW); - - int stride = 4 * Float.BYTES; - RenderSystem.enableVertexAttribArray(0); - RenderSystem.vertexAttribPointer(0, 2, RenderSystem.GL_FLOAT, false, stride, 0); - RenderSystem.enableVertexAttribArray(1); - RenderSystem.vertexAttribPointer(1, 2, RenderSystem.GL_FLOAT, false, stride, 2 * Float.BYTES); - RenderSystem.glBindVertexArray(() -> 0); - - indexCount = activeVertexList.getIndices().length; - uploaded = true; - markClean(); // 上传完成后,将数据标记为干净 - } finally { - MemoryUtil.memFree(interleaved); - MemoryUtil.memFree(ib); - RenderSystem.glBindBuffer(RenderSystem.GL_ARRAY_BUFFER, () -> 0); - } - } - - /** - * 绘制网格(会在第一次绘制时自动上传到 GPU) - */ - public void draw(int shaderProgram, Matrix3f modelMatrix) { - if (GlobalEventBus.EVENT_BUS.post(new Mesh2DRender.Start(this, shaderProgram, modelMatrix)).isCancelled()) { - return; - } - - if (!visible) return; - if (activeVertexList.getIndices() == null || activeVertexList.getIndices().length == 0) return; - if (dirty) { - deleteGPU(); - uploadToGPU(); - } else if (!uploaded) { - uploadToGPU(); - } - - // 保存当前 program,必要时恢复 - int prevProgram = RenderSystem.getCurrentProgram(); - boolean switchedProgram = false; - if (shaderProgram != 0 && prevProgram != shaderProgram) { - RenderSystem.useProgram(shaderProgram); - switchedProgram = true; - } - - // 确保纹理绑定到纹理单元0,并通知 shader 使用纹理单元0 - RenderSystem.activeTexture(GL13.GL_TEXTURE0); - if (texture != null) { - texture.bind(); - } else { - RenderSystem.bindTexture(0); - } - if (shaderProgram != 0) { - int texLoc = RenderSystem.getUniformLocation(shaderProgram, "uTexture"); - if (texLoc != -1) RenderSystem.uniform1i(texLoc, 0); - } - - if (shaderProgram != 0) { - int loc = RenderSystem.getUniformLocation(shaderProgram, "uModelMatrix"); - if (loc == -1) loc = RenderSystem.getUniformLocation(shaderProgram, "uModel"); - if (loc != -1) RenderSystem.uniformMatrix3(loc, modelMatrix); - } - - // 绑定 VAO 并绘制 - RenderSystem.glBindVertexArray(() -> vaoId); - RenderSystem.drawElements(RenderSystem.DRAW_TRIANGLES, indexCount, - RenderSystem.GL_UNSIGNED_INT, 0); - RenderSystem.glBindVertexArray(() -> 0); - - // 解绑纹理(恢复到单元0的绑定0) - if (texture != null) { - texture.unbind(); - } else { - RenderSystem.bindTexture(0); - } - - // 恢复之前的 program(如果我们切换过) - if (switchedProgram) { - RenderSystem.useProgram(prevProgram); - } - - // 选中框绘制(需要切换到固色 shader) - if (selected && !isRenderVertices) { - RenderSystem.pushState(); - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - try { - setSolidShader(modelMatrix); - if (isInMultiSelection()) { - drawMultiSelectionBox(); - } else { - drawSelectBox(); - } - } finally { - RenderSystem.popState(); - } - } - - RanderToolsManager.getInstance().renderAllTools(modelMatrix, this); - - if (isSuspension && !selected) { - RenderSystem.pushState(); - setSolidShader(modelMatrix); - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - - Tesselator t = Tesselator.getInstance(); - BufferBuilder bb = t.getBuilder(); - BoundingBox bbox = getBounds(); - float zoom = ModelRender.getCamera().getZoom(); - - if (bbox != null && bbox.isValid() && zoom > 1e-6f) { - - // --- 1. 定义所有元素的期望“像素”尺寸 --- - final float PIXEL_BORDER_THICKNESS = 1.5f; - final float PIXEL_BOX_OFFSET_Y = 8.0f; // 提示框距离物体顶部的垂直像素距离 - final float PIXEL_FONT_SIZE_BASE = 14.0f; - final float PIXEL_LINE_HEIGHT = 18.0f; - final float PIXEL_PADDING = 5.0f; // 文本背景框的内边距 - - // --- 2. 根据 zoom 计算出在“世界坐标”中应有的大小 --- - float worldBorderThickness = PIXEL_BORDER_THICKNESS / zoom; - float worldBoxOffsetY = PIXEL_BOX_OFFSET_Y / zoom; - float worldLineHeight = PIXEL_LINE_HEIGHT / zoom; - float worldPadding = PIXEL_PADDING / zoom; - float textScale = (PIXEL_FONT_SIZE_BASE / 14.0f) / zoom; - - // --- 3. 绘制动态厚度的红色悬停边框 --- - bb.begin(GL11.GL_TRIANGLES, 4 * 6); - bb.setColor(new Vector4f(1f, 0f, 0f, 1f)); - MultiSelectionBoxRenderer.addQuadLine(bb, bbox.getMinX(), bbox.getMinY(), bbox.getMaxX(), bbox.getMinY(), worldBorderThickness); - MultiSelectionBoxRenderer.addQuadLine(bb, bbox.getMaxX(), bbox.getMinY(), bbox.getMaxX(), bbox.getMaxY(), worldBorderThickness); - MultiSelectionBoxRenderer.addQuadLine(bb, bbox.getMaxX(), bbox.getMaxY(), bbox.getMinX(), bbox.getMaxY(), worldBorderThickness); - MultiSelectionBoxRenderer.addQuadLine(bb, bbox.getMinX(), bbox.getMaxY(), bbox.getMinX(), bbox.getMinY(), worldBorderThickness); - t.end(); - - // --- 4. 计算文本布局 --- - String hoverText = getName(); - List lines = splitLines(hoverText, 30); - float maxTextWidth = 0f; - for (String line : lines) { - maxTextWidth = Math.max(maxTextWidth, ModelRender.getTextRenderer().getTextWidth(line) * textScale); - } - float totalTextHeight = (lines.size() -1) * worldLineHeight; - float boxX = bbox.getCenterX() - (maxTextWidth / 2f); - float boxY = bbox.getMaxY() + worldBoxOffsetY; - Vector4f bgColor = new Vector4f(1f, 0f, 0f, 0.8f); - bb.begin(GL11.GL_TRIANGLES, 6); - bb.setColor(bgColor); - float bgX0 = boxX - worldPadding; - float bgY0 = boxY - worldPadding; - float bgX1 = boxX + maxTextWidth + worldPadding; - float bgY1 = boxY + totalTextHeight + worldPadding + worldLineHeight; - MultiSelectionBoxRenderer.addFilledQuad(bb, bgX0, bgY0, bgX1, bgY1); - t.end(); - Vector4f fgColor = new Vector4f(1f, 1f, 1f, 1f); - for (int i = 0; i < lines.size(); i++) { - String line = lines.get(i); - float lineY = boxY + (lines.size() - 1 - i) * worldLineHeight; - ModelRender.renderText(line, boxX, lineY + 30.0f, fgColor, textScale); - } - } - - RenderSystem.popState(); - } - GlobalEventBus.EVENT_BUS.post(new Mesh2DRender.End(this, shaderProgram, modelMatrix)); - } - - public void setSolidShader(Matrix3f modelMatrix) { - ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader"); - if (solidShader != null && solidShader.programId != 0) { - solidShader.use(); - int modelLoc = solidShader.getUniformLocation("uModelMatrix"); - if (modelLoc != -1) { - RenderSystem.uniformMatrix3(modelLoc, modelMatrix); - } - int colorLoc = solidShader.getUniformLocation("uColor"); - if (colorLoc != -1) { - RenderSystem.uniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); - } - } - } - - public static List splitLines(String text, int maxCharsPerLine) { - List lines = new ArrayList<>(); - StringBuilder line = new StringBuilder(); - - for (String word : text.split(" ")) { - if (line.length() + word.length() + 1 > maxCharsPerLine) { - if (!line.isEmpty()) { - lines.add(line.toString()); - line = new StringBuilder(); - } - if (word.length() > maxCharsPerLine) { - int start = 0; - while (start < word.length()) { - int end = Math.min(start + maxCharsPerLine, word.length()); - lines.add(word.substring(start, end)); - start = end; - } - continue; - } - } - if (!line.isEmpty()) { - line.append(" "); - } - line.append(word); - } - if (!line.isEmpty()) { - lines.add(line.toString()); - } - return lines; - } - - private void drawSelectBox() { - BoundingBox bounds = getBounds(); - MultiSelectionBoxRenderer.drawSelectBox(bounds, pivot, ModelRender.getCamera().getZoom()); - } - - /** - * 添加网格到多选列表 - */ - public void addToMultiSelection(Mesh2D mesh) { - if (mesh != null && !multiSelectedParts.contains(mesh)) { - multiSelectedParts.add(mesh); - multiSelectionDirty = true; - markDirty(); - } - } - - /** - * 从多选列表移除网格 - */ - public void removeFromMultiSelection(Mesh2D mesh) { - if (multiSelectedParts.remove(mesh)) { - multiSelectionDirty = true; - markDirty(); - } - } - - /** - * 清空多选列表 - */ - public void clearMultiSelection() { - if (!multiSelectedParts.isEmpty()) { - multiSelectedParts.clear(); - multiSelectionDirty = true; - markDirty(); - } - } - - /** - * 获取多选列表 - */ - public List getMultiSelectedParts() { - return new ArrayList<>(multiSelectedParts); - } - - /** - * 检查是否在多选状态 - */ - public boolean isInMultiSelection() { - return !multiSelectedParts.isEmpty(); - } - - /** - * 获取多选状态下的组合边界框 - */ - public BoundingBox getMultiSelectionBounds() { - if (multiSelectionDirty) { - updateMultiSelectionBounds(); - } - return multiSelectionBounds; - } - - /** - * 更新多选边界框 - */ - private void updateMultiSelectionBounds() { - multiSelectionBounds.reset(); - - // 首先包含自己的边界(应用变换后的边界) - BoundingBox selfBounds = getBounds(); - if (selfBounds.isValid()) { - multiSelectionBounds.expand(selfBounds); - } - - // 然后包含所有多选部分的边界(应用它们各自的变换) - for (Mesh2D mesh : multiSelectedParts) { - // 确保其他网格的边界也是最新的 - mesh.updateBounds(); - BoundingBox meshBounds = mesh.getBounds(); - if (meshBounds.isValid()) { - multiSelectionBounds.expand(meshBounds); - } - } - - multiSelectionDirty = false; - } - - /** - * 强制更新多选边界框(在外部变换操作后调用) - */ - public void forceUpdateMultiSelectionBounds() { - multiSelectionDirty = true; - updateMultiSelectionBounds(); - } - - /** - * 检查点是否在多选边界框内 - */ - public boolean multiSelectionContainsPoint(float x, float y) { - if (!isInMultiSelection()) { - return containsPoint(x, y); - } - - BoundingBox multiBounds = getMultiSelectionBounds(); - return multiBounds.contains(x, y); - } - - /** - * 在多选状态下绘制组合边界框 - */ - private void drawMultiSelectionBox() { - BoundingBox multiBounds = getMultiSelectionBounds(); - MultiSelectionBoxRenderer.drawMultiSelectionBox(multiBounds,ModelRender.getCamera().getZoom()); - } - - /** - * 计算模型的边界框 [minX, minY, maxX, maxY] - */ - public float[] calculateBoundingBox() { - // 使用现有的边界计算功能 - BoundingBox bounds = getBounds(); - return new float[]{ - bounds.getMinX(), - bounds.getMinY(), - bounds.getMaxX(), - bounds.getMaxY() - }; - } - - /** - * 计算带扩展的边界框 [minX, minY, maxX, maxY] - */ - public float[] calculateBoundingBox(float expand) { - float[] bounds = calculateBoundingBox(); - return new float[]{ - bounds[0] - expand, - bounds[1] - expand, - bounds[2] + expand, - bounds[3] + expand - }; - } - public void draw() { - if (!visible) return; - if (activeVertexList.getIndices() == null || activeVertexList.getIndices().length == 0) return; - if (!uploaded) { - uploadToGPU(); - } - if (texture != null) { - texture.bind(); - } - GL30.glBindVertexArray(vaoId); - GL11.glDrawElements(GL11.GL_TRIANGLES, indexCount, GL11.GL_UNSIGNED_INT, 0); - GL30.glBindVertexArray(0); - if (texture != null) { - texture.unbind(); - } - } - - /** - * 从 GPU 删除本网格相关的 VAO/VBO/EBO - */ - public void deleteGPU() { - if (!uploaded) return; - // 禁用属性并删除缓冲 - try { - GL30.glBindVertexArray(0); - if (vboId != -1) { - GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); - GL15.glDeleteBuffers(vboId); - vboId = -1; - } - if (eboId != -1) { - GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0); - GL15.glDeleteBuffers(eboId); - eboId = -1; - } - if (vaoId != -1) { - GL30.glDeleteVertexArrays(vaoId); - vaoId = -1; - } - } catch (Exception ignored) { - // 在某些上下文销毁阶段 GL 调用可能不可用 - } finally { - uploaded = false; - } - } - - // ==================== Getter/Setter ==================== - - public String getName() { - if (modelPart != null){ - return modelPart.getName(); - } - return name; - } - - public void setName(String name) { - this.name = name; - } - - public float[] getVertices() { - if (activeVertexList == null) return new float[0]; - float[] verts = new float[getVertexCount() * 2]; - for (int i = 0; i < getVertexCount(); i++) { - Vertex v = activeVertexList.get(i); - verts[i * 2] = v.position.x; - verts[i * 2 + 1] = v.position.y; - } - return verts; - } - - public float[] getUVs() { - if (activeVertexList == null) return new float[0]; - float[] uvs = new float[getVertexCount() * 2]; - for (int i = 0; i < getVertexCount(); i++) { - Vertex v = activeVertexList.get(i); - uvs[i * 2] = v.uv.x; - uvs[i * 2 + 1] = v.uv.y; - } - return uvs; - } - - public int[] getIndices() { - return activeVertexList.getIndices().clone(); - } - - public Texture getTexture() { - return texture; - } - - public void setTexture(Texture texture) { - this.texture = texture; - } - - public boolean isVisible() { - return visible; - } - - public void setVisible(boolean visible) { - this.visible = visible; - } - - public int getDrawMode() { - return drawMode; - } - - public void setDrawMode(int drawMode) { - if (drawMode < POINTS || drawMode > TRIANGLE_FAN) { - throw new IllegalArgumentException("Invalid draw mode: " + drawMode); - } - this.drawMode = drawMode; - } - - public int getIndexCount() { - return activeVertexList.getIndices().length; - } - - // ==================== 工具方法 ==================== - - /** - * 创建网格的深拷贝 - */ - public Mesh2D copy() { - Mesh2D copy = new Mesh2D(name + "_copy"); - copy.pivot = new Vector2f(this.pivot); - copy.originalPivot = new Vector2f(this.originalPivot); - copy.texture = this.texture; - copy.visible = this.visible; - copy.drawMode = this.drawMode; - copy.bakedToWorld = this.bakedToWorld; - copy.dirty = true; - copy.boundsDirty = true; - copy.selected = this.selected; - return copy; - } - - - public int getVaoId() { - return vaoId; - } - - /** - * 获取绘制模式字符串 - */ - public String getDrawModeString() { - switch (drawMode) { - case POINTS: return "POINTS"; - case LINES: return "LINES"; - case LINE_STRIP: return "LINE_STRIP"; - case TRIANGLES: return "TRIANGLES"; - case TRIANGLE_STRIP: return "TRIANGLE_STRIP"; - case TRIANGLE_FAN: return "TRIANGLE_FAN"; - default: return "UNKNOWN"; - } - } - - /** 标记或查询网格顶点是否已经被烘焙到世界坐标 */ - public void setBakedToWorld(boolean baked) { - this.bakedToWorld = baked; - } - - public boolean isBakedToWorld() { - return bakedToWorld; - } - - - // ==================== Object 方法 ==================== - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Mesh2D mesh2D = (Mesh2D) o; - return visible == mesh2D.visible && - drawMode == mesh2D.drawMode && - Objects.equals(name, mesh2D.name) && - Objects.equals(pivot, mesh2D.pivot); - } - - @Override - public int hashCode() { - int result = Objects.hash(name, - pivot, - visible, drawMode); - result = 31 * result + java.util.Arrays.hashCode(activeVertexList.getIndices()); - return result; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("Mesh2D{") - .append("name='").append(name).append('\'') - .append(", activeList='").append(activeVertexList != null ? activeVertexList.getName() : "null").append('\'') - .append(", vertices=").append(getVertexCount()) - .append(", pivot=(").append(String.format("%.2f", pivot.x)) - .append(", ").append(String.format("%.2f", pivot.y)).append(")") - .append(", visible=").append(visible) - .append(", selected=").append(selected) - .append(", inMultiSelection=").append(isInMultiSelection()) - .append(", multiSelectionCount=").append(multiSelectedParts.size()) - .append(", drawMode=").append(getDrawModeString()) - .append(", bounds=").append(getBounds()); - if (isInMultiSelection()) { - sb.append(", multiSelectionBounds=").append(getMultiSelectionBounds()); - } - if (activeVertexList != null && !activeVertexList.isEmpty()) { - sb.append(", coordinates=["); - for (int i = 0; i < activeVertexList.size(); i++) { - if (i > 0) sb.append(", "); - Vertex v = activeVertexList.get(i); - sb.append("(") - .append(String.format("%.2f", v.position.x)) - .append(", ") - .append(String.format("%.2f", v.position.y)) - .append(")"); - } - sb.append("]"); - } - - sb.append('}'); - return sb.toString(); - } - - private static class Delaunay { - public static List triangulate(List vertices) { - List triangles = new ArrayList<>(); - if (vertices == null || vertices.size() < 3) { - return triangles; - } - - // 1. 创建一个足够大的“超级三角形”,它必须能包含所有点 - float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE; - float maxX = Float.MIN_VALUE, maxY = Float.MIN_VALUE; - for (Vertex v : vertices) { - minX = Math.min(minX, v.originalPosition.x); - minY = Math.min(minY, v.originalPosition.y); - maxX = Math.max(maxX, v.originalPosition.x); - maxY = Math.max(maxY, v.originalPosition.y); - } - float dx = maxX - minX; - float dy = maxY - minY; - float deltaMax = Math.max(dx, dy); - float midx = (minX + maxX) / 2f; - float midy = (minY + maxY) / 2f; - - Vertex p1 = new Vertex(midx - 20 * deltaMax, midy - deltaMax, 0, 0); - Vertex p2 = new Vertex(midx, midy + 20 * deltaMax, 0, 0); - Vertex p3 = new Vertex(midx + 20 * deltaMax, midy - deltaMax, 0, 0); - Triangle superTriangle = new Triangle(p1, p2, p3); - - triangles.add(superTriangle); - - // 2. 逐个将点添加到三角剖分中 - for (Vertex vertex : vertices) { - List badTriangles = new ArrayList<>(); - List polygon = new ArrayList<>(); - - // a. 找到所有外接圆包含当前点的“坏”三角形 - for (Triangle t : triangles) { - if (t.circumcircleContains(vertex)) { - badTriangles.add(t); - } - } - - // b. 从“坏”三角形中提取出不共享的边,形成一个多边形空腔 - for (Triangle t1 : badTriangles) { - for (Edge e : t1.edges) { - boolean shared = false; - for (Triangle t2 : badTriangles) { - if (t1 != t2 && t2.hasEdge(e)) { - shared = true; - break; - } - } - if (!shared) { - polygon.add(e); - } - } - } - - // c. 移除所有“坏”三角形 - triangles.removeAll(badTriangles); - - // d. 将当前点与多边形空腔的每一条边连接,形成新的三角形 - for (Edge e : polygon) { - triangles.add(new Triangle(e.p1, e.p2, vertex)); - } - } - - // 3. 移除所有与“超级三角形”的顶点相连的三角形 - triangles.removeIf(t -> t.hasVertex(p1) || t.hasVertex(p2) || t.hasVertex(p3)); - - return triangles; - } - } - - public void removeVertexAndRemapIndices(Vertex vertexToRemove) { - if (activeVertexList == null || !activeVertexList.vertices.contains(vertexToRemove)) { - return; - } - - final int removedIndex = activeVertexList.vertices.indexOf(vertexToRemove); - if (removedIndex == -1) { - return; - } - - // 备份旧索引和顶点快照(在移除顶点前获取) - int[] oldIndices = activeVertexList.getIndices() != null ? activeVertexList.getIndices().clone() : new int[0]; - List oldVerticesSnapshot = activeVertexList.getVertices(); // 位置等信息用它来计算 - Vector2f removedPos = oldVerticesSnapshot.get(removedIndex).originalPosition; - - // 收集与被移除顶点相连的邻居(无序集合去重) - Set neighborIndices = new LinkedHashSet<>(); - for (int i = 0; i < oldIndices.length; i += 3) { - int i1 = oldIndices[i]; - int i2 = oldIndices[i + 1]; - int i3 = oldIndices[i + 2]; - - if (i1 == removedIndex) { - neighborIndices.add(i2); - neighborIndices.add(i3); - } else if (i2 == removedIndex) { - neighborIndices.add(i1); - neighborIndices.add(i3); - } else if (i3 == removedIndex) { - neighborIndices.add(i1); - neighborIndices.add(i2); - } - } - - // 如果邻居少于3,不存在需要填补的多边形,直接移除并重映射索引即可 - List sortedNeighbors = new ArrayList<>(neighborIndices); - // 若邻居>=3,按角度排序以重建有序环(相对于被移除顶点) - if (sortedNeighbors.size() >= 3) { - sortedNeighbors.sort((a, b) -> { - Vector2f pa = oldVerticesSnapshot.get(a).originalPosition; - Vector2f pb = oldVerticesSnapshot.get(b).originalPosition; - double angA = Math.atan2(pa.y - removedPos.y, pa.x - removedPos.x); - double angB = Math.atan2(pb.y - removedPos.y, pb.x - removedPos.x); - return Double.compare(angA, angB); - }); - } - - // 执行真正的顶点移除(会调整 activeVertexList.vertices) - activeVertexList.remove(removedIndex); - - // 重建索引:先把原索引中不包含 removedIndex 的三角形添加进来,并对大于 removedIndex 的索引 -1 - List newIndicesList = new ArrayList<>(); - for (int i = 0; i < oldIndices.length; i += 3) { - int i1 = oldIndices[i]; - int i2 = oldIndices[i + 1]; - int i3 = oldIndices[i + 2]; - - // 跳过包含已删除顶点的三角形 - if (i1 == removedIndex || i2 == removedIndex || i3 == removedIndex) { - continue; - } - - if (i1 > removedIndex) i1--; - if (i2 > removedIndex) i2--; - if (i3 > removedIndex) i3--; - - newIndicesList.add(i1); - newIndicesList.add(i2); - newIndicesList.add(i3); - } - - // 填补孔洞:使用按角度排序的邻居形成扇形三角化(若邻居>=3) - if (sortedNeighbors.size() >= 3) { - // remap neighbor indices 到移除后索引空间 - List remapped = new ArrayList<>(); - for (int n : sortedNeighbors) { - int rn = (n > removedIndex) ? (n - 1) : n; - // 避免重复 - if (remapped.isEmpty() || remapped.get(remapped.size() - 1) != rn) { - remapped.add(rn); - } - } - // 去重首尾相同的项(若存在) - if (remapped.size() >= 2 && remapped.get(0).equals(remapped.get(remapped.size() - 1))) { - remapped.remove(remapped.size() - 1); - } - - // 如果仍然不足3个顶点则跳过填充 - if (remapped.size() >= 3) { - // 检查顺序方向(计算多边形的有向面积),确保为逆时针,如为顺时针则反转(以获得一致的三角形朝向) - double area = 0.0; - List currentVertices = activeVertexList.getVertices(); - for (int i = 0; i < remapped.size(); i++) { - Vector2f p1 = currentVertices.get(remapped.get(i)).originalPosition; - Vector2f p2 = currentVertices.get(remapped.get((i + 1) % remapped.size())).originalPosition; - area += (p1.x * p2.y - p2.x * p1.y); - } - if (area < 0) { // 顺时针 -> 反转为逆时针 - Collections.reverse(remapped); - } - - // 选择扇形三角化的枢轴顶点为 remapped.get(0) - int pivot = remapped.get(0); - for (int i = 1; i < remapped.size() - 1; i++) { - int a = pivot; - int b = remapped.get(i); - int c = remapped.get(i + 1); - // 防止生成退化三角形(共线) - Vector2f pa = activeVertexList.get(a).originalPosition; - Vector2f pb = activeVertexList.get(b).originalPosition; - Vector2f pc = activeVertexList.get(c).originalPosition; - float cross = (pb.x - pa.x) * (pc.y - pa.y) - (pb.y - pa.y) * (pc.x - pa.x); - if (Math.abs(cross) < 1e-8f) { - // 跳过退化三角形 - continue; - } - newIndicesList.add(a); - newIndicesList.add(b); - newIndicesList.add(c); - } - } - } - - // 最后设置新索引并标记脏 - activeVertexList.setIndices(newIndicesList.stream().mapToInt(Integer::intValue).toArray()); - markDirty(); - } - - private static class Triangle { - Vertex p1, p2, p3; - Edge[] edges; - Circle circumcircle; - - Triangle(Vertex v1, Vertex v2, Vertex v3) { - this.p1 = v1; this.p2 = v2; this.p3 = v3; - this.edges = new Edge[]{new Edge(v1, v2), new Edge(v2, v3), new Edge(v3, v1)}; - - // 计算外接圆 - float ax = p1.originalPosition.x, ay = p1.originalPosition.y; - float bx = p2.originalPosition.x, by = p2.originalPosition.y; - float cx = p3.originalPosition.x, cy = p3.originalPosition.y; - - float d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)); - if (Math.abs(d) < 1e-12) { // 共线 - this.circumcircle = new Circle(0,0,-1); // 无效圆 - return; - } - float asq = ax * ax + ay * ay; - float bsq = bx * bx + by * by; - float csq = cx * cx + cy * cy; - float ux = (asq * (by - cy) + bsq * (cy - ay) + csq * (ay - by)) / d; - float uy = (asq * (cx - bx) + bsq * (ax - cx) + csq * (bx - ax)) / d; - float r = (float) Math.sqrt((ax - ux) * (ax - ux) + (ay - uy) * (ay - uy)); - this.circumcircle = new Circle(ux, uy, r); - } - - boolean hasVertex(Vertex v) { return p1 == v || p2 == v || p3 == v; } - boolean hasEdge(Edge e) { return edges[0].equals(e) || edges[1].equals(e) || edges[2].equals(e); } - boolean circumcircleContains(Vertex v) { return circumcircle.contains(v); } - } - - private static class Edge { - Vertex p1, p2; - Edge(Vertex v1, Vertex v2) { this.p1 = v1; this.p2 = v2; } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - Edge edge = (Edge) obj; - return (p1 == edge.p1 && p2 == edge.p2) || (p1 == edge.p2 && p2 == edge.p1); - } - @Override - public int hashCode() { return Objects.hash(p1) + Objects.hash(p2); } - } - - private static class Circle { - float x, y, radius, radiusSq; - Circle(float x, float y, float r) { - this.x = x; this.y = y; this.radius = r; this.radiusSq = r * r; - } - boolean contains(Vertex v) { - if (radius < 0) return false; - float dx = x - v.originalPosition.x; - float dy = y - v.originalPosition.y; - return (dx * dx + dy * dy) < radiusSq; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java deleted file mode 100644 index 71395e9..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java +++ /dev/null @@ -1,683 +0,0 @@ -package com.chuangzhou.vivid2D.render.model; - -import com.chuangzhou.vivid2D.render.model.data.ModelData; -import com.chuangzhou.vivid2D.render.model.data.ModelMetadata; -import com.chuangzhou.vivid2D.render.model.util.*; -import com.chuangzhou.vivid2D.util.ModelDataJsonConverter; -import org.joml.Matrix3f; - -import javax.swing.tree.DefaultMutableTreeNode; -import java.util.*; - -/** - * 2D 模型核心数据结构 - * - *

定义 vivid2D 模型系统中的核心数据结构和基础数据类型,包括:

- * - *
    - *
  • 几何数据:顶点、边、面等基本几何元素
  • - *
  • 拓扑结构:模型的组织关系和连接信息
  • - *
  • 属性数据:颜色、纹理坐标、法向量等附加属性
  • - *
  • 层次结构:模型的父子关系和变换信息
  • - *
- * - *

主要包含:

- *
    - *
  • 基础几何类(点、向量、矩阵)
  • - *
  • 模型节点和组件类
  • - *
  • 数据容器和缓冲区
  • - *
  • 序列化和反序列化支持
  • - *
- * - * @author tzdwindows - * @version 1.0 - * @since 2024-01-01 - */ -public class Model2D { - // ==================== 基础属性 ==================== - private String name; - private String version = "1.0.0"; - private UUID uuid; - private ModelMetadata metadata; - - // ==================== 层级结构 ==================== - private final List parts; - private final Map partMap; // 快速查找 - private ModelPart rootPart; - - // ==================== 网格系统 ==================== - private final List meshes; - private final Map textures; // 纹理映射 - - // ==================== 动画系统 ==================== - private final Map parameters; - private final List animationLayers; - private final PhysicsSystem physics; - - // ==================== 渲染状态 ==================== - private transient boolean needsUpdate = true; - private transient BoundingBox bounds; - - // ==================== 姿态系统 ==================== - private final Map poses; // 存储所有预设姿态 - private String currentPoseName = "default"; // 当前应用的姿态名称 - private transient ModelPose currentPose; // 当前姿态实例 - private transient ModelPose blendTargetPose; // 混合目标姿态 - private float blendProgress = 1.0f; // 混合进度 (0-1) - private float blendSpeed = 1.0f; // 混合速度 - - - // ==================== 光源系统 ==================== - private final List lights; - - private final List events = new java.util.concurrent.CopyOnWriteArrayList<>(); - // ==================== 构造器 ==================== - public Model2D() { - this.uuid = UUID.randomUUID(); - this.parts = new ArrayList<>() { - @Override - public boolean add(ModelPart modelPart) {triggerEvent("model_part_added");return super.add(modelPart);} - - @Override - public void add(int index, ModelPart element) {triggerEvent("model_part_added");super.add(index, element);} - - @Override - public ModelPart set(int index, ModelPart element) {triggerEvent("model_part_added");return super.set(index, element);} - }; - this.partMap = new HashMap<>(); - this.meshes = new ArrayList<>(); - this.textures = new HashMap<>(); - this.parameters = new LinkedHashMap<>(); // 保持插入顺序 - this.animationLayers = new ArrayList<>(); - this.physics = new PhysicsSystem(); - this.currentPose = new ModelPose(); - this.metadata = new ModelMetadata(); - this.lights = new ArrayList<>(); - - this.poses = new HashMap<>(); - this.currentPose = new ModelPose("default"); - initializeDefaultPose(); - } - - public Model2D(String name) { - this(); - this.name = name; - } - - // ==================== 姿态管理 ==================== - - /** - * 添加或更新姿态 - */ - public void addPose(ModelPose pose) { - if (pose == null) { - throw new IllegalArgumentException("Pose cannot be null"); - } - poses.put(pose.getName(), pose); - markNeedsUpdate(); - } - - /** - * 获取姿态 - */ - public ModelPose getPose(String name) { - return poses.get(name); - } - - /** - * 应用姿态(立即) - */ - public void applyPose(String poseName) { - ModelPose pose = poses.get(poseName); - if (pose != null) { - applyPoseInternal(pose); - this.currentPoseName = poseName; - this.currentPose = pose; - this.blendProgress = 1.0f; - this.blendTargetPose = null; - markNeedsUpdate(); - } - } - - /** - * 混合到目标姿态 - */ - public void blendToPose(String targetPoseName, float blendTime) { - ModelPose targetPose = poses.get(targetPoseName); - if (targetPose != null) { - this.blendTargetPose = targetPose; - this.blendProgress = 0.0f; - this.blendSpeed = blendTime > 0 ? 1.0f / blendTime : 10.0f; // 默认0.1秒 - markNeedsUpdate(); - } - } - - /** - * 保存当前状态为姿态 - */ - public void saveCurrentPose(String poseName) { - ModelPose pose = new ModelPose(poseName); - captureCurrentPose(pose); - addPose(pose); - } - - /** - * 获取当前姿态名称 - */ - public String getCurrentPoseName() { - return currentPoseName; - } - - // ==================== 光源管理 ==================== - public List getLights() { - return Collections.unmodifiableList(lights); - } - - public void addLight(LightSource light) { - if (light == null) { - throw new IllegalArgumentException("LightSource cannot be null"); - } - lights.add(light); - markNeedsUpdate(); - } - - public void removeLight(LightSource light) { - if (lights.remove(light)) { - markNeedsUpdate(); - } - } - - public boolean isStartLight(LightSource light) { - return lights.isEmpty(); - } - - public void clearLights() { - lights.clear(); - markNeedsUpdate(); - } - - // ==================== 部件管理 ==================== - public ModelPart createPart(String name) { - ModelPart part = new ModelPart(name); - addPart(part); - return part; - } - - private void triggerEvent(String eventName) { - for (ModelEvent event : events) { - if (event != null) - event.trigger(eventName,this); - } - } - - public void addEvent(ModelEvent event) { - events.add(event); - } - - public void removeEvent(ModelEvent event) { - events.remove(event); - } - public void addPart(ModelPart part) { - if (partMap.containsKey(part.getName())) { - throw new IllegalArgumentException("Part already exists: " + part.getName()); - } - parts.add(part); - partMap.put(part.getName(), part); - - // 设置根部件(第一个添加的部件) - if (rootPart == null) { - rootPart = part; - } - - triggerEvent("model_part_added"); - } - - public ModelPart getPart(String name) { - return partMap.get(name); - } - - public Map getPartMap() { - return partMap; - } - - public List getParts() { - return parts; - } - - // ==================== 参数管理 ==================== - public AnimationParameter createParameter(String id, float min, float max, float defaultValue) { - AnimationParameter param = new AnimationParameter(id, min, max, defaultValue); - parameters.put(id, param); - return param; - } - - public AnimationParameter getParameter(String id) { - return parameters.get(id); - } - - public void addParameter(AnimationParameter param) { - parameters.put(param.getId(), param); - } - - public void setParameterValue(String paramId, float value) { - AnimationParameter param = parameters.get(paramId); - if (param != null) { - param.setValue(value); - markNeedsUpdate(); - } - } - - public float getParameterValue(String paramId) { - AnimationParameter param = parameters.get(paramId); - return param != null ? param.getValue() : 0.0f; - } - - // ==================== 网格管理 ==================== - public Mesh2D createMesh(String name, float[] vertices, float[] uvs, int[] indices) { - Mesh2D mesh = new Mesh2D(name, vertices, uvs, indices); - meshes.add(mesh); - return mesh; - } - - public void addMesh(Mesh2D mesh) { - meshes.add(mesh); - } - - public Mesh2D getMesh(String name) { - for (Mesh2D mesh : meshes) { - if (mesh.getName().equals(name)) { - return mesh; - } - } - return null; - } - - // ==================== 纹理管理 ==================== - public void addTexture(Texture texture) { - if (texture == null) { - throw new IllegalArgumentException("Texture cannot be null"); - } - - String textureName = texture.getName(); - if (textureName == null || textureName.trim().isEmpty()) { - throw new IllegalArgumentException("Texture name cannot be null or empty"); - } - - if (textures.containsKey(textureName)) { - Texture oldTexture = textures.get(textureName); - if (oldTexture != null && oldTexture != texture) { - oldTexture.dispose(); - } - } - - textures.put(textureName, texture); - } - - public Texture getTexture(String name) { - return textures.get(name); - } - - public Map getTextures() { - return Collections.unmodifiableMap(textures); - } - - // ==================== 动画层管理 ==================== - public AnimationLayer createAnimationLayer(String name) { - AnimationLayer layer = new AnimationLayer(name); - animationLayers.add(layer); - return layer; - } - - // ==================== 更新系统 (已修改) ==================== - public void update(float deltaTime) { - - updatePoseBlending(deltaTime); - - // 物理系统更新已被移至渲染器(ModelRender)中,以确保它在渲染前被调用。 - // 这里的 hasActivePhysics() 检查可以保留,用于决定是否需要更新变换,以优化性能。 - if (!needsUpdate && !physics.hasActivePhysics()) { - return; - } - - // 核心修改:移除了 physics.update(deltaTime, this); 这一行。 - // 该调用现在由 ModelRender.render() 方法负责。 - - // 更新所有参数驱动的变形 - updateParameterDeformations(); - - // 更新层级变换 - updateHierarchyTransforms(); - - // 更新包围盒 - updateBoundingBox(); - - needsUpdate = false; - } - - /** - * 更新姿态混合 - */ - private void updatePoseBlending(float deltaTime) { - if (blendTargetPose != null && blendProgress < 1.0f) { - blendProgress += deltaTime * blendSpeed; - if (blendProgress >= 1.0f) { - blendProgress = 1.0f; - currentPose = blendTargetPose; - currentPoseName = blendTargetPose.getName(); - blendTargetPose = null; - } - markNeedsUpdate(); - } - } - - /** - * 应用当前姿态到模型 - */ - private void applyCurrentPoseToModel() { - if (blendTargetPose != null && blendProgress < 1.0f) { - // 混合姿态 - ModelPose blendedPose = ModelPose.lerp(currentPose, blendTargetPose, blendProgress, "blended"); - applyPoseInternal(blendedPose); - } else { - // 直接应用当前姿态 - applyPoseInternal(currentPose); - } - } - - /** - * 内部姿态应用方法 - */ - private void applyPoseInternal(ModelPose pose) { - for (String partName : pose.getPartNames()) { - ModelPart part = partMap.get(partName); - if (part != null) { - ModelPose.PartPose partPose = pose.getPartPose(partName); - part.setPosition(partPose.getPosition()); - part.setRotation(partPose.getRotation()); - part.setScale(partPose.getScale()); - part.setOpacity(partPose.getOpacity()); - part.setVisible(partPose.isVisible()); - } - } - } - - public DefaultMutableTreeNode toTreeNode() { - DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(this.name != null ? this.name : "模型"); - for (ModelPart part : parts) { - rootNode.add(part.toTreeNode()); - } - return rootNode; - } - - /** - * 捕获当前模型状态到姿态 - */ - private void captureCurrentPose(ModelPose pose) { - for (ModelPart part : parts) { - ModelPose.PartPose partPose = new ModelPose.PartPose( - part.getPosition(), - part.getRotation(), - part.getScale(), - part.getOpacity(), - part.isVisible(), - new org.joml.Vector3f(1, 1, 1) // 默认颜色,可根据需要修改 - ); - pose.setPartPose(part.getName(), partPose); - } - } - - /** - * 初始化默认姿态 - */ - private void initializeDefaultPose() { - ModelPose defaultPose = ModelPose.createDefaultPose(); - captureCurrentPose(defaultPose); - poses.put("default", defaultPose); - } - - private void updateParameterDeformations() { - for (AnimationParameter param : parameters.values()) { - if (param.hasChanged()) { - applyParameterDeformations(param); - param.markClean(); - } - } - } - - private void applyParameterDeformations(AnimationParameter param) { - // 这里将实现参数到具体变形的映射 - // 例如:参数"face_smile" -> 应用到嘴部网格的变形 - for (ModelPart part : parts) { - part.applyParameter(param); - } - } - - private void updateHierarchyTransforms() { - if (rootPart != null) { - Matrix3f matrix = new Matrix3f(); - matrix.identity(); - rootPart.updateWorldTransform(matrix, true); - } - } - - private void updateBoundingBox() { - if (bounds == null) { - bounds = new BoundingBox(); - } - bounds.reset(); - - for (ModelPart part : parts) { - bounds.expand(part.getWorldBounds()); - } - } - - // ==================== 工具方法 ==================== - public void markNeedsUpdate() { - this.needsUpdate = true; - } - - public boolean isVisible() { - return rootPart != null && rootPart.isVisible(); - } - - public void setVisible(boolean visible) { - if (rootPart != null) { - rootPart.setVisible(visible); - } - } - - /** - * 检查是否存在指定姿态 - */ - public boolean hasPose(String poseName) { - return poses.containsKey(poseName); - } - - /** - * 移除姿态 - */ - public void removePose(String poseName) { - if (!"default".equals(poseName)) { // 保护默认姿态 - poses.remove(poseName); - if (currentPoseName.equals(poseName)) { - applyPose("default"); // 回退到默认姿态 - } - } - } - - /** - * 获取所有姿态名称 - */ - public java.util.Set getPoseNames() { - return Collections.unmodifiableSet(poses.keySet()); - } - - /** - * 立即混合到姿态(指定混合系数) - */ - public void setPoseBlend(String poseName, float blendFactor) { - ModelPose targetPose = poses.get(poseName); - if (targetPose != null) { - ModelPose blendedPose = ModelPose.lerp(currentPose, targetPose, blendFactor, "manual_blend"); - applyPoseInternal(blendedPose); - markNeedsUpdate(); - } - } - - // ==================== 序列化支持 ==================== - public ModelData serialize() { - return new ModelData(this); - } - - public static Model2D deserialize(ModelData data) { - return data.deserializeToModel(); - } - - /** - * 保存模型到文件 - */ - public void saveToFile(String filePath) { - try { - ModelData data = serialize(); - data.saveToFile(filePath); - String jsonFilePath = getJsonFilePath(filePath); - ModelDataJsonConverter.convert(filePath, jsonFilePath, false); - } catch (Exception e) { - throw new RuntimeException("Failed to save model and convert to JSON: " + filePath, e); - } - } - - /** - * 从文件加载模型 - */ - public static Model2D loadFromFile(String filePath) { - try { - ModelData data = ModelData.loadFromFile(filePath); - return deserialize(data); - } catch (Exception e) { - throw new RuntimeException("Failed to load model from: " + filePath, e); - } - } - - /** - * 设置动画层列表(用于反序列化) - */ - public void setAnimationLayers(List animationLayers) { - this.animationLayers.clear(); - this.animationLayers.addAll(animationLayers); - } - - /** - * 保存模型到压缩文件 - */ - public void saveToCompressedFile(String filePath) { - try { - ModelData data = serialize(); - data.saveToCompressedFile(filePath); - String jsonFilePath = getJsonFilePath(filePath); - ModelDataJsonConverter.convert(filePath, jsonFilePath, true); - } catch (Exception e) { - throw new RuntimeException("Failed to save compressed model and convert to JSON: " + filePath, e); - } - } - - private String getJsonFilePath(String originalFilePath) { - int lastDotIndex = originalFilePath.lastIndexOf('.'); - if (lastDotIndex > 0) { - String baseName = originalFilePath.substring(0, lastDotIndex); - return baseName + ".json"; - } else { - return originalFilePath + ".json"; - } - } - - /** - * 从压缩文件加载模型 - */ - public static Model2D loadFromCompressedFile(String filePath) { - try { - ModelData data = ModelData.loadFromCompressedFile(filePath); - return deserialize(data); - } catch (Exception e) { - throw new RuntimeException("Failed to load compressed model from: " + filePath, e); - } - } - - // ==================== Getter/Setter ==================== - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public UUID getUuid() { - return uuid; - } - - public void setUuid(UUID uuid) { - this.uuid = uuid; - } - - public ModelMetadata getMetadata() { - return metadata; - } - - public void setMetadata(ModelMetadata metadata) { - this.metadata = metadata; - } - - public ModelPart getRootPart() { - return rootPart; - } - - public void setRootPart(ModelPart rootPart) { - this.rootPart = rootPart; - } - - - public List getMeshes() { - return Collections.unmodifiableList(meshes); - } - - public Map getParameters() { - return Collections.unmodifiableMap(parameters); - } - - public List getAnimationLayers() { - return Collections.unmodifiableList(animationLayers); - } - - public PhysicsSystem getPhysics() { - return physics; - } - - public ModelPose getCurrentPose() { - return new ModelPose(currentPose); - } - - public float getBlendProgress() { - return blendProgress; - } - - public boolean isBlending() { - return blendProgress < 1.0f; - } - - public Map getPoses() { - return Collections.unmodifiableMap(poses); - } - - public BoundingBox getBounds() { - return bounds; - } - - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelEvent.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelEvent.java deleted file mode 100644 index 77b769c..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelEvent.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.chuangzhou.vivid2D.render.model; - -/** - * 模型事件 - * - * @author tzdwindows 7 - */ -public interface ModelEvent { - void trigger(String eventName, Object eventBus); -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java deleted file mode 100644 index c0adb42..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ /dev/null @@ -1,2272 +0,0 @@ -package com.chuangzhou.vivid2D.render.model; - -import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal; -import com.chuangzhou.vivid2D.render.model.util.BoundingBox; -import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.util.Vertex; -import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; -import org.joml.Matrix3f; -import org.joml.Vector2f; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.*; -import javax.swing.tree.DefaultMutableTreeNode; - -/** - * 2D模型部件,支持层级变换和变形器 - * 使用 JOML 库进行数学计算 - * - * @author tzdwindows 7 - */ -public class ModelPart { - private static final Logger logger = LoggerFactory.getLogger(ModelPart.class); - // ==================== 基础属性 ==================== - private String name; - private ModelPart parent; - private final List children; - private final List meshes; - - // ==================== 变换属性 ==================== - private final Vector2f position; - private float rotation; - private final Vector2f scale; - private final Matrix3f localTransform; - private final Matrix3f worldTransform; - private final Vector2f pivot = new Vector2f(0, 0); - private float scaleX = 1.0f; - private float scaleY = 1.0f; - - // ==================== 渲染属性 ==================== - private boolean visible; - private BlendMode blendMode; - private float opacity; - - // ==================== 变形系统 ==================== - private final List deformers; - private final List liquifyStrokes = new ArrayList<>(); - - // ==================== 状态标记 ==================== - private boolean transformDirty; - private boolean boundsDirty; - private boolean pivotInitialized; - - private final List events = new java.util.concurrent.CopyOnWriteArrayList<>(); - private boolean inMultiSelectionOperation = false; - private boolean startLiquefy =false; - private final Map parameters = new LinkedHashMap<>(); - - // ====== 液化模式枚举 ====== - public enum LiquifyMode { - PUSH, // 推开(从画笔中心向外推) - PULL, // 拉近(向画笔中心吸) - SWIRL_CW, // 顺时针旋转 - SWIRL_CCW, // 逆时针旋转 - BLOAT, // 鼓起(放大) - PINCH, // 收缩(缩小) - SMOOTH, // 平滑(邻域平均) - TURBULENCE // 湍流(噪声扰动) - } - - - // ==================== 构造器 ==================== - - public ModelPart() { - this("unnamed"); - } - - public ModelPart(String name) { - this.name = name; - this.children = new ArrayList<>(); - this.meshes = new ArrayList<>(); - this.deformers = new ArrayList<>(); - - // 初始化变换属性 - this.position = new Vector2f(); - this.rotation = 0.0f; - this.scale = new Vector2f(1.0f, 1.0f); - this.localTransform = new Matrix3f(); - this.worldTransform = new Matrix3f(); - - // 初始化渲染属性 - this.visible = true; - this.blendMode = BlendMode.NORMAL; - this.opacity = 1.0f; - - // 标记需要更新 - this.transformDirty = true; - this.boundsDirty = true; - - updateLocalTransform(); - recomputeWorldTransformRecursive(); - } - - public void addEvent(ModelEvent event) { - events.add(event); - } - - public void removeEvent(ModelEvent event) { - events.remove(event); - } - - private void triggerEvent(String eventName) { - for (ModelEvent event : events) { - if (event != null) - event.trigger(eventName,this); - } - } - - /** - * 设置液化状态 - */ - public void setStartLiquefy(boolean startLiquefy) { - this.startLiquefy = startLiquefy; - - // 同步到所有网格 - for (Mesh2D mesh : meshes) { - mesh.setShowLiquifyOverlay(startLiquefy); - } - - triggerEvent("liquifyModeChanged"); - } - - // ==================== 多选支持 ==================== - - /** - * 标记多选状态需要更新 - */ - public void markMultiSelectionDirty() { - List selectedMeshes = getSelectedMeshes(); - for (Mesh2D mesh : selectedMeshes) { - mesh.markDirty(); - if (mesh.isInMultiSelection()) { - mesh.forceUpdateMultiSelectionBounds(); - } - } - } - - /** - * 更新所有选中网格的多选列表 - */ - private void updateMultiSelectionInMeshes() { - List selectedMeshes = getSelectedMeshes(); - if (selectedMeshes.size() <= 1) { - // 单选或没有选中,清除所有多选列表 - for (Mesh2D mesh : meshes) { - mesh.clearMultiSelection(); - } - return; - } - - // 多选状态,更新每个选中网格的多选列表 - for (Mesh2D selectedMesh : selectedMeshes) { - selectedMesh.clearMultiSelection(); - for (Mesh2D otherMesh : selectedMeshes) { - if (otherMesh != selectedMesh) { - selectedMesh.addToMultiSelection(otherMesh); - } - } - } - } - - /** - * 获取当前选中的所有网格(从所有网格中筛选出选中的) - */ - public List getSelectedMeshes() { - List selected = new ArrayList<>(); - for (Mesh2D mesh : meshes) { - if (mesh.isSelected()) { - selected.add(mesh); - } - } - return selected; - } - - /** - * 获取多选状态下的组合边界框 - */ - public BoundingBox getMultiSelectionBounds() { - BoundingBox bounds = new BoundingBox(); - List selectedMeshes = getSelectedMeshes(); - - if (selectedMeshes.isEmpty()) { - return bounds; - } - - // 使用第一个选中网格的多选边界框(如果处于多选状态) - Mesh2D firstSelected = selectedMeshes.get(0); - if (firstSelected.isInMultiSelection()) { - return firstSelected.getMultiSelectionBounds(); - } - - // 否则计算所有选中网格的组合边界框 - for (Mesh2D mesh : selectedMeshes) { - BoundingBox meshBounds = mesh.getBounds(); - if (meshBounds.isValid()) { - bounds.expand(meshBounds); - } - } - - return bounds; - } - - /** - * 检查是否处于多选状态 - */ - public boolean isInMultiSelection() { - List selectedMeshes = getSelectedMeshes(); - if (selectedMeshes.isEmpty()) { - return false; - } - - // 如果任意选中的网格处于多选状态,则认为整个部件处于多选状态 - for (Mesh2D mesh : selectedMeshes) { - if (mesh.isInMultiSelection()) { - return true; - } - } - - return selectedMeshes.size() > 1; - } - - /** - * 获取多选状态下的中心点 - */ - public Vector2f getMultiSelectionCenter() { - BoundingBox bounds = getMultiSelectionBounds(); - return bounds.getCenter(); - } - - /** - * 移动所有选中的网格(整体移动) - */ - public void moveSelectedMeshes(float dx, float dy) { - List selectedMeshes = getSelectedMeshes(); - if (selectedMeshes.isEmpty()) return; - - // 如果是多选状态,整体移动 - if (isInMultiSelection()) { - // 整体移动:所有选中网格使用相同的位移 - for (Mesh2D mesh : selectedMeshes) { - ModelPart meshPart = findPartByMesh(mesh); - if (meshPart != null) { - Vector2f pos = meshPart.getPosition(); - meshPart.setPosition(pos.x + dx, pos.y + dy); - } - } - } else { - // 单选状态,只移动选中的网格 - for (Mesh2D mesh : selectedMeshes) { - ModelPart meshPart = findPartByMesh(mesh); - if (meshPart != null) { - Vector2f pos = meshPart.getPosition(); - meshPart.setPosition(pos.x + dx, pos.y + dy); - } - } - } - - triggerEvent("multiSelectionMove"); - } - - /** - * 旋转所有选中的网格(整体旋转) - */ - public void rotateSelectedMeshes(float deltaAngle) { - List selectedMeshes = getSelectedMeshes(); - if (selectedMeshes.isEmpty()) return; - - // 如果是多选状态,整体旋转 - if (isInMultiSelection()) { - Vector2f center = getMultiSelectionCenter(); - - for (Mesh2D mesh : selectedMeshes) { - ModelPart meshPart = findPartByMesh(mesh); - if (meshPart != null) { - // 计算相对于中心点的旋转 - Vector2f meshPos = meshPart.getPosition(); - Vector2f relativePos = new Vector2f(meshPos.x - center.x, meshPos.y - center.y); - - // 应用旋转 - float cos = (float) Math.cos(deltaAngle); - float sin = (float) Math.sin(deltaAngle); - float newX = center.x + relativePos.x * cos - relativePos.y * sin; - float newY = center.y + relativePos.x * sin + relativePos.y * cos; - - meshPart.setPosition(newX, newY); - meshPart.rotate(deltaAngle); - } - } - } else { - // 单选状态,各自绕自己的中心旋转 - for (Mesh2D mesh : selectedMeshes) { - ModelPart meshPart = findPartByMesh(mesh); - if (meshPart != null) { - meshPart.rotate(deltaAngle); - } - } - } - - triggerEvent("multiSelectionRotate"); - } - - /** - * 缩放所有选中的网格(整体缩放) - */ - public void scaleSelectedMeshes(float scaleX, float scaleY) { - List selectedMeshes = getSelectedMeshes(); - if (selectedMeshes.isEmpty()) return; - - // 如果是多选状态,整体缩放 - if (isInMultiSelection()) { - Vector2f center = getMultiSelectionCenter(); - - for (Mesh2D mesh : selectedMeshes) { - ModelPart meshPart = findPartByMesh(mesh); - if (meshPart != null) { - // 计算相对于中心点的缩放 - Vector2f meshPos = meshPart.getPosition(); - Vector2f relativePos = new Vector2f(meshPos.x - center.x, meshPos.y - center.y); - - // 应用缩放 - float newX = center.x + relativePos.x * scaleX; - float newY = center.y + relativePos.y * scaleY; - - meshPart.setPosition(newX, newY); - meshPart.scale(scaleX, scaleY); - } - } - } else { - // 单选状态,各自绕自己的中心缩放 - for (Mesh2D mesh : selectedMeshes) { - ModelPart meshPart = findPartByMesh(mesh); - if (meshPart != null) { - meshPart.scale(scaleX, scaleY); - } - } - } - - triggerEvent("multiSelectionScale"); - } - - /** - * 通过网格查找对应的 ModelPart(递归查找) - */ - private ModelPart findPartByMesh(Mesh2D targetMesh) { - // 先检查当前部件的网格 - for (Mesh2D mesh : meshes) { - if (mesh == targetMesh) { - return this; - } - } - - // 递归检查子部件 - for (ModelPart child : children) { - ModelPart found = child.findPartByMeshRecursive(targetMesh); - if (found != null) { - return found; - } - } - - return null; - } - - /** - * 递归查找包含指定网格的部件 - */ - private ModelPart findPartByMeshRecursive(Mesh2D targetMesh) { - // 检查当前部件的网格 - for (Mesh2D mesh : meshes) { - if (mesh == targetMesh) { - return this; - } - } - - // 递归检查子部件 - for (ModelPart child : children) { - ModelPart found = child.findPartByMeshRecursive(targetMesh); - if (found != null) { - return found; - } - } - - return null; - } - - // ==================== 层级管理 ==================== - - /** - * 添加子部件 - */ - public void addChild(ModelPart child) { - if (child == this) { - throw new IllegalArgumentException("Cannot add self as child"); - } - if (child.parent != null) { - child.parent.removeChild(child); - } - children.add(child); - child.parent = this; - child.markTransformDirty(); - // 确保子节点的 worldTransform 立即更新 - child.recomputeWorldTransformRecursive(); - } - - /** - * 移除子部件 - */ - public boolean removeChild(ModelPart child) { - boolean removed = children.remove(child); - if (removed) { - child.parent = null; - child.markTransformDirty(); - child.recomputeWorldTransformRecursive(); - } - return removed; - } - - /** - * 获取所有子部件 - */ - public List getChildren() { - return new ArrayList<>(children); - } - - public DefaultMutableTreeNode toTreeNode() { - DefaultMutableTreeNode node = new DefaultMutableTreeNode(this.name != null ? this.name : "部件"); - for (ModelPart child : children) { - node.add(child.toTreeNode()); - } - return node; - } - - /** - * 根据名称查找子部件 - */ - public ModelPart findChild(String name) { - for (ModelPart child : children) { - if (name.equals(child.getName())) { - return child; - } - } - return null; - } - - /** - * 递归查找子部件 - */ - public ModelPart findChildRecursive(String name) { - // 先检查直接子节点 - ModelPart result = findChild(name); - if (result != null) { - return result; - } - - // 递归检查子节点的子节点 - for (ModelPart child : children) { - result = child.findChildRecursive(name); - if (result != null) { - return result; - } - } - - return null; - } - - // ==================== 变换系统 ==================== - - /** - * 更新世界变换(保留旧方法以兼容) - */ - public void updateWorldTransform(Matrix3f parentTransform, boolean recursive) { - // 如果需要更新局部变换 - if (transformDirty) { - updateLocalTransform(); - } - - // 计算世界变换:parent * local - parentTransform.mul(localTransform, worldTransform); - - // 递归更新子部件 - if (recursive) { - for (ModelPart child : children) { - child.updateWorldTransform(worldTransform, true); - } - } - - // 标记边界需要更新 - boundsDirty = true; - transformDirty = false; - } - - /** - * 获取当前部件记录的所有液化笔划(用于序列化 / 导出) - */ - public List getLiquifyStrokes() { - return liquifyStrokes; - } - - /** - * 添加并记录一个完整的液化笔划(不会自动在调用时 apply,除非你在调用后显式重放) - */ - public void addLiquifyStroke(LiquifyStroke stroke) { - if (stroke != null) liquifyStrokes.add(stroke); - } - - /** - * 清除已记录的所有液化笔划 - */ - public void clearLiquifyStrokes() { - liquifyStrokes.clear(); - } - - /** - * 重放并应用某个记录的液化笔划(将对每个点调用 applyLiquify) - * 兼容 PartData 在反序列化时逐点调用 applyLiquify 的方式。 - */ - public void replayLiquifyStroke(LiquifyStroke stroke) { - if (stroke == null || stroke.points == null) return; - LiquifyMode mode = stroke.mode != null ? stroke.mode : LiquifyMode.PUSH; - for (LiquifyPoint p : stroke.points) { - applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, mode, stroke.iterations,true); - } - } - - /** - * 对当前部件下所有网格应用液化笔效果(类似 Photoshop 的液化工具)。 - * 请在使用前注册在使用addLiquifyStroke方法注册LiquifyStroke - * - * 注意: - * - brushCenter 使用世界坐标(与 ModelPart 的世界坐标体系一致)。 - * - radius 为画笔半径(像素),strength 为强度(建议范围 0.0 - 1.0,数值越大效果越强)。 - * - mode 选择液化操作类型。 - * - iterations 为迭代次数(>0),可用来让效果更平滑,默认 1 次即可。 - * - * 该方法会直接修改 mesh 的顶点并更新其边界(mesh.updateBounds)。 - */ - public void applyLiquify(Vector2f brushCenter, float radius, float strength, LiquifyMode mode, int iterations, boolean createVertices) { - if (radius <= 0f || strength == 0f || iterations <= 0) return; - - logger.debug("应用液化效果: 中心({}, {}), 半径{}, 强度{}, 模式{}, 迭代{}, 创建顶点{}", - brushCenter.x, brushCenter.y, radius, strength, mode, iterations, createVertices); - - // 限制 strength 到合理范围 - float s = Math.max(-2f, Math.min(2f, strength)); - - // 随机用于 Turbulence - java.util.Random rand = new java.util.Random(); - - // 记录液化前的网格状态用于操作历史 - Map beforeStates = new HashMap<>(); - for (Mesh2D mesh : meshes) { - if (mesh.getVertexCount() > 0) { - beforeStates.put(mesh, createMeshState(mesh)); - } - } - - // 只在真正需要时添加顶点,避免破坏网格结构 - //if (createVertices) { - // addMinimalVerticesIfNeeded(brushCenter, radius); - //} - - // 迭代多个小步以获得平滑结果 - for (int iter = 0; iter < iterations; iter++) { - // 对每个网格执行液化 - for (Mesh2D mesh : meshes) { - int vc = mesh.getVertexCount(); - if (vc <= 0) continue; - - // 获取原始顶点数据 - float[] originalVertices = mesh.getOriginalVertices(); - if (originalVertices == null || originalVertices.length == 0) { - logger.warn("网格 {} 没有原始顶点数据", mesh.getName()); - continue; - } - - // 创建顶点副本用于处理 - float[] processedVertices = originalVertices.clone(); - - // 找到画笔区域内需要影响的顶点 - List influencedVertices = findVerticesToInfluence(mesh, brushCenter, radius); - - // 对每个需要影响的顶点计算影响 - for (VertexInfluence vi : influencedVertices) { - int i = vi.vertexIndex; - Vector2f vertexPos = new Vector2f(processedVertices[i * 2], processedVertices[i * 2 + 1]); - - // 将局部坐标转换为世界坐标 - Vector2f worldPos = Matrix3fUtils.transformPoint(worldTransform, vertexPos); - - float dx = worldPos.x - brushCenter.x; - float dy = worldPos.y - brushCenter.y; - float dist = (float) Math.sqrt(dx * dx + dy * dy); - - // 使用软边界 - float softRadius = radius * 1.3f; - if (dist > softRadius) continue; - - // 改进的falloff计算,支持软边界 - float t; - if (dist <= radius) { - t = 1.0f - (dist / radius); - } else { - t = 1.0f - ((dist - radius) / (softRadius - radius)); - t = Math.max(0.01f, t); - } - float falloff = t * t * (3f - 2f * t); - - // 基本影响量 - float influence = falloff * s * 10.0f; - - // 目标点(局部坐标) - Vector2f newLocalPos = new Vector2f(vertexPos); - - switch (mode) { - case PUSH -> { - if (dist < 1e-6f) { - float ang = rand.nextFloat() * (float) (2.0 * Math.PI); - newLocalPos.x += (float) Math.cos(ang) * influence * 0.1f; - newLocalPos.y += (float) Math.sin(ang) * influence * 0.1f; - } else { - float dirX = dx / dist; - float dirY = dy / dist; - Vector2f worldDisplacement = new Vector2f(dirX * influence * 0.1f, dirY * influence * 0.1f); - Vector2f localDisplacement = Matrix3fUtils.transformVectorInverse(worldTransform, worldDisplacement); - newLocalPos.add(localDisplacement); - } - } - case PULL -> { - if (dist < 1e-6f) { - float ang = rand.nextFloat() * (float) (2.0 * Math.PI); - newLocalPos.x -= (float) Math.cos(ang) * influence * 0.1f; - newLocalPos.y -= (float) Math.sin(ang) * influence * 0.1f; - } else { - float dirX = -dx / dist; - float dirY = -dy / dist; - Vector2f worldDisplacement = new Vector2f(dirX * influence * 0.1f, dirY * influence * 0.1f); - Vector2f localDisplacement = Matrix3fUtils.transformVectorInverse(worldTransform, worldDisplacement); - newLocalPos.add(localDisplacement); - } - } - case SWIRL_CW, SWIRL_CCW -> { - float dir = (mode == LiquifyMode.SWIRL_CW) ? -1f : 1f; - float maxAngle = 1.0f * s * falloff; - float angle = dir * maxAngle; - Vector2f rotatedWorldPos = rotateAround(worldPos, brushCenter, angle); - Vector2f newPos = Matrix3fUtils.transformPointInverse(worldTransform, rotatedWorldPos); - newLocalPos.set(newPos); - } - case BLOAT -> { - if (dist < 1e-6f) { - float ang = rand.nextFloat() * (float) (2.0 * Math.PI); - newLocalPos.x += (float) Math.cos(ang) * Math.abs(influence) * 0.05f; - newLocalPos.y += (float) Math.sin(ang) * Math.abs(influence) * 0.05f; - } else { - float factor = 1.0f + Math.abs(influence) * 0.1f; - Vector2f scaledWorldPos = new Vector2f( - brushCenter.x + (worldPos.x - brushCenter.x) * factor, - brushCenter.y + (worldPos.y - brushCenter.y) * factor - ); - Vector2f newPos = Matrix3fUtils.transformPointInverse(worldTransform, scaledWorldPos); - newLocalPos.set(newPos); - } - } - case PINCH -> { - if (dist < 1e-6f) { - float ang = rand.nextFloat() * (float) (2.0 * Math.PI); - newLocalPos.x -= (float) Math.cos(ang) * Math.abs(influence) * 0.05f; - newLocalPos.y -= (float) Math.sin(ang) * Math.abs(influence) * 0.05f; - } else { - float factor = 1.0f - Math.abs(influence) * 0.1f; - factor = Math.max(0.1f, factor); - Vector2f scaledWorldPos = new Vector2f( - brushCenter.x + (worldPos.x - brushCenter.x) * factor, - brushCenter.y + (worldPos.y - brushCenter.y) * factor - ); - Vector2f newPos = Matrix3fUtils.transformPointInverse(worldTransform, scaledWorldPos); - newLocalPos.set(newPos); - } - } - case SMOOTH -> { - Vector2f avg = new Vector2f(0f, 0f); - int count = 0; - float neighborRadius = Math.max(5.0f, radius * 0.3f); - - for (int j = 0; j < vc; j++) { - if (j == i) continue; - Vector2f otherVertex = new Vector2f(processedVertices[j * 2], processedVertices[j * 2 + 1]); - Vector2f otherWorldPos = Matrix3fUtils.transformPoint(worldTransform, otherVertex); - if (worldPos.distance(otherWorldPos) <= neighborRadius) { - avg.add(otherVertex); - count++; - } - } - - if (count > 0) { - avg.mul(1.0f / count); - float blendFactor = Math.abs(influence) * 0.3f * falloff; - newLocalPos.x = newLocalPos.x + (avg.x - newLocalPos.x) * blendFactor; - newLocalPos.y = newLocalPos.y + (avg.y - newLocalPos.y) * blendFactor; - } - } - case TURBULENCE -> { - float jitter = (rand.nextFloat() * 2f - 1f) * Math.abs(influence) * 0.5f; - float jitter2 = (rand.nextFloat() * 2f - 1f) * Math.abs(influence) * 0.5f; - newLocalPos.x += jitter * falloff; - newLocalPos.y += jitter2 * falloff; - } - default -> { /* no-op */ } - } - - // 更新处理后的顶点 - processedVertices[i * 2] = newLocalPos.x; - processedVertices[i * 2 + 1] = newLocalPos.y; - } - - // 更新网格的原始顶点数据 - mesh.setOriginalVertices(processedVertices); - updateMeshVertices(mesh); - - // 更新网格边界 - try { - mesh.updateBounds(); - logger.debug("网格 {} 边界已更新", mesh.getName()); - } catch (Exception e) { - logger.warn("更新网格边界时出错: {}", e.getMessage()); - } - } - - // 标记需要重新计算边界 - this.boundsDirty = true; - } - - // 记录液化操作到历史记录 - if (!beforeStates.isEmpty()) { - recordLiquifyOperation(beforeStates, brushCenter, radius, strength, mode, createVertices); - } - - logger.debug("液化效果应用完成,影响 {} 个网格", meshes.size()); - } - - /** - * 检查画笔区域内是否有足够的顶点 - */ - private boolean hasSufficientVerticesInArea(Mesh2D mesh, Vector2f brushCenter, float radius) { - int verticesInArea = 0; - float[] vertices = mesh.getOriginalVertices(); - - for (int i = 0; i < vertices.length / 2; i++) { - Vector2f vertexPos = new Vector2f(vertices[i * 2], vertices[i * 2 + 1]); - Vector2f worldPos = Matrix3fUtils.transformPoint(worldTransform, vertexPos); - float dist = worldPos.distance(brushCenter); - - if (dist <= radius) { - verticesInArea++; - if (verticesInArea >= 3) { // 至少有3个顶点在区域内 - return true; - } - } - } - - return false; - } - - /** - * 在画笔中心添加单个顶点 - */ - private void addSingleVertexAtBrushCenter(Mesh2D mesh, Vector2f brushCenter) { - float[] originalVertices = mesh.getOriginalVertices(); - float[] originalUVs = mesh.getUVs(); - int[] originalIndices = mesh.getIndices(); - - if (originalVertices == null || originalUVs == null || originalIndices == null) { - return; - } - - // 转换画笔中心到局部坐标 - Vector2f localBrushCenter = Matrix3fUtils.transformPointInverse(worldTransform, brushCenter); - - // 检查是否已经有顶点在附近 - if (hasVertexNearby(originalVertices, localBrushCenter, 5.0f)) { - return; // 已经有顶点在附近,不需要添加 - } - - // 创建新的顶点数组 - int newVertexCount = originalVertices.length / 2 + 1; - float[] newVertices = new float[newVertexCount * 2]; - float[] newUVs = new float[newVertexCount * 2]; - - // 复制原有数据 - System.arraycopy(originalVertices, 0, newVertices, 0, originalVertices.length); - System.arraycopy(originalUVs, 0, newUVs, 0, originalUVs.length); - - // 添加新顶点 - newVertices[originalVertices.length] = localBrushCenter.x; - newVertices[originalVertices.length + 1] = localBrushCenter.y; - - // 估算新顶点的UV坐标 - Vector2f newUV = estimateUVForPosition(localBrushCenter.x, localBrushCenter.y, mesh); - newUVs[originalUVs.length] = newUV.x; - newUVs[originalUVs.length + 1] = newUV.y; - - // 创建新的索引数组 - 保持原有索引不变 - // 注意:这里不自动连接新顶点,避免破坏网格结构 - // 新顶点将在后续的液化操作中被移动,但不会影响整体网格拓扑 - int[] newIndices = originalIndices.clone(); - - // 设置新的网格数据 - mesh.setMeshData(newVertices, newUVs, newIndices); - logger.debug("在画笔中心添加单个顶点: ({}, {})", localBrushCenter.x, localBrushCenter.y); - } - - /** - * 检查指定位置附近是否已有顶点 - */ - private boolean hasVertexNearby(float[] vertices, Vector2f position, float threshold) { - for (int i = 0; i < vertices.length / 2; i++) { - float dx = vertices[i * 2] - position.x; - float dy = vertices[i * 2 + 1] - position.y; - if (dx * dx + dy * dy < threshold * threshold) { - return true; - } - } - return false; - } - - /** - * 为指定位置估算UV坐标 - */ - private Vector2f estimateUVForPosition(float x, float y, Mesh2D mesh) { - BoundingBox bounds = mesh.getBounds(); - if (bounds != null && bounds.isValid()) { - float u = (x - bounds.getMinX()) / bounds.getWidth(); - float v = (y - bounds.getMinY()) / bounds.getHeight(); - // 限制在[0,1]范围内 - u = Math.max(0, Math.min(1, u)); - v = Math.max(0, Math.min(1, v)); - return new Vector2f(u, v); - } - return new Vector2f(0.5f, 0.5f); - } - - /** - * 在画笔区域内全面添加顶点,确保整个区域都可以被液化 - */ - private void addComprehensiveVerticesToBrushArea(Vector2f brushCenter, float radius) { - for (Mesh2D mesh : meshes) { - if (mesh.getVertexCount() <= 0) continue; - - float[] originalVertices = mesh.getOriginalVertices(); - float[] originalUVs = mesh.getUVs(); - int[] originalIndices = mesh.getIndices(); - - // 检查数组长度是否有效 - if (originalVertices == null || originalUVs == null || originalIndices == null) { - continue; - } - - // 确保顶点和UV数组长度匹配 - if (originalVertices.length / 2 != originalUVs.length / 2) { - logger.warn("网格 {} 的顶点和UV数量不匹配: 顶点={}, UV={}", - mesh.getName(), originalVertices.length / 2, originalUVs.length / 2); - continue; - } - - List newVertices = new ArrayList<>(); - List newUVs = new ArrayList<>(); - List newIndices = new ArrayList<>(); - - // 将现有数据添加到列表 - int vertexCount = originalVertices.length / 2; - for (int i = 0; i < vertexCount; i++) { - if (i * 2 + 1 < originalVertices.length) { - newVertices.add(new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1])); - } - if (i * 2 + 1 < originalUVs.length) { - newUVs.add(new Vector2f(originalUVs[i * 2], originalUVs[i * 2 + 1])); - } - } - - // 复制索引数组 - for (int index : originalIndices) { - if (index < vertexCount) { - newIndices.add(index); - } - } - - // 转换画笔中心到局部坐标 - Vector2f localBrushCenter = Matrix3fUtils.transformPointInverse(worldTransform, brushCenter); - - // 获取网格边界 - BoundingBox meshBounds = mesh.getBounds(); - if (meshBounds == null || !meshBounds.isValid()) { - logger.warn("网格 {} 的边界无效,无法添加顶点", mesh.getName()); - continue; - } - - // 计算画笔区域在局部坐标系中的边界 - float localRadius = radius / Math.max(scale.x, scale.y); // 粗略估算局部半径 - float brushMinX = localBrushCenter.x - localRadius; - float brushMaxX = localBrushCenter.x + localRadius; - float brushMinY = localBrushCenter.y - localRadius; - float brushMaxY = localBrushCenter.y + localRadius; - - // 与网格边界求交 - float effectiveMinX = Math.max(brushMinX, meshBounds.getMinX()); - float effectiveMaxX = Math.min(brushMaxX, meshBounds.getMaxX()); - float effectiveMinY = Math.max(brushMinY, meshBounds.getMinY()); - float effectiveMaxY = Math.min(brushMaxY, meshBounds.getMaxY()); - - // 如果画笔区域与网格没有交集,跳过 - if (effectiveMinX >= effectiveMaxX || effectiveMinY >= effectiveMaxY) { - logger.debug("画笔区域与网格 {} 无交集,跳过添加顶点", mesh.getName()); - continue; - } - - // 计算合理的顶点密度 - float brushAreaWidth = effectiveMaxX - effectiveMinX; - float brushAreaHeight = effectiveMaxY - effectiveMinY; - float brushArea = brushAreaWidth * brushAreaHeight; - - // 根据画笔大小和网格复杂度计算顶点密度 - int targetVertexCount = Math.max(4, Math.min(20, (int)(brushArea / (localRadius * localRadius * 0.5f)))); - - // 在画笔区域内均匀添加顶点 - boolean addedVertices = addUniformVerticesInArea(mesh, newVertices, newUVs, newIndices, - effectiveMinX, effectiveMinY, effectiveMaxX, effectiveMaxY, targetVertexCount, localBrushCenter); - - // 如果添加了顶点,更新网格数据 - if (addedVertices) { - updateMeshWithNewVertices(mesh, newVertices, newUVs, newIndices); - logger.info("网格 {} 在画笔区域添加顶点完成,总顶点数: {}", mesh.getName(), newVertices.size()); - } - } - } - - /** - * 在指定区域内均匀添加顶点 - */ - private boolean addUniformVerticesInArea(Mesh2D mesh, List vertices, List uvs, - List indices, float minX, float minY, float maxX, float maxY, - int targetCount, Vector2f brushCenter) { - boolean addedAny = false; - - // 计算网格步长 - float width = maxX - minX; - float height = maxY - minY; - int gridX = (int) Math.sqrt(targetCount * (width / height)); - int gridY = (int) Math.sqrt(targetCount * (height / width)); - - gridX = Math.max(2, gridX); - gridY = Math.max(2, gridY); - - float stepX = width / (gridX - 1); - float stepY = height / (gridY - 1); - - // 在网格交点处添加顶点 - for (int i = 0; i < gridX; i++) { - for (int j = 0; j < gridY; j++) { - float x = minX + i * stepX; - float y = minY + j * stepY; - - // 检查该位置是否已经有顶点(在一定阈值内) - if (!hasVertexNearby(vertices, x, y, Math.min(stepX, stepY) * 0.3f)) { - int newIndex = vertices.size(); - vertices.add(new Vector2f(x, y)); - - // 估算UV坐标 - Vector2f newUV = estimateUVForPosition(x, y, mesh); - uvs.add(newUV); - - // 连接到最近的三角形 - connectNewVertexToMesh(newIndex, vertices, indices); - addedAny = true; - - logger.debug("在位置 ({}, {}) 添加顶点", x, y); - } - } - } - - return addedAny; - } - - /** - * 检查指定位置附近是否已有顶点 - */ - private boolean hasVertexNearby(List vertices, float x, float y, float threshold) { - for (Vector2f vertex : vertices) { - float dx = vertex.x - x; - float dy = vertex.y - y; - if (dx * dx + dy * dy < threshold * threshold) { - return true; - } - } - return false; - } - - /** - * 将新顶点连接到网格 - */ - private void connectNewVertexToMesh(int vertexIndex, List vertices, List indices) { - if (vertices.size() <= 1 || indices.isEmpty()) return; - - // 找到距离最近的三角形 - Vector2f newVertex = vertices.get(vertexIndex); - int closestTriangle = findClosestTriangle(newVertex, vertices, indices); - - if (closestTriangle != -1) { - // 将三角形细分为三个小三角形 - subdivideTriangle(closestTriangle, vertexIndex, indices); - } else { - // 如果没有找到合适的三角形,尝试连接到边界 - connectToMeshBoundary(vertexIndex, vertices, indices); - } - } - - /** - * 将三角形细分为三个小三角形 - */ - private void subdivideTriangle(int triangleStart, int newVertexIndex, List indices) { - int i1 = indices.get(triangleStart); - int i2 = indices.get(triangleStart + 1); - int i3 = indices.get(triangleStart + 2); - - // 修改原三角形为第一个小三角形 - indices.set(triangleStart, newVertexIndex); - indices.set(triangleStart + 1, i1); - indices.set(triangleStart + 2, i2); - - // 添加另外两个小三角形 - indices.add(newVertexIndex); - indices.add(i2); - indices.add(i3); - - indices.add(newVertexIndex); - indices.add(i3); - indices.add(i1); - } - - /** - * 将顶点连接到网格边界(备用方法) - */ - private void connectToMeshBoundary(int vertexIndex, List vertices, List indices) { - // 简单的边界连接策略:找到两个最近的顶点,形成一个三角形 - if (vertices.size() < 3) return; - - Vector2f newVertex = vertices.get(vertexIndex); - - // 找到两个最近的顶点 - int closest1 = -1, closest2 = -1; - float minDist1 = Float.MAX_VALUE, minDist2 = Float.MAX_VALUE; - - for (int i = 0; i < vertices.size() - 1; i++) { // 排除新顶点自己 - if (i == vertexIndex) continue; - - float dist = vertices.get(i).distance(newVertex); - if (dist < minDist1) { - minDist2 = minDist1; - closest2 = closest1; - minDist1 = dist; - closest1 = i; - } else if (dist < minDist2) { - minDist2 = dist; - closest2 = i; - } - } - - if (closest1 != -1 && closest2 != -1) { - // 添加新的三角形 - indices.add(vertexIndex); - indices.add(closest1); - indices.add(closest2); - } - } - - /** - * 使用新的顶点数据更新网格 - */ - private void updateMeshWithNewVertices(Mesh2D mesh, List vertices, List uvs, List indices) { - // 创建新的数组 - float[] finalVertices = new float[vertices.size() * 2]; - float[] finalUVs = new float[uvs.size() * 2]; - int[] finalIndices = new int[indices.size()]; - - // 填充顶点数据 - for (int i = 0; i < vertices.size(); i++) { - finalVertices[i * 2] = vertices.get(i).x; - finalVertices[i * 2 + 1] = vertices.get(i).y; - } - - // 填充UV数据 - for (int i = 0; i < uvs.size(); i++) { - finalUVs[i * 2] = uvs.get(i).x; - finalUVs[i * 2 + 1] = uvs.get(i).y; - } - - // 填充索引数据 - for (int i = 0; i < indices.size(); i++) { - finalIndices[i] = indices.get(i); - } - - // 设置新的网格数据 - mesh.setMeshData(finalVertices, finalUVs, finalIndices); - } - - /** - * 创建网格状态快照 - */ - private OperationHistoryGlobal.MeshState createMeshState(Mesh2D mesh) { - return new OperationHistoryGlobal.MeshState( - mesh.getName(), - mesh.getVertices(), - mesh.getOriginalVertices(), - mesh.getOriginalPivot(), - mesh.getTexture() - ); - } - - /** - * 记录液化操作到历史记录 - */ - private void recordLiquifyOperation(Map beforeStates, - Vector2f brushCenter, float radius, float strength, - LiquifyMode mode, boolean createVertices) { - try { - // 创建液化后的状态 - Map afterStates = new HashMap<>(); - for (Mesh2D mesh : meshes) { - if (mesh.getVertexCount() > 0) { - afterStates.put(mesh, createMeshState(mesh)); - } - } - - // 记录操作 - 确保参数顺序正确 - // params[0] = beforeStates (撤回时使用) - // params[1] = afterStates (重做时使用) - Object[] params = new Object[]{ - beforeStates, // 索引0: 液化前状态(撤回时恢复这个) - afterStates, // 索引1: 液化后状态(重做时恢复这个) - brushCenter, // 索引2: 画笔中心 - radius, // 索引3: 画笔半径 - strength, // 索引4: 画笔强度 - mode, // 索引5: 液化模式 - createVertices // 索引6: 是否创建顶点 - }; - - OperationHistoryGlobal.getInstance().recordOperation("LIQUIFY", params); - logger.debug("已记录液化操作到历史记录,影响 {} 个网格", beforeStates.size()); - - } catch (Exception e) { - logger.error("记录液化操作时出错: {}", e.getMessage(), e); - } - } - - /** - * 应用液化操作历史记录 - */ - public void applyLiquifyFromHistory(OperationHistoryGlobal.MeshState beforeState, OperationHistoryGlobal.MeshState afterState) { - if (beforeState == null || afterState == null) return; - - // 查找对应的网格 - for (Mesh2D mesh : meshes) { - if (mesh.getName().equals(beforeState.name)) { - // 恢复网格状态 - if (afterState.vertices != null) { - mesh.setOriginalVertices(afterState.vertices); - updateMeshVertices(mesh); - } - if (afterState.originalVertices != null) { - mesh.setOriginalVertices(afterState.originalVertices); - } - if (afterState.originalPivot != null) { - mesh.setOriginalPivot(afterState.originalPivot); - } - // 注意:纹理恢复需要根据具体实现调整 - - // 更新边界 - try { - mesh.updateBounds(); - } catch (Exception e) { - logger.warn("更新网格边界时出错: {}", e.getMessage()); - } - break; - } - } - } - - /** - * 撤回液化操作 - */ - public void undoLiquifyFromHistory(OperationHistoryGlobal.MeshState beforeState, OperationHistoryGlobal.MeshState afterState) { - if (beforeState == null) return; - - // 查找对应的网格 - for (Mesh2D mesh : meshes) { - if (mesh.getName().equals(beforeState.name)) { - // 恢复到液化前的状态 - if (beforeState.vertices != null) { - mesh.setOriginalVertices(beforeState.vertices); - updateMeshVertices(mesh); - } - if (beforeState.originalVertices != null) { - mesh.setOriginalVertices(beforeState.originalVertices); - } - if (beforeState.originalPivot != null) { - mesh.setOriginalPivot(beforeState.originalPivot); - } - - // 更新边界 - try { - mesh.updateBounds(); - } catch (Exception e) { - logger.warn("更新网格边界时出错: {}", e.getMessage()); - } - break; - } - } - } - - /** - * 顶点影响信息 - */ - private static class VertexInfluence { - int vertexIndex; - float distance; - - VertexInfluence(int vertexIndex, float distance) { - this.vertexIndex = vertexIndex; - this.distance = distance; - } - } - - /** - * 找到需要影响的顶点(只找最近的几个) - */ - private List findVerticesToInfluence(Mesh2D mesh, Vector2f brushCenter, float radius) { - List influencedVertices = new ArrayList<>(); - float[] vertices = mesh.getOriginalVertices(); - - // 收集画笔区域内的所有顶点 - for (int i = 0; i < vertices.length / 2; i++) { - Vector2f vertexPos = new Vector2f(vertices[i * 2], vertices[i * 2 + 1]); - Vector2f worldPos = Matrix3fUtils.transformPoint(worldTransform, vertexPos); - float dist = worldPos.distance(brushCenter); - - if (dist <= radius * 1.3f) { // 软边界 - influencedVertices.add(new VertexInfluence(i, dist)); - } - } - - // 按距离排序,只保留最近的几个顶点 - influencedVertices.sort((v1, v2) -> Float.compare(v1.distance, v2.distance)); - - // 限制影响顶点的数量,避免过度变形 - int maxVerticesToInfluence = Math.min(8, influencedVertices.size()); - return influencedVertices.subList(0, maxVerticesToInfluence); - } - - /** - * 在画笔区域内稀疏地添加顶点 - */ - private void addSparseVerticesToBrushArea(Vector2f brushCenter, float radius) { - for (Mesh2D mesh : meshes) { - if (mesh.getVertexCount() <= 0) continue; - - float[] originalVertices = mesh.getOriginalVertices(); - float[] originalUVs = mesh.getUVs(); - int[] originalIndices = mesh.getIndices(); - - // 检查数组长度是否有效 - if (originalVertices == null || originalUVs == null || originalIndices == null) { - continue; - } - - // 确保顶点和UV数组长度匹配 - if (originalVertices.length / 2 != originalUVs.length / 2) { - logger.warn("网格 {} 的顶点和UV数量不匹配: 顶点={}, UV={}", - mesh.getName(), originalVertices.length / 2, originalUVs.length / 2); - continue; - } - - List newVertices = new ArrayList<>(); - List newUVs = new ArrayList<>(); - List newIndices = new ArrayList<>(); - - // 将现有数据添加到列表 - 添加边界检查 - int vertexCount = originalVertices.length / 2; - for (int i = 0; i < vertexCount; i++) { - if (i * 2 + 1 < originalVertices.length) { - newVertices.add(new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1])); - } - if (i * 2 + 1 < originalUVs.length) { - newUVs.add(new Vector2f(originalUVs[i * 2], originalUVs[i * 2 + 1])); - } - } - - // 复制索引数组 - for (int index : originalIndices) { - if (index < vertexCount) { // 确保索引有效 - newIndices.add(index); - } - } - - boolean addedVertices = false; - - // 检查画笔中心区域是否缺少顶点 - Vector2f localBrushCenter = Matrix3fUtils.transformPointInverse(worldTransform, brushCenter); - - // 找到距离画笔中心最近的顶点 - float minDistance = Float.MAX_VALUE; - int closestVertexIndex = -1; - - for (int i = 0; i < newVertices.size(); i++) { - Vector2f vertex = newVertices.get(i); - Vector2f worldVertex = Matrix3fUtils.transformPoint(worldTransform, vertex); - float dist = worldVertex.distance(brushCenter); - - if (dist < minDistance) { - minDistance = dist; - closestVertexIndex = i; - } - } - - // 如果最近的顶点距离超过阈值,才添加新顶点 - float addVertexThreshold = radius * 0.6f; // 只有距离超过这个阈值才添加顶点 - if (minDistance > addVertexThreshold && closestVertexIndex != -1) { - // 添加一个顶点在画笔中心 - int centerIndex = newVertices.size(); - newVertices.add(new Vector2f(localBrushCenter)); - - // 估算UV坐标 - Vector2f centerUV = estimateUVForNewVertex(localBrushCenter.x, localBrushCenter.y, mesh); - newUVs.add(centerUV); - - // 找到最近的三角形来连接这个新顶点 - int closestTriangle = findClosestTriangle(localBrushCenter, newVertices, newIndices); - if (closestTriangle != -1) { - connectVertexToTriangle(centerIndex, closestTriangle, newIndices); - } - - addedVertices = true; - logger.debug("在画笔中心添加顶点: ({}, {}), 最近顶点距离: {}", - localBrushCenter.x, localBrushCenter.y, minDistance); - } - - // 如果添加了顶点,更新网格数据 - if (addedVertices) { - // 创建新的数组 - float[] finalVertices = new float[newVertices.size() * 2]; - float[] finalUVs = new float[newUVs.size() * 2]; - int[] finalIndices = new int[newIndices.size()]; - - // 填充顶点数据 - for (int i = 0; i < newVertices.size(); i++) { - if (i * 2 + 1 < finalVertices.length) { - finalVertices[i * 2] = newVertices.get(i).x; - finalVertices[i * 2 + 1] = newVertices.get(i).y; - } - } - - // 填充UV数据 - for (int i = 0; i < newUVs.size(); i++) { - if (i * 2 + 1 < finalUVs.length) { - finalUVs[i * 2] = newUVs.get(i).x; - finalUVs[i * 2 + 1] = newUVs.get(i).y; - } - } - - // 填充索引数据 - for (int i = 0; i < newIndices.size(); i++) { - if (i < finalIndices.length) { - finalIndices[i] = newIndices.get(i); - } - } - - // 设置新的网格数据 - mesh.setMeshData(finalVertices, finalUVs, finalIndices); - logger.info("网格 {} 已添加顶点: {} -> {}", mesh.getName(), originalVertices.length / 2, newVertices.size()); - } - } - } - - /** - * 找到距离给定点最近的三角形 - */ - private int findClosestTriangle(Vector2f point, List vertices, List indices) { - int closestTriangle = -1; - float minDistance = Float.MAX_VALUE; - - for (int i = 0; i < indices.size(); i += 3) { - // 检查是否有足够的索引 - if (i + 2 >= indices.size()) break; - - int i1 = indices.get(i); - int i2 = indices.get(i + 1); - int i3 = indices.get(i + 2); - - // 检查索引是否有效 - if (i1 < 0 || i1 >= vertices.size() || - i2 < 0 || i2 >= vertices.size() || - i3 < 0 || i3 >= vertices.size()) { - continue; - } - - Vector2f v1 = vertices.get(i1); - Vector2f v2 = vertices.get(i2); - Vector2f v3 = vertices.get(i3); - - // 计算三角形中心 - Vector2f center = new Vector2f( - (v1.x + v2.x + v3.x) / 3f, - (v1.y + v2.y + v3.y) / 3f - ); - - float distance = center.distance(point); - if (distance < minDistance) { - minDistance = distance; - closestTriangle = i; - } - } - - return closestTriangle; - } - - /** - * 将顶点连接到三角形(将三角形细分为3个小三角形) - */ - private void connectVertexToTriangle(int vertexIndex, int triangleStart, List indices) { - int i1 = indices.get(triangleStart); - int i2 = indices.get(triangleStart + 1); - int i3 = indices.get(triangleStart + 2); - - // 修改原三角形为第一个小三角形 - indices.set(triangleStart, vertexIndex); - indices.set(triangleStart + 1, i1); - indices.set(triangleStart + 2, i2); - - // 添加另外两个小三角形 - indices.add(vertexIndex); - indices.add(i2); - indices.add(i3); - - indices.add(vertexIndex); - indices.add(i3); - indices.add(i1); - } - - /** - * 为新顶点估算UV坐标 - */ - private Vector2f estimateUVForNewVertex(float x, float y, Mesh2D mesh) { - BoundingBox bounds = mesh.getBounds(); - if (bounds != null && bounds.isValid()) { - float u = (x - bounds.getMinX()) / bounds.getWidth(); - float v = (y - bounds.getMinY()) / bounds.getHeight(); - return new Vector2f(u, v); - } - return new Vector2f(0.5f, 0.5f); - } - - /** - * 辅助:绕中心旋转点 - */ - private static Vector2f rotateAround(Vector2f point, Vector2f center, float angleRad) { - float cos = (float) Math.cos(angleRad); - float sin = (float) Math.sin(angleRad); - float x = point.x - center.x; - float y = point.y - center.y; - float rx = x * cos - y * sin; - float ry = x * sin + y * cos; - return new Vector2f(center.x + rx, center.y + ry); - } - - // 更新局部矩阵 - private void updateLocalTransform() { - float cos = (float) Math.cos(rotation); - float sin = (float) Math.sin(rotation); - - float sx = scale.x; - float sy = scale.y; - - // 旋转 + 缩放矩阵 - float m00 = cos * sx; - float m01 = -sin * sy; - float m10 = sin * sx; - float m11 = cos * sy; - - // 平移部分考虑 pivot - // pivot 影响旋转中心,而 position 是最终放置位置 - float m02 = position.x - (m00 * pivot.x + m01 * pivot.y) + pivot.x; - float m12 = position.y - (m10 * pivot.x + m11 * pivot.y) + pivot.y; - - localTransform.set( - m00, m01, m02, - m10, m11, m12, - 0f, 0f, 1f - ); - } - - /** - * 立即重新计算本节点的 worldTransform(并递归到子节点) - */ - public void recomputeWorldTransformRecursive() { - if (transformDirty) { - updateLocalTransform(); - } - - if (parent != null) { - // parent.worldTransform 已经是最新(假设父节点正确维护) - parent.worldTransform.mul(localTransform, worldTransform); - } else { - worldTransform.set(localTransform); - } - - // 递归更新子节点 - for (ModelPart child : children) { - child.recomputeWorldTransformRecursive(); - } - - boundsDirty = true; - transformDirty = false; - } - - // 打印世界坐标(修正为使用 worldTransform) - public void printWorldPosition() { - float worldX = worldTransform.m02(); - float worldY = worldTransform.m12(); - logger.info("World position: {}, {}", worldX, worldY); - } - - /** - * 标记变换需要更新 - */ - public void markTransformDirty() { - this.transformDirty = true; - for (ModelPart child : children) { - child.markTransformDirty(); - } - } - - /** - * 设置位置 - */ - public void setPosition(float x, float y) { - // 防止递归调用 - if (inMultiSelectionOperation) { - // 直接执行单选择辑,避免递归 - position.set(x, y); - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - - for (Mesh2D mesh : meshes) { - Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, mesh.getOriginalPivot()); - mesh.setPivot(worldPivot.x, worldPivot.y); - } - - updateMeshVertices(); - triggerEvent("position"); - return; - } - - // 如果是多选状态下的移动,使用多选移动方法 - if (isInMultiSelection() && !getSelectedMeshes().isEmpty()) { - Vector2f currentPos = getPosition(); - float dx = x - currentPos.x; - float dy = y - currentPos.y; - - // 设置标志防止递归 - inMultiSelectionOperation = true; - try { - moveSelectedMeshes(dx, dy); - } finally { - inMultiSelectionOperation = false; - } - return; - } - - // 原有单选择辑 - position.set(x, y); - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - - for (Mesh2D mesh : meshes) { - Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, mesh.getOriginalPivot()); - mesh.setPivot(worldPivot.x, worldPivot.y); - } - - updateMeshVertices(); - triggerEvent("position"); - } - - - /** - * 更新所有网格的顶点位置以反映当前变换 - */ - public void updateMeshVertices() { - recomputeWorldTransformRecursive(); - for (Mesh2D mesh : meshes) { - if (mesh == null) continue; - for (Vertex vertex : mesh.getActiveVertexList()) { - Vector2f localPoint = vertex.originalPosition; - Vector2f worldPoint = Matrix3fUtils.transformPoint(this.worldTransform, localPoint); - vertex.position.set(worldPoint); - triggerEvent("vertex_position"); - } - mesh.markDirty(); - } - for (ModelPart child : children) { - child.updateMeshVertices(); - } - } - - - /** - * 强制更新单个网格的顶点位置 - */ - private void updateMeshVertices(Mesh2D mesh) { - if (mesh == null) return; - - // 获取原始顶点数据(局部坐标) - float[] originalVertices = mesh.getOriginalVertices(); - if (originalVertices == null || originalVertices.length == 0) { - logger.warn("网格 {} 没有原始顶点数据,无法更新变换", mesh.getName()); - return; - } - - // 确保世界变换是最新的 - if (transformDirty) { - updateLocalTransform(); - recomputeWorldTransformRecursive(); - } - - int vertexCount = originalVertices.length / 2; - - // 应用当前世界变换到每个顶点 - 添加边界检查 - for (int i = 0; i < vertexCount; i++) { - if (i * 2 + 1 >= originalVertices.length) { - logger.warn("顶点索引 {} 超出原始顶点数组范围", i); - continue; - } - - Vector2f localPoint = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]); - Vector2f worldPoint = Matrix3fUtils.transformPoint(worldTransform, localPoint); - - // 检查目标索引是否有效 - if (i < mesh.getVertexCount()) { - mesh.setVertex(i, worldPoint.x, worldPoint.y); - } else { - logger.warn("顶点索引 {} 超出网格顶点范围 (总顶点数: {})", i, mesh.getVertexCount()); - } - } - - // 同步 mesh 的原始局部 pivot -> 当前世界 pivot - try { - Vector2f origPivot = mesh.getOriginalPivot(); - Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, origPivot); - mesh.setPivot(worldPivot.x, worldPivot.y); - } catch (Exception e) { - logger.warn("更新网格pivot时出错: {}", e.getMessage()); - } - - // 标记网格需要更新 - mesh.markDirty(); - mesh.setBakedToWorld(true); - } - - public void setPosition(Vector2f pos) { - // 记录旧世界变换和旧位置,用于计算位移 - Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); - Vector2f oldPosition = new Vector2f(this.position); - - // 更新部件的局部位置 - this.position.set(pos); - - // 标记变换脏,更新局部变换和世界变换 - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - - // 计算部件的实际位移 - float dx = this.position.x - oldPosition.x; - float dy = this.position.y - oldPosition.y; - - // 更新每个网格的 pivot 和 originalPivot - for (Mesh2D mesh : meshes) { - // 将 mesh 的原始局部 pivot 变换到旧的世界坐标系 - Vector2f oldWorldPivot = Matrix3fUtils.transformPoint(oldWorldTransform, mesh.getOriginalPivot()); - // 在世界坐标系中应用位移 - Vector2f movedWorldPivot = new Vector2f(oldWorldPivot.x + dx, oldWorldPivot.y + dy); - // 将位移后的世界 pivot 逆变换回新的局部坐标系(即新的 originalPivot) - Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, movedWorldPivot); - - mesh.setOriginalPivot(newLocalOriginalPivot); - mesh.setPivot(movedWorldPivot.x, movedWorldPivot.y); - } - - // 更新网格顶点位置 - updateMeshVertices(); - } - - /** - * 移动部件 - */ - public void translate(float dx, float dy) { - position.add(dx, dy); - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - } - - public void translate(Vector2f delta) { - position.add(delta); - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - } - - /** - * 设置旋转(弧度) - */ - public void setRotation(float radians) { - // 如果是多选状态下的旋转,使用多选旋转方法 - if (isInMultiSelection() && !getSelectedMeshes().isEmpty()) { - float currentRotation = getRotation(); - float deltaAngle = radians - currentRotation; - rotateSelectedMeshes(deltaAngle); - return; - } - - // 原有单选择辑 - Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); - this.rotation = radians; - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - - for (Mesh2D mesh : meshes) { - Vector2f oldWorldPivot = Matrix3fUtils.transformPoint(oldWorldTransform, mesh.getOriginalPivot()); - Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot); - mesh.setOriginalPivot(newLocalOriginalPivot); - mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); - } - - updateMeshVertices(); - triggerEvent("rotation"); - } - - /** - * 旋转部件 - */ - public void rotate(float deltaRadians) { - this.rotation += deltaRadians; - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - updateMeshVertices(); - triggerEvent("rotation"); - } - - /** - * 设置缩放 - */ - public void setScale(float sx, float sy) { - // 如果是多选状态下的缩放,使用多选缩放方法 - if (isInMultiSelection() && !getSelectedMeshes().isEmpty()) { - Vector2f currentScale = getScale(); - float scaleX = sx / currentScale.x; - float scaleY = sy / currentScale.y; - scaleSelectedMeshes(scaleX, scaleY); - return; - } - - // 原有单选择辑 - Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); - this.scaleX = sx; - this.scaleY = sy; - scale.set(sx, sy); - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - - for (Mesh2D mesh : meshes) { - Vector2f oldWorldPivot = Matrix3fUtils.transformPoint(oldWorldTransform, mesh.getOriginalPivot()); - Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot); - mesh.setOriginalPivot(newLocalOriginalPivot); - mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); - } - - updateMeshVertices(); - triggerEvent("scale"); - } - - - public void setScale(float uniformScale) { - // 记录旧的世界变换,用于计算 pivot 的相对位置 - Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); - - scale.set(uniformScale, uniformScale); - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - - // 缩放操作会改变部件的局部坐标系,因此需要更新网格的 originalPivot - for (Mesh2D mesh : meshes) { - // 将 mesh 的原始局部 pivot 变换到旧的世界坐标系 - Vector2f oldWorldPivot = Matrix3fUtils.transformPoint(oldWorldTransform, mesh.getOriginalPivot()); - // 将旧的世界 pivot 逆变换回新的局部坐标系(即新的 originalPivot) - Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot); - - mesh.setOriginalPivot(newLocalOriginalPivot); - // 同时更新 mesh 的当前 pivot 到新的世界坐标 - mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); - } - - updateMeshVertices(); - triggerEvent("scale"); - } - - public void setScale(Vector2f scale) { - // 记录旧的世界变换,用于计算 pivot 的相对位置 - Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); - - this.scale.set(scale); - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - - // 缩放操作会改变部件的局部坐标系,因此需要更新网格的 originalPivot - for (Mesh2D mesh : meshes) { - // 将 mesh 的原始局部 pivot 变换到旧的世界坐标系 - Vector2f oldWorldPivot = Matrix3fUtils.transformPoint(oldWorldTransform, mesh.getOriginalPivot()); - // 将旧的世界 pivot 逆变换回新的局部坐标系(即新的 originalPivot) - Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot); - - mesh.setOriginalPivot(newLocalOriginalPivot); - // 同时更新 mesh 的当前 pivot 到新的世界坐标 - mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); - } - - updateMeshVertices(); - triggerEvent("scale"); - } - - /** - * 缩放部件 - */ - public void scale(float sx, float sy) { - scale.mul(sx, sy); - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - triggerEvent("scale"); - } - - // ==================== 网格管理 ==================== - - /** - * 添加网格 - */ - public void addMesh(Mesh2D mesh) { - if (mesh == null) return; - - // 确保拷贝保留原始的纹理引用(copy() 已处理) - //mesh.setTexture(mesh.getTexture()); - mesh.setModelPart(this); - // 确保本节点的 worldTransform 是最新的 - recomputeWorldTransformRecursive(); - - // 保存拷贝的原始(局部)顶点供后续重算 world 顶点使用 - float[] originalVertices = mesh.getVertices().clone(); - mesh.setOriginalVertices(originalVertices); - // 把 originalPivot 保存在 mesh 中(setMeshData 已经初始化 originalPivot) - // 将每个顶点从本地空间变换到世界空间(烘焙到 world) - int vc = mesh.getVertexCount(); - for (int i = 0; i < vc; i++) { - Vector2f local = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]); - Vector2f worldPt = Matrix3fUtils.transformPoint(this.worldTransform, local); - mesh.setVertex(i, worldPt.x, worldPt.y); - } - - // 同步 originalPivot -> world pivot(如果 originalPivot 有意义) - try { - Vector2f origPivot = mesh.getOriginalPivot(); - Vector2f worldPivot = Matrix3fUtils.transformPoint(this.worldTransform, origPivot); - mesh.setPivot(worldPivot.x, worldPivot.y); - } catch (Exception ignored) { } - - // 标记为已烘焙到世界坐标(语义上明确),并确保 bounds/dirty 状态被正确刷新 - mesh.setBakedToWorld(true); - - // 确保 GPU 数据在下一次绘制时会被上传(如果当前在渲染线程,也可以直接 uploadToGPU) - mesh.markDirty(); - - // 将拷贝加入到本部件 - meshes.add(mesh); - boundsDirty = true; - } - - /** - * 设置中心点 - */ - public boolean setPivot(float x, float y) { - // 无论是否首次设置,都允许设置任意 pivot - // pivotInitialized = true; // 此行不再需要,因为不再强制 (0,0) - - for (Mesh2D mesh : meshes) { - // ModelPart 的 pivot 是在部件的局部坐标系中定义的 - // Mesh2D 的 setPivot 期望的是 Mesh2D 自己的局部坐标系中的 pivot - // 因此需要将 ModelPart 的局部 pivot 转换为 Mesh2D 的局部 pivot - // 由于 Mesh2D 的 originalPivot 已经存储了其在 ModelPart 局部坐标系中的相对位置, - // 我们可以直接将 ModelPart 的新 pivot 赋值给 Mesh2D 的 originalPivot - // 然后再通过变换更新 Mesh2D 的实际 pivot - if (!mesh.setOriginalPivot(new Vector2f(x, y))){ - return false; - } - // Mesh2D 的实际 pivot 应该根据 ModelPart 的世界变换来计算 - // 这里只是设置了 originalPivot,Mesh2D 的实际 pivot 会在 updateMeshVertices 中更新 - // 或者在 ModelPart 的 setPivot 之后,立即触发 Mesh2D 的 pivot 更新 - // 为了简化,我们假设 Mesh2D 的 setPivot 能够处理好 originalPivot 和实际 pivot 的关系 - // 或者在 ModelPart 的 updateMeshVertices 中统一处理 - // 暂时不在这里调用 mesh.setPivot(x, y),因为 Mesh2D.setPivot 有边界检查,可能导致设置失败 - // 正确的做法是更新 Mesh2D 的 originalPivot,然后让 ModelPart 的变换系统来更新 Mesh2D 的实际 pivot - // if (!mesh.setPivot(x, y)){ - // return false; - // } - } - - pivot.set(x, y); - - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - triggerEvent("pivot"); - updateMeshVertices(); - return true; - } - - /** - * 获取旋转中心 - */ - public Vector2f getPivot() { - return new Vector2f(pivot); - } - - /** - * 移除网格 - */ - public boolean removeMesh(Mesh2D mesh) { - boolean removed = meshes.remove(mesh); - if (removed) { - boundsDirty = true; - } - return removed; - } - - /** - * 获取所有网格 - */ - public List getMeshes() { - return meshes; - } - - // ==================== 参数管理 ==================== - public AnimationParameter createParameter(String id, float min, float max, float defaultValue) { - AnimationParameter param = new AnimationParameter(id, min, max, defaultValue); - parameters.put(id, param); - return param; - } - - public AnimationParameter getParameter(String id) { - return parameters.get(id); - } - - public void addParameter(AnimationParameter param) { - parameters.put(param.getId(), param); - } - - public void setParameterValue(String paramId, float value) { - AnimationParameter param = parameters.get(paramId); - if (param != null) { - param.setValue(value); - } - } - - public float getParameterValue(String paramId) { - AnimationParameter param = parameters.get(paramId); - return param != null ? param.getValue() : 0.0f; - } - - // ==================== 变形器管理 ==================== - - /** - * 添加变形器 - */ - public void addDeformer(Deformer deformer) { - deformers.add(deformer); - } - - /** - * 移除变形器 - */ - public boolean removeDeformer(Deformer deformer) { - return deformers.remove(deformer); - } - - /** - * 应用参数到所有变形器 - */ - public void applyParameter(AnimationParameter param) { - for (Deformer deformer : deformers) { - if (deformer.isDrivenBy(param.getId())) { - deformer.apply(param.getValue()); - } - } - - // 如果变形器改变了网格,需要更新边界 - if (!deformers.isEmpty()) { - boundsDirty = true; - } - } - - /** - * 应用所有变形器 - */ - public void applyDeformers() { - for (Deformer deformer : deformers) { - for (Mesh2D mesh : meshes) { - deformer.applyToMesh(mesh); - } - } - boundsDirty = true; - } - - // ==================== 工具方法 ==================== - - /** - * 变换点从局部空间到世界空间 - */ - public Vector2f localToWorld(Vector2f localPoint) { - return Matrix3fUtils.transformPoint(worldTransform, localPoint); - } - - /** - * 变换点从世界空间到局部空间 - */ - public Vector2f worldToLocal(Vector2f worldPoint) { - return Matrix3fUtils.transformPointInverse(worldTransform, worldPoint); - } - - /** - * 获取世界空间中的包围盒 - */ - public BoundingBox getWorldBounds() { - if (boundsDirty) { - updateBounds(); - } - - BoundingBox worldBounds = new BoundingBox(); - for (Mesh2D mesh : meshes) { - BoundingBox meshBounds = mesh.getBounds(); - if (meshBounds != null) { - // 变换到世界空间 - Vector2f min = localToWorld(new Vector2f(meshBounds.getMinX(), meshBounds.getMinY())); - Vector2f max = localToWorld(new Vector2f(meshBounds.getMaxX(), meshBounds.getMaxY())); - worldBounds.expand(min.x, min.y); - worldBounds.expand(max.x, max.y); - } - } - - return worldBounds; - } - - /** - * 更新边界 - */ - private void updateBounds() { - for (Mesh2D mesh : meshes) { - mesh.updateBounds(); - } - boundsDirty = false; - } - - /** - * 检查是否可见(考虑父级可见性) - */ - public boolean isEffectivelyVisible() { - if (!visible) { - return false; - } - if (parent != null) { - return parent.isEffectivelyVisible(); - } - return true; - } - - // ==================== Getter/Setter ==================== - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public ModelPart getParent() { - return parent; - } - - public Vector2f getPosition() { - return new Vector2f(position); - } - - public float getRotation() { - return rotation; - } - - public Vector2f getScale() { - return new Vector2f(scale); - } - - public Matrix3f getLocalTransform() { - return new Matrix3f(localTransform); - } - - public Matrix3f getWorldTransform() { - return new Matrix3f(worldTransform); - } - - public boolean isVisible() { - return visible; - } - - public void setVisible(boolean visible) { - this.visible = visible; - } - - public BlendMode getBlendMode() { - return blendMode; - } - - public void setBlendMode(BlendMode blendMode) { - this.blendMode = blendMode; - } - - public float getOpacity() { - return opacity; - } - - public float getScaleX() { return scaleX; } - public float getScaleY() { return scaleY; } - public Map getParameters() { - return Collections.unmodifiableMap(parameters); - } - public void setOpacity(float opacity) { - this.opacity = Math.max(0.0f, Math.min(1.0f, opacity)); - } - - public List getDeformers() { - return new ArrayList<>(deformers); - } - - // ====== 新增:单个液化点数据结构(Serializable 友好,并提供 getter) ====== - public static class LiquifyPoint { - public float x; - public float y; - public float pressure = 1.0f; - - public LiquifyPoint() {} - - public LiquifyPoint(float x, float y) { - this.x = x; - this.y = y; - } - - public LiquifyPoint(float x, float y, float pressure) { - this.x = x; - this.y = y; - this.pressure = pressure; - } - - public float getX() { return x; } - public float getY() { return y; } - public float getPressure() { return pressure; } - } - - // ====== 液化笔划数据结构(包含点序列与笔划参数),提供 getter 以便反射读取 ====== - public static class LiquifyStroke { - public LiquifyMode mode = LiquifyMode.PUSH; - public float radius = 50.0f; - public float strength = 0.5f; - public int iterations = 1; - public List points = new ArrayList<>(); - - public LiquifyStroke() {} - - public LiquifyStroke(LiquifyMode mode, float radius, float strength, int iterations) { - this.mode = mode; - this.radius = radius; - this.strength = strength; - this.iterations = iterations; - } - - public String getMode() { return mode.name(); } // PartData 反射时读取字符串也可 - public float getRadius() { return radius; } - public float getStrength() { return strength; } - public int getIterations() { return iterations; } - public List getPoints() { return points; } - - public void addPoint(float x, float y, float pressure) { - this.points.add(new LiquifyPoint(x, y, pressure)); - } - } - - // ==================== 枚举和内部类 ==================== - - /** - * 混合模式枚举 - */ - public enum BlendMode { - NORMAL, - ADDITIVE, - MULTIPLY, - SCREEN - } - - // ==================== Object 方法 ==================== - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ModelPart modelPart = (ModelPart) o; - return Float.compare(rotation, modelPart.rotation) == 0 && - visible == modelPart.visible && - Float.compare(opacity, modelPart.opacity) == 0 && - Objects.equals(name, modelPart.name) && - Objects.equals(position, modelPart.position) && - Objects.equals(scale, modelPart.scale); - } - - @Override - public int hashCode() { - return Objects.hash(name, position, rotation, scale, visible, opacity); - } - - @Override - public String toString() { - List selectedMeshes = getSelectedMeshes(); - return "ModelPart{" + - "name='" + name + '\'' + - ", position=" + position + - ", rotation=" + rotation + - ", scale=" + scale + - ", visible=" + visible + - ", children=" + children.size() + - ", meshes=" + meshes.size() + - ", selectedMeshes=" + selectedMeshes.size() + - ", inMultiSelection=" + isInMultiSelection() + - '}'; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/AnimationLayerData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/AnimationLayerData.java deleted file mode 100644 index c4bb5d8..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/AnimationLayerData.java +++ /dev/null @@ -1,172 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.data; - -import com.chuangzhou.vivid2D.render.model.util.AnimationClip; -import com.chuangzhou.vivid2D.render.model.util.AnimationLayer; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * 动画层数据 - * - * @author tzdwindows 7 - */ -public class AnimationLayerData implements Serializable { - private static final long serialVersionUID = 1L; - - public String name; - public float weight; - public boolean enabled; - public AnimationLayer.BlendMode blendMode; - public int priority; - public float playbackSpeed; - public boolean looping; - public Map tracks; - public List clipNames; - - public AnimationLayerData() { - this.tracks = new HashMap<>(); - this.clipNames = new ArrayList<>(); - } - - public AnimationLayerData(AnimationLayer layer) { - this(); - this.name = layer.getName(); - this.weight = layer.getWeight(); - this.enabled = layer.isEnabled(); - this.blendMode = layer.getBlendMode(); - this.priority = layer.getPriority(); - this.playbackSpeed = layer.getPlaybackSpeed(); - this.looping = layer.isLooping(); - - // 序列化轨道 - for (AnimationLayer.AnimationTrack track : layer.getTracks().values()) { - this.tracks.put(track.getParameterId(), new AnimationTrackData(track)); - } - - // 序列化剪辑名称(剪辑对象本身需要单独序列化) - for (AnimationClip clip : layer.getClips()) { - this.clipNames.add(clip.getName()); - } - } - - public AnimationLayer toAnimationLayer() { - AnimationLayer layer = new AnimationLayer(name, weight); - layer.setEnabled(enabled); - layer.setBlendMode(blendMode); - layer.setPriority(priority); - layer.setPlaybackSpeed(playbackSpeed); - layer.setLooping(looping); - - // 反序列化轨道 - for (AnimationTrackData trackData : tracks.values()) { - AnimationLayer.AnimationTrack track = trackData.toAnimationTrack(); - layer.getTracks().put(track.getParameterId(), track); - } - - // 注意:剪辑对象需要在外部设置 - - return layer; - } - - public AnimationLayerData copy() { - AnimationLayerData copy = new AnimationLayerData(); - copy.name = this.name; - copy.weight = this.weight; - copy.enabled = this.enabled; - copy.blendMode = this.blendMode; - copy.priority = this.priority; - copy.playbackSpeed = this.playbackSpeed; - copy.looping = this.looping; - copy.tracks = new HashMap<>(); - for (Map.Entry entry : this.tracks.entrySet()) { - copy.tracks.put(entry.getKey(), entry.getValue().copy()); - } - copy.clipNames = new ArrayList<>(this.clipNames); - return copy; - } - - /** - * 动画轨道数据 - */ - public static class AnimationTrackData implements Serializable { - private static final long serialVersionUID = 1L; - - public String parameterId; - public boolean enabled; - public AnimationLayer.InterpolationType interpolation; - public List keyframes; - - public AnimationTrackData() { - this.keyframes = new ArrayList<>(); - } - - public AnimationTrackData(AnimationLayer.AnimationTrack track) { - this(); - this.parameterId = track.getParameterId(); - this.enabled = track.isEnabled(); - this.interpolation = track.getInterpolation(); - - // 序列化关键帧 - for (AnimationLayer.Keyframe keyframe : track.getKeyframes()) { - this.keyframes.add(new KeyframeData(keyframe)); - } - } - - public AnimationLayer.AnimationTrack toAnimationTrack() { - AnimationLayer.AnimationTrack track = new AnimationLayer.AnimationTrack(parameterId); - track.setEnabled(enabled); - track.setInterpolation(interpolation); - - // 反序列化关键帧 - for (KeyframeData kfData : keyframes) { - track.addKeyframe(kfData.time, kfData.value, kfData.interpolation); - } - - return track; - } - - public AnimationTrackData copy() { - AnimationTrackData copy = new AnimationTrackData(); - copy.parameterId = this.parameterId; - copy.enabled = this.enabled; - copy.interpolation = this.interpolation; - copy.keyframes = new ArrayList<>(); - for (KeyframeData kf : this.keyframes) { - copy.keyframes.add(kf.copy()); - } - return copy; - } - } - - /** - * 关键帧数据(重用现有的 KeyframeData) - */ - public static class KeyframeData implements Serializable { - private static final long serialVersionUID = 1L; - - public float time; - public float value; - public AnimationLayer.InterpolationType interpolation; - - public KeyframeData() { - } - - public KeyframeData(AnimationLayer.Keyframe keyframe) { - this.time = keyframe.getTime(); - this.value = keyframe.getValue(); - this.interpolation = keyframe.getInterpolation(); - } - - public KeyframeData copy() { - KeyframeData copy = new KeyframeData(); - copy.time = this.time; - copy.value = this.value; - copy.interpolation = this.interpolation; - return copy; - } - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/LightSourceData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/LightSourceData.java deleted file mode 100644 index 432458e..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/LightSourceData.java +++ /dev/null @@ -1,293 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.data; - -import com.chuangzhou.vivid2D.render.model.util.LightSource; -import com.chuangzhou.vivid2D.render.model.util.SaveVector2f; -import org.joml.Vector2f; -import org.joml.Vector3f; - -import java.io.Serializable; - -/** - * LightSource 的序列化数据类(扩展:包含辉光/Glow 的序列化字段) - * - * @author tzdwindows 7 - */ -public class LightSourceData implements Serializable { - private static final long serialVersionUID = 1L; - - // 光源属性 - private String id; - private String position; // 使用字符串格式存储 Vector2f - private String color; // 使用字符串格式存储 Vector3f - private float intensity; - private boolean enabled; - private boolean isAmbient; - - // ======= 辉光(Glow)相关序列化字段 ======= - private boolean isGlow; - private String glowDirection; // 使用字符串格式存储 Vector2f - private float glowIntensity; - private float glowRadius; - private float glowAmount; - - // 默认构造器 - public LightSourceData() { - this.id = "light_" + System.currentTimeMillis(); - this.position = "0,0"; - this.color = "1,1,1"; - this.intensity = 1.0f; - this.enabled = true; - this.isAmbient = false; - - // 默认辉光值 - this.isGlow = false; - this.glowDirection = "0,0"; - this.glowIntensity = 0.0f; - this.glowRadius = 50.0f; - this.glowAmount = 1.0f; - } - - // 从 LightSource 对象构造 - public LightSourceData(LightSource light) { - this(); - if (light != null) { - this.id = "light_" + System.currentTimeMillis() + "_" + light.hashCode(); - this.position = SaveVector2f.toString(light.getPosition()); - this.color = vector3fToString(light.getColor()); - this.intensity = light.getIntensity(); - this.enabled = light.isEnabled(); - this.isAmbient = light.isAmbient(); - - // 辉光相关 - this.isGlow = light.isGlow(); - this.glowDirection = SaveVector2f.toString(light.getGlowDirection() != null ? light.getGlowDirection() : new Vector2f(0f, 0f)); - this.glowIntensity = light.getGlowIntensity(); - this.glowRadius = light.getGlowRadius(); - this.glowAmount = light.getGlowAmount(); - } - } - - // 转换为 LightSource 对象 - public LightSource toLightSource() { - Vector2f pos = SaveVector2f.fromString(position); - Vector3f col = stringToVector3f(color); - - LightSource light; - light = new LightSource(LightSource.vector3fToColor(col), intensity); - light.setEnabled(enabled); - light.setAmbient(isAmbient); - - // 如果使用了环境光构造器但需要设置辉光(罕见),通过 setter 设置 - if (isAmbient) { - light.setGlow(isGlow); - light.setGlowDirection(SaveVector2f.fromString(glowDirection)); - light.setGlowIntensity(glowIntensity); - light.setGlowRadius(glowRadius); - light.setGlowAmount(glowAmount); - } - - return light; - } - - // 深拷贝 - public LightSourceData copy() { - LightSourceData copy = new LightSourceData(); - copy.id = this.id; - copy.position = this.position; - copy.color = this.color; - copy.intensity = this.intensity; - copy.enabled = this.enabled; - copy.isAmbient = this.isAmbient; - - copy.isGlow = this.isGlow; - copy.glowDirection = this.glowDirection; - copy.glowIntensity = this.glowIntensity; - copy.glowRadius = this.glowRadius; - copy.glowAmount = this.glowAmount; - - return copy; - } - - // ==================== 工具方法 ==================== - - private String vector3fToString(Vector3f vec) { - if (vec == null) { - return "1,1,1"; - } - return vec.x + "," + vec.y + "," + vec.z; - } - - private Vector3f stringToVector3f(String str) { - if (str == null || str.trim().isEmpty()) { - return new Vector3f(1, 1, 1); - } - - str = str.trim(); - String[] parts = str.split(","); - if (parts.length != 3) { - return new Vector3f(1, 1, 1); - } - - try { - float x = Float.parseFloat(parts[0].trim()); - float y = Float.parseFloat(parts[1].trim()); - float z = Float.parseFloat(parts[2].trim()); - return new Vector3f(x, y, z); - } catch (NumberFormatException e) { - return new Vector3f(1, 1, 1); - } - } - - // ==================== Getter/Setter ==================== - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getPosition() { - return position; - } - - public void setPosition(String position) { - this.position = position; - } - - public String getColor() { - return color; - } - - public void setColor(String color) { - this.color = color; - } - - public float getIntensity() { - return intensity; - } - - public void setIntensity(float intensity) { - this.intensity = intensity; - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public boolean isAmbient() { - return isAmbient; - } - - public void setAmbient(boolean ambient) { - isAmbient = ambient; - } - - // ======= 辉光相关 Getter/Setter ======= - - public boolean isGlow() { - return isGlow; - } - - public void setGlow(boolean glow) { - isGlow = glow; - } - - public String getGlowDirection() { - return glowDirection; - } - - public void setGlowDirection(String glowDirection) { - this.glowDirection = glowDirection; - } - - public float getGlowIntensity() { - return glowIntensity; - } - - public void setGlowIntensity(float glowIntensity) { - this.glowIntensity = glowIntensity; - } - - public float getGlowRadius() { - return glowRadius; - } - - public void setGlowRadius(float glowRadius) { - this.glowRadius = glowRadius; - } - - public float getGlowAmount() { - return glowAmount; - } - - public void setGlowAmount(float glowAmount) { - this.glowAmount = glowAmount; - } - - // ==================== 工具方法(向量形式) ==================== - - /** - * 设置位置为 Vector2f - */ - public void setPosition(Vector2f position) { - this.position = SaveVector2f.toString(position); - } - - /** - * 获取位置为 Vector2f - */ - public Vector2f getPositionAsVector() { - return SaveVector2f.fromString(position); - } - - /** - * 设置颜色为 Vector3f - */ - public void setColor(Vector3f color) { - this.color = vector3fToString(color); - } - - /** - * 获取颜色为 Vector3f - */ - public Vector3f getColorAsVector() { - return stringToVector3f(color); - } - - /** - * 设置辉光方向(Vector2f) - */ - public void setGlowDirection(Vector2f dir) { - this.glowDirection = SaveVector2f.toString(dir); - } - - /** - * 获取辉光方向为 Vector2f - */ - public Vector2f getGlowDirectionAsVector() { - return SaveVector2f.fromString(glowDirection); - } - - @Override - public String toString() { - return "LightSourceData{" + - "id='" + id + '\'' + - ", position='" + position + '\'' + - ", color='" + color + '\'' + - ", intensity=" + intensity + - ", enabled=" + enabled + - ", isAmbient=" + isAmbient + - ", isGlow=" + isGlow + - ", glowDirection='" + glowDirection + '\'' + - ", glowIntensity=" + glowIntensity + - ", glowRadius=" + glowRadius + - ", glowAmount=" + glowAmount + - '}'; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java deleted file mode 100644 index 25c009c..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.data; - -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.util.Vertex; -import com.chuangzhou.vivid2D.render.model.util.VertexList; -import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; -import org.joml.Matrix3f; -import org.joml.Vector2f; - -import java.io.Serial; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -/** - * Mesh2D的数据传输对象(DTO),用于序列化和反序列化网格数据。 - * 负责处理从世界坐标到局部坐标的转换,以便正确保存模型数据。 - */ -public class MeshData implements Serializable { - @Serial - private static final long serialVersionUID = 2L; - - public VertexListData vertexListData; - public VertexListData deformationControlVerticesData; - public String name; - public String textureName; - public boolean visible; - public int drawMode; - public float pivotX, pivotY; - public float originalPivotX, originalPivotY; - - public MeshData() { - this.visible = true; - this.drawMode = Mesh2D.TRIANGLES; - } - - public MeshData(Mesh2D mesh) { - this.name = mesh.getName(); - this.visible = mesh.isVisible(); - this.drawMode = mesh.getDrawMode(); - if (mesh.getTexture() != null) { - this.textureName = mesh.getTexture().getName(); - } - - // ==================== 核心修复开始 ==================== - // 1. 获取逆变换矩阵:用于将 世界坐标 -> 局部坐标 - // 这是为了确保保存的是模型相对于父级的原始形状,而不是经过变换后的世界状态 - Matrix3f inverseTransform = new Matrix3f(); - ModelPart parentPart = mesh.getModelPart(); - - if (parentPart != null) { - parentPart.getWorldTransform().invert(inverseTransform); - } - - // 2. 处理顶点数据 (Active Vertex List) - // 我们必须创建一个顶点的深拷贝,以免修改运行时正在渲染的 Mesh2D 对象 - VertexList originalVertexList = mesh.getActiveVertexList(); - List localSpaceVertices = new ArrayList<>(); - - if (originalVertexList != null) { - for (int i = 0; i < originalVertexList.size(); i++) { - // 深拷贝顶点 - Vertex vCopy = originalVertexList.get(i).copy(); - - // 变换位置:pos = inverse * pos (将世界坐标还原为局部坐标) - Matrix3fUtils.transformPoint(inverseTransform, vCopy.position); - - // 重要:同时重置 originalPosition 为局部坐标 - // 这样加载后,deformation 算法才能基于正确的局部 bind-pose 进行计算 - vCopy.originalPosition.set(vCopy.position); - - localSpaceVertices.add(vCopy); - } - } - - // 重建一个临时的 VertexList 用于封装数据(保留原有的索引) - VertexList tempLocalVertexList = new VertexList( - originalVertexList != null ? originalVertexList.getName() : "temp", - localSpaceVertices, - originalVertexList != null ? originalVertexList.getIndices() : new int[0] - ); - this.vertexListData = new VertexListData(tempLocalVertexList); - - // 3. 处理变形控制点 (Deformation Control Vertices) - // 控制点同样存在于世界空间,必须还原回局部 - if (mesh.getDeformationControlVertices() != null) { - List tempControlVertices = new ArrayList<>(); - for (Vertex v : mesh.getDeformationControlVertices()) { - // 深拷贝 - Vertex vCopy = v.copy(); - - // 还原坐标 - Matrix3fUtils.transformPoint(inverseTransform, vCopy.position); - vCopy.originalPosition.set(vCopy.position); // 同步 original - - tempControlVertices.add(vCopy); - } - // 重建 VertexList 结构用于存储 - VertexList controlListWrapper = new VertexList("control_points", tempControlVertices, new int[0]); - this.deformationControlVerticesData = new VertexListData(controlListWrapper); - } - - // 4. 处理 Pivot (中心点) - // Pivot 在 Mesh2D 中通常随着 ModelPart 变换被推到了世界坐标 - Vector2f currentPivot = new Vector2f(mesh.getPivot()); - Matrix3fUtils.transformPoint(inverseTransform, currentPivot); // 还原回局部 - this.pivotX = currentPivot.x; - this.pivotY = currentPivot.y; - - // OriginalPivot 通常本身就是局部的,但为了保险,我们使用刚刚逆变换计算出来的 currentPivot - // 因为在未变形状态下,OriginalPivot 应该等于 Pivot - this.originalPivotX = currentPivot.x; - this.originalPivotY = currentPivot.y; - // ==================== 核心修复结束 ==================== - } - - public Mesh2D toMesh2D() { - Mesh2D mesh = new Mesh2D(this.name); - - // 恢复顶点 (此时它们是局部坐标) - if (this.vertexListData != null) { - VertexList restoredVertexList = this.vertexListData.toVertexList(); - mesh.setActiveVertexList(restoredVertexList); - - // 恢复控制点 (此时它们是局部坐标) - if (this.deformationControlVerticesData != null) { - VertexList restoredControlListData = this.deformationControlVerticesData.toVertexList(); - List controlVertices = new ArrayList<>(); - - // 尝试将控制点链接回主网格的顶点引用(如果索引匹配) - // 这样拖动控制点时,主网格的顶点也会被标记为被控制 - for (Vertex restoredControlInfoVertex : restoredControlListData) { - int originalIndex = restoredControlInfoVertex.getIndex(); - if (originalIndex >= 0 && originalIndex < restoredVertexList.size()) { - Vertex actualVertexInMainList = restoredVertexList.get(originalIndex); - // 确保位置一致(理论上现在都是局部坐标,应该是一致的) - controlVertices.add(actualVertexInMainList); - } else { - // 如果是独立的控制点(不在网格上,或者 cage 顶点),则直接添加 - controlVertices.add(restoredControlInfoVertex); - } - } - mesh.setDeformationControlVertices(controlVertices); - } - } - - mesh.setVisible(this.visible); - mesh.setDrawMode(this.drawMode); - - // 恢复 Pivot (局部坐标) - // 注意:这里设置的是局部 Pivot。 - // 当 mesh 被 addMesh 加入 ModelPart 时,ModelPart 会根据自身的变换 - // 将这个局部 Pivot 再次推算到新的世界 Pivot 位置。 - mesh.setPivot(this.pivotX, this.pivotY); - mesh.setOriginalPivot(new Vector2f(this.originalPivotX, this.originalPivotY)); - - mesh.markDirty(); - return mesh; - } - - public MeshData copy() { - MeshData c = new MeshData(); - c.vertexListData = this.vertexListData != null ? this.vertexListData.copy() : null; - c.deformationControlVerticesData = this.deformationControlVerticesData != null ? this.deformationControlVerticesData.copy() : null; - c.name = this.name; - c.textureName = this.textureName; - c.visible = this.visible; - c.drawMode = this.drawMode; - c.pivotX = this.pivotX; - c.pivotY = this.pivotY; - c.originalPivotX = this.originalPivotX; - c.originalPivotY = this.originalPivotY; - return c; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelData.java deleted file mode 100644 index de74c87..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelData.java +++ /dev/null @@ -1,1166 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.data; - -import com.chuangzhou.vivid2D.render.model.AnimationParameter; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.util.*; -import org.joml.Vector2f; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.*; -import java.util.*; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -/** - * 模型数据类,用于模型的序列化、反序列化和数据交换 - * 支持二进制和JSON格式的模型数据存储 - * - * @author tzdwindows 7 - */ -public class ModelData implements Serializable { - private static final Logger logger = LoggerFactory.getLogger(ModelData.class); - private static final long serialVersionUID = 1L; - - // ==================== 模型元数据 ==================== - private String name; - private String version; - private UUID uuid; - private String author; - private String description; - private long creationTime; - private long lastModifiedTime; - - // ==================== 模型结构数据 ==================== - private List parts; - private List meshes; - private List textures; - private List parameters; - private List animations; - private List animationLayers; - private final List physicsParticles; - private final List physicsSprings; - private final List physicsColliders; - private final List physicsConstraints; - private final List lights; - private List poses; - private String currentPoseName; // 当前应用的姿态名称 - - // 全局物理参数(便于序列化) - private float physicsGravityX; - private float physicsGravityY; - private float physicsAirResistance; - private float physicsTimeScale; - private boolean physicsEnabled; - - - // ==================== 模型设置 ==================== - private Vector2f pivotPoint; - private float unitsPerMeter; - private Map userData; - - // ==================== 构造器 ==================== - - public ModelData() { - this("unnamed"); - } - - public ModelData(String name) { - this.name = name; - this.version = "1.0.0"; - this.uuid = UUID.randomUUID(); - this.creationTime = System.currentTimeMillis(); - this.lastModifiedTime = creationTime; - - this.parts = new ArrayList<>(); - this.meshes = new ArrayList<>(); - this.textures = new ArrayList<>(); - this.parameters = new ArrayList<>(); - this.animations = new ArrayList<>(); - this.animationLayers = new ArrayList<>(); - this.lights = new ArrayList<>(); - - this.pivotPoint = new Vector2f(); - this.unitsPerMeter = 100.0f; // 默认100单位/米 - this.userData = new HashMap<>(); - - // 物理数据初始化 - this.physicsParticles = new ArrayList<>(); - this.physicsSprings = new ArrayList<>(); - this.physicsColliders = new ArrayList<>(); - this.physicsConstraints = new ArrayList<>(); - this.physicsGravityX = 0.0f; - this.physicsGravityY = -98.0f; - this.physicsAirResistance = 0.1f; - this.physicsTimeScale = 1.0f; - this.physicsEnabled = true; - - this.poses = new ArrayList<>(); - this.currentPoseName = "default"; - } - - public ModelData(Model2D model) { - this(model.getName()); - serializeFromModel(model); - } - - // ==================== 序列化方法 ==================== - - private void serializePhysics(Model2D model) { - physicsParticles.clear(); - physicsSprings.clear(); - physicsColliders.clear(); - physicsConstraints.clear(); - - if (model == null) return; - PhysicsSystem phys = model.getPhysics(); - if (phys == null) return; - - // 全局参数 - Vector2f g = phys.getGravity(); - this.physicsGravityX = g.x; - this.physicsGravityY = g.y; - this.physicsAirResistance = phys.getAirResistance(); - this.physicsTimeScale = phys.getTimeScale(); - this.physicsEnabled = phys.isEnabled(); - - // 粒子 - for (Map.Entry e : phys.getParticles().entrySet()) { - PhysicsSystem.PhysicsParticle p = e.getValue(); - ParticleData pd = new ParticleData(); - pd.id = p.getId(); - Vector2f pos = p.getPosition(); - pd.x = pos.x; - pd.y = pos.y; - pd.mass = p.getMass(); - pd.radius = p.getRadius(); - pd.movable = p.isMovable(); - pd.affectedByGravity = p.isAffectedByGravity(); - pd.affectedByWind = p.isAffectedByWind(); - // 如果 userData 是 ModelPart,则保存其 name 以便反序列化时恢复关联 - Object ud = p.getUserData(); - if (ud instanceof ModelPart) { - pd.userPartName = ((ModelPart) ud).getName(); - } else { - pd.userPartName = null; - } - physicsParticles.add(pd); - } - - // 弹簧 - for (PhysicsSystem.PhysicsSpring s : phys.getSprings()) { - SpringData sd = new SpringData(); - sd.id = s.getId(); - sd.aId = s.getParticleA().getId(); - sd.bId = s.getParticleB().getId(); - sd.restLength = s.getRestLength(); - sd.stiffness = s.getStiffness(); - sd.damping = s.getDamping(); - sd.enabled = s.isEnabled(); - physicsSprings.add(sd); - } - - // 约束(仅序列化常见两类) - for (PhysicsSystem.PhysicsConstraint c : phys.getConstraints()) { - if (c instanceof PhysicsSystem.PositionConstraint pc) { - ConstraintData cd = new ConstraintData(); - cd.type = "position"; - cd.particleId = pc.getParticle().getId(); - Vector2f tp = pc.getTargetPosition(); - cd.targetX = tp.x; - cd.targetY = tp.y; - cd.strength = pc.getStrength(); - cd.enabled = pc.isEnabled(); - physicsConstraints.add(cd); - } else if (c instanceof PhysicsSystem.DistanceConstraint dc) { - ConstraintData cd = new ConstraintData(); - cd.type = "distance"; - cd.particleId = dc.getParticle().getId(); - cd.targetParticleId = dc.getTarget().getId(); - cd.maxDistance = dc.getMaxDistance(); - cd.enabled = dc.isEnabled(); - physicsConstraints.add(cd); - } else { - // 忽略未知类型 - } - } - - // 碰撞体 - for (PhysicsSystem.PhysicsCollider collider : phys.getColliders()) { - ColliderData cd = new ColliderData(); - cd.id = collider.getId(); - cd.enabled = collider.isEnabled(); - if (collider instanceof PhysicsSystem.CircleCollider cc) { - cd.type = "circle"; - cd.centerX = cc.getCenter().x; - cd.centerY = cc.getCenter().y; - cd.radius = cc.getRadius(); - } else if (collider instanceof PhysicsSystem.RectangleCollider rc) { - cd.type = "rect"; - cd.centerX = rc.getCenter().x; - cd.centerY = rc.getCenter().y; - cd.width = rc.getWidth(); - cd.height = rc.getHeight(); - } else { - // 未知类型:跳过或扩展 - continue; - } - physicsColliders.add(cd); - } - } - - /** - * 序列化姿态数据 - */ - private void serializePoses(Model2D model) { - poses.clear(); - - // 序列化所有预设姿态 - for (ModelPose pose : model.getPoses().values()) { - poses.add(new PoseData(pose)); - } - - // 保存当前姿态名称 - this.currentPoseName = model.getCurrentPoseName(); - } - - /** - * 反序列化姿态数据 - */ - private void deserializePoses(Model2D model) { - if (poses != null) { - for (PoseData poseData : poses) { - ModelPose pose = poseData.toModelPose(); - if (pose != null) { - model.addPose(pose); - } - } - } - - // 恢复当前姿态 - if (currentPoseName != null && model.hasPose(currentPoseName)) { - model.applyPose(currentPoseName); - } - } - - private void serializeLights(Model2D model) { - lights.clear(); - if (model.getLights() != null) { - for (LightSource light : model.getLights()) { - lights.add(new LightSourceData(light)); - } - } - } - - private void deserializeLights(Model2D model) { - if (lights != null) { - for (LightSourceData lightData : lights) { - LightSource light = lightData.toLightSource(); - if (light != null) { - model.addLight(light); - } - } - } - } - - /** - * 从Model2D对象序列化数据 - */ - public void serializeFromModel(Model2D model) { - if (model == null) { - throw new IllegalArgumentException("Model cannot be null"); - } - - this.name = model.getName(); - this.version = model.getVersion(); - this.uuid = model.getUuid(); - - // 序列化元数据 - if (model.getMetadata() != null) { - this.author = model.getMetadata().getAuthor(); - this.description = model.getMetadata().getDescription(); - this.unitsPerMeter = model.getMetadata().getUnitsPerMeter(); - this.pivotPoint = new Vector2f(model.getMetadata().getPivotPoint()); - this.userData = new HashMap<>(model.getMetadata().getUserProperties()); - } - - // 序列化部件 - serializeParts(model); - - // 序列化网格 - serializeMeshes(model); - - // 序列化纹理 - serializeTextures(model); - - // 序列化参数 - serializeParameters(model); - - // 序列化动画层 - serializeAnimationLayers(model); - - // 序列化物理系统 - serializePhysics(model); - - // 序列化光源 - serializeLights(model); - - // 序列化姿态 - serializePoses(model); - - lastModifiedTime = System.currentTimeMillis(); - } - - - private void serializeParts(Model2D model) { - parts.clear(); - for (ModelPart part : model.getParts()) { - parts.add(new PartData(part)); - } - } - - private void serializeMeshes(Model2D model) { - meshes.clear(); - - // 使用Set记录已序列化的网格名称,避免重复 - Set serializedMeshNames = new HashSet<>(); - - // 先序列化模型级别的网格 - for (Mesh2D mesh : model.getMeshes()) { - if (!serializedMeshNames.contains(mesh.getName())) { - meshes.add(new MeshData(mesh)); - serializedMeshNames.add(mesh.getName()); - } - } - - // 再序列化部件中的网格(去重) - for (ModelPart part : model.getParts()) { - for (Mesh2D mesh : part.getMeshes()) { - if (!serializedMeshNames.contains(mesh.getName())) { - meshes.add(new MeshData(mesh)); - serializedMeshNames.add(mesh.getName()); - } - } - } - } - - private void serializeTextures(Model2D model) { - textures.clear(); - for (Texture texture : model.getTextures().values()) { - textures.add(new TextureData(texture)); - } - } - - private void serializeParameters(Model2D model) { - parameters.clear(); - for (AnimationParameter param : model.getParameters().values()) { - parameters.add(new ParameterData(param)); - } - } - - private void serializeAnimationLayers(Model2D model) { - animationLayers.clear(); - for (AnimationLayer layer : model.getAnimationLayers()) { - animationLayers.add(new AnimationLayerData(layer)); - } - } - - private void deserializeAnimationLayers(Model2D model) { - List layers = new ArrayList<>(); - for (AnimationLayerData layerData : animationLayers) { - AnimationLayer layer = layerData.toAnimationLayer(); - layers.add(layer); - } - model.setAnimationLayers(layers); - } - - /** - * 反序列化到Model2D对象 - */ - public Model2D deserializeToModel() { - Model2D model = new Model2D(name); - model.setVersion(version); - model.setUuid(uuid); - - // 设置元数据 - ModelMetadata metadata = new ModelMetadata(); - metadata.setAuthor(author); - metadata.setDescription(description); - model.setMetadata(metadata); - - // 先创建所有纹理 - Map textureMap = deserializeTextures(); - - // === 关键修复:将纹理添加到模型中 === - for (Texture texture : textureMap.values()) { - model.addTexture(texture); - } - - Map meshMap = deserializeMeshes(textureMap); - - // 然后创建部件(依赖网格) - Map partMap = deserializeParts(model, meshMap); - - // 最后创建参数 - deserializeParameters(model); - - // 创建动画层 - deserializeAnimationLayers(model); - - // 反序列化物理系统 - deserializePhysics(model, partMap); - - // 反序列化光源 - deserializeLights(model); - - // 反序列化姿态 - deserializePoses(model); - return model; - } - - private void deserializePhysics(Model2D model, Map partMap) { - if (model == null) return; - PhysicsSystem phys = model.getPhysics(); - if (phys == null) return; - - phys.reset(); - phys.setGravity(this.physicsGravityX, this.physicsGravityY); - phys.setAirResistance(this.physicsAirResistance); - phys.setTimeScale(this.physicsTimeScale); - phys.setEnabled(this.physicsEnabled); - - // 安全处理列表,避免 null - List particles = physicsParticles != null ? physicsParticles : new ArrayList<>(); - List springs = physicsSprings != null ? physicsSprings : new ArrayList<>(); - List constraints = physicsConstraints != null ? physicsConstraints : new ArrayList<>(); - List colliders = physicsColliders != null ? physicsColliders : new ArrayList<>(); - - // 创建粒子 - Map idToParticle = new HashMap<>(); - for (ParticleData pd : particles) { - PhysicsSystem.PhysicsParticle p = phys.addParticle(pd.id, new Vector2f(pd.x, pd.y), pd.mass); - p.setRadius(pd.radius); - p.setMovable(pd.movable); - p.setAffectedByGravity(pd.affectedByGravity); - p.setAffectedByWind(pd.affectedByWind); - - if (pd.userPartName != null && partMap != null) { - ModelPart mp = partMap.get(pd.userPartName); - if (mp != null) p.setUserData(mp); - } - idToParticle.put(pd.id, p); - } - - // 创建弹簧 - for (SpringData sd : springs) { - PhysicsSystem.PhysicsParticle a = idToParticle.get(sd.aId); - PhysicsSystem.PhysicsParticle b = idToParticle.get(sd.bId); - if (a != null && b != null) { - PhysicsSystem.PhysicsSpring s = phys.addSpring(sd.id, a, b, sd.restLength, sd.stiffness, sd.damping); - s.setEnabled(sd.enabled); - } - } - - // 创建约束 - for (ConstraintData cd : constraints) { - if ("position".equals(cd.type)) { - PhysicsSystem.PhysicsParticle p = idToParticle.get(cd.particleId); - if (p != null) { - PhysicsSystem.PhysicsConstraint c = phys.addPositionConstraint(p, new Vector2f(cd.targetX, cd.targetY)); - if (c instanceof PhysicsSystem.PositionConstraint) { - ((PhysicsSystem.PositionConstraint) c).setStrength(cd.strength); - c.setEnabled(cd.enabled); - } - } - } else if ("distance".equals(cd.type)) { - PhysicsSystem.PhysicsParticle p = idToParticle.get(cd.particleId); - PhysicsSystem.PhysicsParticle target = idToParticle.get(cd.targetParticleId); - if (p != null && target != null) { - PhysicsSystem.PhysicsConstraint c = phys.addDistanceConstraint(p, target, cd.maxDistance); - c.setEnabled(cd.enabled); - } - } - } - - // 创建碰撞体 - for (ColliderData cd : colliders) { - if ("circle".equals(cd.type)) { - PhysicsSystem.PhysicsCollider coll = phys.addCircleCollider(cd.id, new Vector2f(cd.centerX, cd.centerY), cd.radius); - coll.setEnabled(cd.enabled); - } else if ("rect".equals(cd.type)) { - PhysicsSystem.PhysicsCollider coll = phys.addRectangleCollider(cd.id, new Vector2f(cd.centerX, cd.centerY), cd.width, cd.height); - coll.setEnabled(cd.enabled); - } - } - } - - - /** - * 反序列化部件并返回 name->ModelPart 的映射,供物理系统恢复 userData 使用 - */ - private Map deserializeParts(Model2D model, Map meshMap) { - Map partMap = new HashMap<>(); - - // 1. 创建所有部件对象 - for (PartData partData : parts) { - ModelPart part = partData.toModelPart(meshMap); - partMap.put(part.getName(), part); - - for (Mesh2D mesh : part.getMeshes()) { - if (!model.getMeshes().contains(mesh)) { - model.addMesh(mesh); - } - } - } - - // 2. 建立父子关系 - for (PartData partData : parts) { - if (partData.parentName != null && !partData.parentName.isEmpty()) { - ModelPart child = partMap.get(partData.name); - ModelPart parent = partMap.get(partData.parentName); - if (parent != null && child != null) { - parent.addChild(child); - } - } - } - - // 3. 找到根部件并设置 - ModelPart rootPart = null; - for (PartData partData : parts) { - if (partData.parentName == null || partData.parentName.isEmpty()) { - rootPart = partMap.get(partData.name); - model.setRootPart(rootPart); - break; - } - } - - // 4. 把所有部件加入 model(如果 addPart 是注册用,不会重复) - for (ModelPart part : partMap.values()) { - model.addPart(part); - } - - return partMap; - } - - - private Map deserializeTextures() { - Map textureMap = new HashMap<>(); - - for (TextureData textureData : textures) { - try { - - Texture texture = textureData.toTexture(); - - if (texture != null) { - textureMap.put(texture.getName(), texture); - } - } catch (Exception e) { - logger.error("Error creating texture '{}': {}", textureData.name, e.getMessage()); - e.printStackTrace(); - Texture fallbackTexture = createFallbackTexture(textureData.name, textureData.width, textureData.height); - if (fallbackTexture != null) { - textureMap.put(textureData.name, fallbackTexture); - } - } - } - return textureMap; - } - - /** - * 创建后备纹理 - */ - private Texture createFallbackTexture(String name, int width, int height) { - try { - int color; - if (name.contains("body")) { - color = 0xFFFF0000; - } else if (name.contains("head")) { - color = 0xFF00FF00; - } else if (name.contains("checker")) { - return Texture.createCheckerboard(name + "_fallback", width, height, 16, 0xFFFFFFFF, 0xFF0000FF); - } else { - color = 0xFF0000FF; - } - return Texture.createSolidColor(name + "_fallback", width, height, color); - } catch (Exception e) { - System.err.println("Failed to create fallback texture: " + e.getMessage()); - return null; - } - } - - private Map deserializeMeshes(Map textureMap) { - Map meshMap = new HashMap<>(); - for (MeshData meshData : meshes) { - try { - Mesh2D mesh = meshData.toMesh2D(); - - // 设置纹理 - if (meshData.textureName != null) { - Texture texture = textureMap.get(meshData.textureName); - if (texture != null) { - mesh.setTexture(texture); - } else { - logger.error("Texture not found for mesh '{}': {}", meshData.name, meshData.textureName); - } - } - meshMap.put(mesh.getName(), mesh); - } catch (Exception e) { - logger.error("Error creating mesh '{}': {}", meshData.name, e.getMessage()); - } - } - return meshMap; - } - - private void deserializeParameters(Model2D model) { - for (ParameterData paramData : parameters) { - AnimationParameter param = paramData.toAnimationParameter(); - model.addParameter(param); - } - } - - // ==================== 文件操作 ==================== - - /** - * 保存到文件 - */ - public void saveToFile(String filePath) throws IOException { - saveToFile(new File(filePath)); - } - - public void saveToFile(File file) throws IOException { - // 确保目录存在 - File parentDir = file.getParentFile(); - if (parentDir != null && !parentDir.exists()) { - parentDir.mkdirs(); - } - - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) { - oos.writeObject(this); - } - } - - /** - * 保存为压缩文件 - */ - public void saveToCompressedFile(String filePath) throws IOException { - saveToCompressedFile(new File(filePath)); - } - - public void saveToCompressedFile(File file) throws IOException { - // 确保目录存在 - File parentDir = file.getParentFile(); - if (parentDir != null && !parentDir.exists()) { - parentDir.mkdirs(); - } - - try (ObjectOutputStream oos = new ObjectOutputStream( - new GZIPOutputStream(new FileOutputStream(file)))) { - oos.writeObject(this); - } - } - - /** - * 从文件加载 - */ - public static ModelData loadFromFile(String filePath) throws IOException, ClassNotFoundException { - return loadFromFile(new File(filePath)); - } - - public static ModelData loadFromFile(File file) throws IOException, ClassNotFoundException { - if (!file.exists()) { - throw new FileNotFoundException("Model file not found: " + file.getAbsolutePath()); - } - - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { - return (ModelData) ois.readObject(); - } - } - - /** - * 从压缩文件加载 - */ - public static ModelData loadFromCompressedFile(String filePath) throws IOException, ClassNotFoundException { - return loadFromCompressedFile(new File(filePath)); - } - - public static ModelData loadFromCompressedFile(File file) throws IOException, ClassNotFoundException { - if (!file.exists()) { - throw new FileNotFoundException("Compressed model file not found: " + file.getAbsolutePath()); - } - - try (ObjectInputStream ois = new ObjectInputStream( - new GZIPInputStream(new FileInputStream(file)))) { - return (ModelData) ois.readObject(); - } - } - - // ==================== 数据验证 ==================== - - /** - * 验证模型数据的完整性 - */ - public boolean validate() { - if (name == null || name.trim().isEmpty()) { - return false; - } - - if (uuid == null) { - return false; - } - - // 检查所有部件引用有效的网格 - for (PartData part : parts) { - for (String meshName : part.meshNames) { - if (!meshExists(meshName)) { - return false; - } - } - } - - return true; - } - - private boolean meshExists(String meshName) { - return meshes.stream().anyMatch(mesh -> mesh.name.equals(meshName)); - } - - /** - * 获取验证错误信息 - */ - public List getValidationErrors() { - List errors = new ArrayList<>(); - - if (name == null || name.trim().isEmpty()) { - errors.add("Model name is required"); - } - - if (uuid == null) { - errors.add("Model UUID is required"); - } - - // 检查网格引用 - for (PartData part : parts) { - for (String meshName : part.meshNames) { - if (!meshExists(meshName)) { - errors.add("Part '" + part.name + "' references non-existent mesh: " + meshName); - } - } - } - - return errors; - } - - // ==================== 工具方法 ==================== - - /** - * 创建深拷贝 - */ - public ModelData copy() { - ModelData copy = new ModelData(name + "_copy"); - copy.version = this.version; - copy.uuid = UUID.randomUUID(); - copy.author = this.author; - copy.description = this.description; - copy.creationTime = System.currentTimeMillis(); - copy.lastModifiedTime = copy.creationTime; - - // 拷贝核心数据 - for (PartData part : this.parts) copy.parts.add(part.copy()); - for (MeshData mesh : this.meshes) copy.meshes.add(mesh.copy()); - for (TextureData tex : this.textures) copy.textures.add(tex.copy()); - for (ParameterData param : this.parameters) copy.parameters.add(param.copy()); - for (AnimationLayerData layer : this.animationLayers) copy.animationLayers.add(layer.copy()); - for (LightSourceData light : this.lights) copy.lights.add(light.copy()); - - // 拷贝物理系统 - for (ParticleData p : this.physicsParticles) copy.physicsParticles.add(p.copy()); - for (SpringData s : this.physicsSprings) copy.physicsSprings.add(s.copy()); - for (ConstraintData c : this.physicsConstraints) copy.physicsConstraints.add(c.copy()); - for (ColliderData c : this.physicsColliders) copy.physicsColliders.add(c.copy()); - for (PoseData pose : this.poses) copy.poses.add(pose.copy()); - copy.currentPoseName = this.currentPoseName; - - copy.physicsGravityX = this.physicsGravityX; - copy.physicsGravityY = this.physicsGravityY; - copy.physicsAirResistance = this.physicsAirResistance; - copy.physicsTimeScale = this.physicsTimeScale; - copy.physicsEnabled = this.physicsEnabled; - - copy.pivotPoint = new Vector2f(this.pivotPoint); - copy.unitsPerMeter = this.unitsPerMeter; - copy.userData = new HashMap<>(this.userData); - - return copy; - } - - /** - * 合并另一个模型数据 - */ - public void merge(ModelData other) { - if (other == null) return; - - // 合并网格(避免名称冲突) - for (MeshData mesh : other.meshes) { - String originalName = mesh.name; - int counter = 1; - while (meshExists(mesh.name)) { - mesh.name = originalName + "_" + counter++; - } - this.meshes.add(mesh); - } - - // 合并部件 - for (PartData part : other.parts) { - String originalName = part.name; - int counter = 1; - while (partExists(part.name)) { - part.name = originalName + "_" + counter++; - } - this.parts.add(part); - } - - // 合并参数 - for (ParameterData param : other.parameters) { - this.parameters.add(param.copy()); - } - - // 合并纹理 - for (TextureData texture : other.textures) { - String originalName = texture.name; - int counter = 1; - while (textureExists(texture.name)) { - texture.name = originalName + "_" + counter++; - } - this.textures.add(texture); - } - - // 合并姿态 - for (PoseData pose : other.poses) { - String originalName = pose.name; - int counter = 1; - while (poseExists(pose.name)) { - pose.name = originalName + "_" + counter++; - } - this.poses.add(pose); - } - - - lastModifiedTime = System.currentTimeMillis(); - } - - private boolean poseExists(String poseName) { - return poses.stream().anyMatch(pose -> pose.name.equals(poseName)); - } - - private boolean partExists(String partName) { - return parts.stream().anyMatch(part -> part.name.equals(partName)); - } - - private boolean textureExists(String textureName) { - return textures.stream().anyMatch(texture -> texture.name.equals(textureName)); - } - - // ==================== 内部数据类 ==================== - - - // ---------- 物理数据的序列化类 ---------- - public static class ParticleData implements Serializable { - public String id; - public float x, y; - public float mass; - public float radius; - public boolean movable; - public boolean affectedByGravity; - public boolean affectedByWind; - public String userPartName; - - public ParticleData copy() { - ParticleData copy = new ParticleData(); - copy.id = this.id; - copy.x = this.x; - copy.y = this.y; - copy.mass = this.mass; - copy.radius = this.radius; - copy.movable = this.movable; - copy.affectedByGravity = this.affectedByGravity; - copy.affectedByWind = this.affectedByWind; - copy.userPartName = this.userPartName; - return copy; - } - } - - public static class SpringData implements Serializable { - public String id; - public String aId; - public String bId; - public float restLength; - public float stiffness; - public float damping; - public boolean enabled; - - public SpringData copy() { - SpringData copy = new SpringData(); - copy.id = this.id; - copy.aId = this.aId; - copy.bId = this.bId; - copy.restLength = this.restLength; - copy.stiffness = this.stiffness; - copy.damping = this.damping; - copy.enabled = this.enabled; - return copy; - } - } - - public static class ColliderData implements Serializable { - public String id; - public String type; // "circle" or "rect" - public float centerX, centerY; - public float radius; - public float width, height; - public boolean enabled; - - public ColliderData copy() { - ColliderData copy = new ColliderData(); - copy.id = this.id; - copy.type = this.type; - copy.centerX = this.centerX; - copy.centerY = this.centerY; - copy.radius = this.radius; - copy.width = this.width; - copy.height = this.height; - copy.enabled = this.enabled; - return copy; - } - } - - public static class ConstraintData implements Serializable { - public String type; // "position" or "distance" - public String particleId; - // position target - public float targetX, targetY; - public float strength; - // distance target - public String targetParticleId; - public float maxDistance; - public boolean enabled; - - public ConstraintData copy() { - ConstraintData copy = new ConstraintData(); - copy.type = this.type; - copy.particleId = this.particleId; - copy.targetX = this.targetX; - copy.targetY = this.targetY; - copy.strength = this.strength; - copy.targetParticleId = this.targetParticleId; - copy.maxDistance = this.maxDistance; - copy.enabled = this.enabled; - return copy; - } - } - - /** - * 动画数据 - */ - public static class AnimationData implements Serializable { - private static final long serialVersionUID = 1L; - - public String name; - public float duration; - public boolean looping; - public Map> tracks; - - public AnimationData() { - this.tracks = new HashMap<>(); - } - - public AnimationData copy() { - AnimationData copy = new AnimationData(); - copy.name = this.name; - copy.duration = this.duration; - copy.looping = this.looping; - copy.tracks = new HashMap<>(this.tracks); - return copy; - } - } - - /** - * 关键帧数据 - */ - public static class KeyframeData implements Serializable { - private static final long serialVersionUID = 1L; - - public float time; - public float value; - public String interpolation; - - public KeyframeData copy() { - KeyframeData copy = new KeyframeData(); - copy.time = this.time; - copy.value = this.value; - copy.interpolation = this.interpolation; - return copy; - } - } - - // ==================== Getter/Setter ==================== - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } - - public UUID getUuid() { - return uuid; - } - - public void setUuid(UUID uuid) { - this.uuid = uuid; - } - - public String getAuthor() { - return author; - } - - public void setAuthor(String author) { - this.author = author; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public long getCreationTime() { - return creationTime; - } - - public void setCreationTime(long creationTime) { - this.creationTime = creationTime; - } - - public long getLastModifiedTime() { - return lastModifiedTime; - } - - public void setLastModifiedTime(long lastModifiedTime) { - this.lastModifiedTime = lastModifiedTime; - } - - public List getParts() { - return parts; - } - - public void setParts(List parts) { - this.parts = parts; - } - - public List getMeshes() { - return meshes; - } - - public void setMeshes(List meshes) { - this.meshes = meshes; - } - - public List getTextures() { - return textures; - } - - public void setTextures(List textures) { - this.textures = textures; - } - - public List getParameters() { - return parameters; - } - - public void setParameters(List parameters) { - this.parameters = parameters; - } - - public List getAnimations() { - return animations; - } - - public void setAnimations(List animations) { - this.animations = animations; - } - - public Vector2f getPivotPoint() { - return pivotPoint; - } - - public void setPivotPoint(Vector2f pivotPoint) { - this.pivotPoint = pivotPoint; - } - - public float getUnitsPerMeter() { - return unitsPerMeter; - } - - public void setUnitsPerMeter(float unitsPerMeter) { - this.unitsPerMeter = unitsPerMeter; - } - - public Map getUserData() { - return userData; - } - - public void setUserData(Map userData) { - this.userData = userData; - } - - public List getAnimationLayers() { - return animationLayers; - } - - public void setAnimationLayers(List animationLayers) { - this.animationLayers = animationLayers; - } - - public List getPoses() { - return poses; - } - - public void setPoses(List poses) { - this.poses = poses; - } - - public String getCurrentPoseName() { - return currentPoseName; - } - - public void setCurrentPoseName(String currentPoseName) { - this.currentPoseName = currentPoseName; - } - - // ==================== Object方法 ==================== - - @Override - public String toString() { - return "ModelData{" + - "name='" + name + '\'' + - ", version='" + version + '\'' + - ", parts=" + parts.size() + - ", meshes=" + meshes.size() + - ", parameters=" + parameters.size() + - ", animations=" + animations.size() + - '}'; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelMetadata.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelMetadata.java deleted file mode 100644 index 584cf71..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelMetadata.java +++ /dev/null @@ -1,627 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.data; - -import org.joml.Vector2f; - -import java.io.Serializable; -import java.util.*; - -/** - * 模型元数据类 - * 用于存储模型的描述性信息、创建信息、版本信息等 - * - * @author tzdwindows 7 - */ -public class ModelMetadata implements Serializable, Cloneable { - private static final long serialVersionUID = 1L; - - // ==================== 基础信息 ==================== - private String name; - private String version; - private UUID uuid; - private String description; - - // ==================== 创建信息 ==================== - private String author; - private String creator; - private String copyright; - private String license; - private long creationTime; - private long lastModifiedTime; - - // ==================== 技术信息 ==================== - private String fileFormatVersion; - private int vertexCount; - private int polygonCount; - private int textureCount; - private int parameterCount; - private int partCount; - - // ==================== 渲染设置 ==================== - private Vector2f pivotPoint; - private float unitsPerMeter; - private boolean visibleInScene; - - // ==================== 用户数据 ==================== - private Map userProperties; - private List tags; - - // ==================== 构造器 ==================== - - public ModelMetadata() { - this("unnamed", "1.0.0"); - } - - public ModelMetadata(String name) { - this(name, "1.0.0"); - } - - public ModelMetadata(String name, String version) { - this.name = name; - this.version = version; - this.uuid = UUID.randomUUID(); - this.creationTime = System.currentTimeMillis(); - this.lastModifiedTime = creationTime; - - // 初始化默认值 - this.pivotPoint = new Vector2f(); - this.unitsPerMeter = 100.0f; - this.visibleInScene = true; - - this.userProperties = new HashMap<>(); - this.tags = new ArrayList<>(); - } - - // ==================== 基础信息方法 ==================== - - /** - * 验证元数据的基本完整性 - */ - public boolean isValid() { - return name != null && !name.trim().isEmpty() && - version != null && !version.trim().isEmpty() && - uuid != null; - } - - /** - * 获取模型的显示名称 - */ - public String getDisplayName() { - if (name != null && !name.trim().isEmpty()) { - return name; - } - return "Unnamed Model"; - } - - /** - * 获取完整的版本信息 - */ - public String getFullVersion() { - if (fileFormatVersion != null) { - return version + " (Format: " + fileFormatVersion + ")"; - } - return version; - } - - // ==================== 时间管理 ==================== - - /** - * 标记为已修改 - */ - public void markModified() { - this.lastModifiedTime = System.currentTimeMillis(); - } - - /** - * 获取模型年龄(以天为单位) - */ - public long getAgeInDays() { - long currentTime = System.currentTimeMillis(); - long ageMillis = currentTime - creationTime; - return ageMillis / (1000 * 60 * 60 * 24); - } - - /** - * 获取最后修改后的时间(以小时为单位) - */ - public long getHoursSinceLastModified() { - long currentTime = System.currentTimeMillis(); - long diffMillis = currentTime - lastModifiedTime; - return diffMillis / (1000 * 60 * 60); - } - - // ==================== 标签管理 ==================== - - /** - * 添加标签 - */ - public void addTag(String tag) { - if (tag != null && !tag.trim().isEmpty() && !tags.contains(tag)) { - tags.add(tag); - markModified(); - } - } - - /** - * 移除标签 - */ - public boolean removeTag(String tag) { - boolean removed = tags.remove(tag); - if (removed) { - markModified(); - } - return removed; - } - - /** - * 检查是否包含标签 - */ - public boolean hasTag(String tag) { - return tags.contains(tag); - } - - /** - * 检查是否包含任何指定的标签 - */ - public boolean hasAnyTag(String... searchTags) { - for (String tag : searchTags) { - if (tags.contains(tag)) { - return true; - } - } - return false; - } - - /** - * 检查是否包含所有指定的标签 - */ - public boolean hasAllTags(String... searchTags) { - for (String tag : searchTags) { - if (!tags.contains(tag)) { - return false; - } - } - return true; - } - - // ==================== 用户属性管理 ==================== - - /** - * 设置用户属性 - */ - public void setProperty(String key, String value) { - if (key != null && !key.trim().isEmpty()) { - userProperties.put(key, value); - markModified(); - } - } - - /** - * 获取用户属性 - */ - public String getProperty(String key) { - return userProperties.get(key); - } - - /** - * 获取用户属性,如果不存在则返回默认值 - */ - public String getProperty(String key, String defaultValue) { - return userProperties.getOrDefault(key, defaultValue); - } - - /** - * 移除用户属性 - */ - public String removeProperty(String key) { - String removed = userProperties.remove(key); - if (removed != null) { - markModified(); - } - return removed; - } - - /** - * 检查是否存在属性 - */ - public boolean hasProperty(String key) { - return userProperties.containsKey(key); - } - - // ==================== 统计信息方法 ==================== - - /** - * 更新统计信息 - */ - public void updateStatistics(int vertexCount, int polygonCount, int textureCount, - int parameterCount, int partCount) { - this.vertexCount = vertexCount; - this.polygonCount = polygonCount; - this.textureCount = textureCount; - this.parameterCount = parameterCount; - this.partCount = partCount; - markModified(); - } - - /** - * 获取模型复杂度评级 - */ - public ComplexityRating getComplexityRating() { - int totalComplexity = vertexCount + (polygonCount * 10) + - (textureCount * 100) + (parameterCount * 5) + - (partCount * 20); - - if (totalComplexity < 1000) { - return ComplexityRating.VERY_SIMPLE; - } else if (totalComplexity < 5000) { - return ComplexityRating.SIMPLE; - } else if (totalComplexity < 20000) { - return ComplexityRating.MEDIUM; - } else if (totalComplexity < 50000) { - return ComplexityRating.COMPLEX; - } else { - return ComplexityRating.VERY_COMPLEX; - } - } - - /** - * 获取估计的文件大小(字节) - */ - public long getEstimatedFileSize() { - // 粗略估算:顶点数据 + 纹理数据 + 其他开销 - long vertexDataSize = (long) vertexCount * 8 * 2; // 每个顶点8字节(float x,y),2份(原始+变形) - long textureDataSize = (long) textureCount * 1024 * 1024; // 假设每个纹理1MB - long otherDataSize = parameterCount * 16L + partCount * 64L + polygonCount * 12L; - - return vertexDataSize + textureDataSize + otherDataSize + 1024; // +1KB元数据 - } - - // ==================== 工具方法 ==================== - - /** - * 创建深拷贝 - */ - @Override - public ModelMetadata clone() { - try { - ModelMetadata clone = (ModelMetadata) super.clone(); - - // 深拷贝可变对象 - clone.pivotPoint = new Vector2f(this.pivotPoint); - clone.userProperties = new HashMap<>(this.userProperties); - clone.tags = new ArrayList<>(this.tags); - - return clone; - } catch (CloneNotSupportedException e) { - throw new AssertionError("Clone should be supported", e); - } - } - - /** - * 创建带有新名称的拷贝 - */ - public ModelMetadata copyWithName(String newName) { - ModelMetadata copy = clone(); - copy.name = newName; - copy.uuid = UUID.randomUUID(); - copy.creationTime = System.currentTimeMillis(); - copy.lastModifiedTime = copy.creationTime; - return copy; - } - - /** - * 合并另一个元数据(主要用于模型合并) - */ - public void merge(ModelMetadata other) { - if (other == null) return; - - // 合并描述 - if (this.description == null || this.description.isEmpty()) { - this.description = other.description; - } else if (other.description != null && !other.description.isEmpty()) { - this.description += "; " + other.description; - } - - // 合并作者信息 - if (this.author == null || this.author.isEmpty()) { - this.author = other.author; - } else if (other.author != null && !other.author.isEmpty()) { - this.author += ", " + other.author; - } - - // 合并标签(去重) - for (String tag : other.tags) { - if (!this.tags.contains(tag)) { - this.tags.add(tag); - } - } - - // 合并用户属性(不覆盖现有属性) - for (Map.Entry entry : other.userProperties.entrySet()) { - if (!this.userProperties.containsKey(entry.getKey())) { - this.userProperties.put(entry.getKey(), entry.getValue()); - } - } - - markModified(); - } - - /** - * 转换为简化的信息映射 - */ - public Map toInfoMap() { - Map info = new LinkedHashMap<>(); - - info.put("name", name); - info.put("version", version); - info.put("uuid", uuid.toString()); - info.put("author", author != null ? author : "Unknown"); - info.put("description", description != null ? description : "No description"); - info.put("creationTime", new Date(creationTime)); - info.put("lastModifiedTime", new Date(lastModifiedTime)); - info.put("vertexCount", vertexCount); - info.put("polygonCount", polygonCount); - info.put("textureCount", textureCount); - info.put("parameterCount", parameterCount); - info.put("partCount", partCount); - info.put("complexity", getComplexityRating().toString()); - info.put("tags", String.join(", ", tags)); - - return info; - } - - // ==================== 枚举和内部类 ==================== - - /** - * 模型复杂度评级 - */ - public enum ComplexityRating { - VERY_SIMPLE("非常简单", "适合初学者"), - SIMPLE("简单", "基础模型"), - MEDIUM("中等", "标准模型"), - COMPLEX("复杂", "高级模型"), - VERY_COMPLEX("非常复杂", "专业级模型"); - - private final String displayName; - private final String description; - - ComplexityRating(String displayName, String description) { - this.displayName = displayName; - this.description = description; - } - - public String getDisplayName() { - return displayName; - } - - public String getDescription() { - return description; - } - - @Override - public String toString() { - return displayName + " (" + description + ")"; - } - } - - // ==================== Getter/Setter ==================== - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - markModified(); - } - - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - markModified(); - } - - public UUID getUuid() { - return uuid; - } - - public void setUuid(UUID uuid) { - this.uuid = uuid; - markModified(); - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - markModified(); - } - - public String getAuthor() { - return author; - } - - public void setAuthor(String author) { - this.author = author; - markModified(); - } - - public String getCreator() { - return creator; - } - - public void setCreator(String creator) { - this.creator = creator; - markModified(); - } - - public String getCopyright() { - return copyright; - } - - public void setCopyright(String copyright) { - this.copyright = copyright; - markModified(); - } - - public String getLicense() { - return license; - } - - public void setLicense(String license) { - this.license = license; - markModified(); - } - - public long getCreationTime() { - return creationTime; - } - - public void setCreationTime(long creationTime) { - this.creationTime = creationTime; - // 不标记修改,因为创建时间通常不应该改变 - } - - public long getLastModifiedTime() { - return lastModifiedTime; - } - - public void setLastModifiedTime(long lastModifiedTime) { - this.lastModifiedTime = lastModifiedTime; - // 不标记修改,避免循环调用 - } - - public String getFileFormatVersion() { - return fileFormatVersion; - } - - public void setFileFormatVersion(String fileFormatVersion) { - this.fileFormatVersion = fileFormatVersion; - markModified(); - } - - public int getVertexCount() { - return vertexCount; - } - - public void setVertexCount(int vertexCount) { - this.vertexCount = vertexCount; - markModified(); - } - - public int getPolygonCount() { - return polygonCount; - } - - public void setPolygonCount(int polygonCount) { - this.polygonCount = polygonCount; - markModified(); - } - - public int getTextureCount() { - return textureCount; - } - - public void setTextureCount(int textureCount) { - this.textureCount = textureCount; - markModified(); - } - - public int getParameterCount() { - return parameterCount; - } - - public void setParameterCount(int parameterCount) { - this.parameterCount = parameterCount; - markModified(); - } - - public int getPartCount() { - return partCount; - } - - public void setPartCount(int partCount) { - this.partCount = partCount; - markModified(); - } - - public Vector2f getPivotPoint() { - return pivotPoint; - } - - public void setPivotPoint(Vector2f pivotPoint) { - this.pivotPoint = pivotPoint; - markModified(); - } - - public float getUnitsPerMeter() { - return unitsPerMeter; - } - - public void setUnitsPerMeter(float unitsPerMeter) { - this.unitsPerMeter = unitsPerMeter; - markModified(); - } - - public boolean isVisibleInScene() { - return visibleInScene; - } - - public void setVisibleInScene(boolean visibleInScene) { - this.visibleInScene = visibleInScene; - markModified(); - } - - public Map getUserProperties() { - return Collections.unmodifiableMap(userProperties); - } - - public void setUserProperties(Map userProperties) { - this.userProperties = new HashMap<>(userProperties); - markModified(); - } - - public List getTags() { - return Collections.unmodifiableList(tags); - } - - public void setTags(List tags) { - this.tags = new ArrayList<>(tags); - markModified(); - } - - // ==================== Object 方法 ==================== - - @Override - public String toString() { - return "ModelMetadata{" + - "name='" + name + '\'' + - ", version='" + version + '\'' + - ", uuid=" + uuid + - ", author='" + author + '\'' + - ", vertexCount=" + vertexCount + - ", polygonCount=" + polygonCount + - ", tags=" + tags.size() + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ModelMetadata that = (ModelMetadata) o; - return creationTime == that.creationTime && - Objects.equals(uuid, that.uuid) && - Objects.equals(name, that.name) && - Objects.equals(version, that.version); - } - - @Override - public int hashCode() { - return Objects.hash(name, version, uuid, creationTime); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/ParameterData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/ParameterData.java deleted file mode 100644 index 0117e90..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/ParameterData.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.data; - -import com.chuangzhou.vivid2D.render.model.AnimationParameter; - -import java.io.Serial; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.SortedSet; - -public class ParameterData implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - - public String id; - public float value; - public float defaultValue; - public float minValue; - public float maxValue; - public boolean changed; - - public List keyframes; - - public ParameterData() { - this.keyframes = new ArrayList<>(); - } - - public ParameterData(AnimationParameter param) { - this(); - this.id = param.getId(); - this.value = param.getValue(); - this.defaultValue = param.getDefaultValue(); - this.minValue = param.getMinValue(); - this.maxValue = param.getMaxValue(); - this.changed = param.hasChanged(); - - SortedSet frames = param.getKeyframes(); - if (frames != null && !frames.isEmpty()) { - this.keyframes.addAll(frames); - } - } - - public AnimationParameter toAnimationParameter() { - AnimationParameter param = new AnimationParameter(id, minValue, maxValue, defaultValue); - param.setValue(value); - - // 恢复 changed 状态 - if (changed) { - // 由于 setValue 会自动设置 changed 标志,我们需要特殊处理 - // 这里使用反射或者额外的方法来直接设置 changed 状态 - try { - java.lang.reflect.Field changedField = param.getClass().getDeclaredField("changed"); - changedField.setAccessible(true); - changedField.set(param, this.changed); - } catch (Exception e) { - // 如果反射失败,至少保证值是正确的 - System.err.println("无法恢复 changed 状态: " + e.getMessage()); - } - } - - // 恢复关键帧 - if (keyframes != null) { - for (Float frameValue : keyframes) { - param.addKeyframe(frameValue); - } - } - - return param; - } - - public ParameterData copy() { - ParameterData copy = new ParameterData(); - copy.id = this.id; - copy.value = this.value; - copy.defaultValue = this.defaultValue; - copy.minValue = this.minValue; - copy.maxValue = this.maxValue; - copy.changed = this.changed; - copy.keyframes = new ArrayList<>(this.keyframes); - return copy; - } - - @Override - public String toString() { - return String.format( - "ParameterData[ID=%s, Value=%.3f, Range=[%.3f, %.3f], Default=%.3f, Changed=%s, Keyframes=%s]", - id, value, minValue, maxValue, defaultValue, changed, keyframes - ); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartData.java deleted file mode 100644 index 9b1ece6..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartData.java +++ /dev/null @@ -1,350 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.data; - -import com.chuangzhou.vivid2D.render.model.AnimationParameter; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import org.joml.Vector2f; - -import java.io.Serial; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * @author tzdwindows 7 - */ -public class PartData implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - - public String name; - public String parentName; - public Vector2f position; - public float rotation; - public Vector2f scale; - public boolean visible; - public float opacity; - public List meshNames; - public Map userData; - public List deformers; - public List liquifyStrokes; - public List parameters; - - public PartData() { - this.position = new Vector2f(); - this.rotation = 0.0f; - this.scale = new Vector2f(1.0f, 1.0f); - this.visible = true; - this.opacity = 1.0f; - this.meshNames = new ArrayList<>(); - this.userData = new HashMap<>(); - this.deformers = new ArrayList<>(); - this.liquifyStrokes = new ArrayList<>(); - this.parameters = new ArrayList<>(); - } - - public PartData(ModelPart part) { - this(); - this.name = part.getName(); - this.position = part.getPosition(); - this.rotation = part.getRotation(); - this.scale = part.getScale(); - this.visible = part.isVisible(); - this.opacity = part.getOpacity(); - - // 收集网格名称 - for (Mesh2D mesh : part.getMeshes()) { - this.meshNames.add(mesh.getName()); - } - - // 收集变形器(序列化每个变形器为键值表) - for (Deformer d : part.getDeformers()) { - try { - DeformerData dd = new DeformerData(); - dd.type = d.getClass().getName(); - dd.name = d.getName(); - Map map = new HashMap<>(); - d.serialization(map); // 让变形器把自己的状态写入 map - dd.properties = map; - this.deformers.add(dd); - } catch (Exception e) { - // 忽略单个变形器序列化错误,避免整个保存失败 - e.printStackTrace(); - } - } - - // 尝试通过反射收集液化笔划数据(兼容性:如果 ModelPart 没有对应 API,则跳过) - try { - // 期望的方法签名: public List getLiquifyStrokes() - java.lang.reflect.Method m = part.getClass().getMethod("getLiquifyStrokes"); - Object strokesObj = m.invoke(part); - if (strokesObj instanceof List strokes) { - for (Object s : strokes) { - // 支持两种情况:存储为自定义类型(有 getMode/getRadius/getStrength/getIterations/getPoints 方法) - // 或者直接存储为通用 Map/POJO。我们做宽松处理:通过反射尽可能读取常见字段。 - LiquifyStrokeData strokeData = new LiquifyStrokeData(); - - try { - java.lang.reflect.Method gm = s.getClass().getMethod("getMode"); - Object modeObj = gm.invoke(s); - if (modeObj != null) strokeData.mode = modeObj.toString(); - } catch (NoSuchMethodException ignored) { - } - - try { - java.lang.reflect.Method gr = s.getClass().getMethod("getRadius"); - Object r = gr.invoke(s); - if (r instanceof Number) strokeData.radius = ((Number) r).floatValue(); - } catch (NoSuchMethodException ignored) { - } - - try { - java.lang.reflect.Method gs = s.getClass().getMethod("getStrength"); - Object st = gs.invoke(s); - if (st instanceof Number) strokeData.strength = ((Number) st).floatValue(); - } catch (NoSuchMethodException ignored) { - } - - try { - java.lang.reflect.Method gi = s.getClass().getMethod("getIterations"); - Object it = gi.invoke(s); - if (it instanceof Number) strokeData.iterations = ((Number) it).intValue(); - } catch (NoSuchMethodException ignored) { - } - - // 读取点列表 - try { - java.lang.reflect.Method gp = s.getClass().getMethod("getPoints"); - Object ptsObj = gp.invoke(s); - if (ptsObj instanceof List pts) { - for (Object p : pts) { - // 支持 Vector2f 或自定义点类型(有 getX/getY/getPressure) - LiquifyPointData pd = new LiquifyPointData(); - if (p instanceof Vector2f v) { - pd.x = v.x; - pd.y = v.y; - pd.pressure = 1.0f; - } else { - try { - java.lang.reflect.Method px = p.getClass().getMethod("getX"); - java.lang.reflect.Method py = p.getClass().getMethod("getY"); - Object ox = px.invoke(p); - Object oy = py.invoke(p); - if (ox instanceof Number && oy instanceof Number) { - pd.x = ((Number) ox).floatValue(); - pd.y = ((Number) oy).floatValue(); - } - try { - java.lang.reflect.Method pp = p.getClass().getMethod("getPressure"); - Object op = pp.invoke(p); - if (op instanceof Number) pd.pressure = ((Number) op).floatValue(); - } catch (NoSuchMethodException ignored2) { - pd.pressure = 1.0f; - } - } catch (NoSuchMethodException ex) { - // 最后尝试 Map 形式(键 x,y) - if (p instanceof Map mapP) { - Object ox = mapP.get("x"); - Object oy = mapP.get("y"); - if (ox instanceof Number && oy instanceof Number) { - pd.x = ((Number) ox).floatValue(); - pd.y = ((Number) oy).floatValue(); - } - Object op = mapP.get("pressure"); - if (op instanceof Number) pd.pressure = ((Number) op).floatValue(); - } - } - } - strokeData.points.add(pd); - } - } - } catch (NoSuchMethodException ignored) { - } - - // 如果没有 mode,则用默认 PUSH - if (strokeData.mode == null) strokeData.mode = ModelPart.LiquifyMode.PUSH.name(); - - this.liquifyStrokes.add(strokeData); - } - } - } catch (NoSuchMethodException ignored) { - // ModelPart 没有 getLiquifyStrokes 方法,跳过(向后兼容) - } catch (Exception e) { - e.printStackTrace(); - } - - // 设置父级名称 - if (part.getParent() != null) { - this.parentName = part.getParent().getName(); - } - - Map partParams = part.getParameters(); - if (partParams != null && !partParams.isEmpty()) { - for (AnimationParameter param : partParams.values()) { - try { - ParameterData paramData = new ParameterData(param); - this.parameters.add(paramData); - } catch (Exception e) { - System.err.println("序列化参数失败: " + param.getId()); - e.printStackTrace(); - } - } - } - } - - public ModelPart toModelPart(Map meshMap) { - ModelPart part = new ModelPart(name); - part.setPosition(position); - part.setRotation(rotation); - part.setScale(scale); - part.setVisible(visible); - part.setOpacity(opacity); - for (String meshName : meshNames) { - Mesh2D mesh = meshMap.get(meshName); - if (mesh != null) { - part.addMesh(mesh); - } - } - if (deformers != null) { - for (DeformerData dd : deformers) { - try { - String className = dd.type; - Class clazz = Class.forName(className); - if (Deformer.class.isAssignableFrom(clazz)) { - Deformer deformer = (Deformer) clazz - .getConstructor(String.class) - .newInstance(dd.name); - try { - deformer.deserialize(dd.properties != null ? dd.properties : new HashMap<>()); - } catch (Exception ex) { - ex.printStackTrace(); - } - - part.addDeformer(deformer); - } else { - System.err.println("跳过无效的变形器类型: " + className); - } - - } catch (Exception e) { - System.err.println("反序列化变形器失败: " + dd.type); - e.printStackTrace(); - } - } - } - if (parameters != null) { - for (ParameterData paramData : parameters) { - try { - AnimationParameter param = paramData.toAnimationParameter(); - part.addParameter(param); - } catch (Exception e) { - System.err.println("反序列化参数失败: " + paramData.id); - e.printStackTrace(); - } - } - } - if (liquifyStrokes != null && !liquifyStrokes.isEmpty()) { - for (LiquifyStrokeData stroke : liquifyStrokes) { - ModelPart.LiquifyMode modeEnum = ModelPart.LiquifyMode.PUSH; - try { - modeEnum = ModelPart.LiquifyMode.valueOf(stroke.mode); - } catch (Exception ignored) { - } - if (stroke.points != null) { - for (LiquifyPointData p : stroke.points) { - try { - part.applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, modeEnum, stroke.iterations, true); - } catch (Exception e) { - } - } - } - } - } - - return part; - } - - public PartData copy() { - PartData copy = new PartData(); - copy.name = this.name; - copy.parentName = this.parentName; - copy.position = new Vector2f(this.position); - copy.rotation = this.rotation; - copy.scale = new Vector2f(this.scale); - copy.visible = this.visible; - copy.opacity = this.opacity; - copy.meshNames = new ArrayList<>(this.meshNames); - copy.userData = new HashMap<>(this.userData); - copy.deformers = new ArrayList<>(); - if (this.deformers != null) { - for (DeformerData d : this.deformers) { - DeformerData cd = new DeformerData(); - cd.type = d.type; - cd.name = d.name; - cd.properties = (d.properties != null) ? new HashMap<>(d.properties) : new HashMap<>(); - copy.deformers.add(cd); - } - } - copy.liquifyStrokes = new ArrayList<>(); - if (this.liquifyStrokes != null) { - for (LiquifyStrokeData s : this.liquifyStrokes) { - LiquifyStrokeData cs = new LiquifyStrokeData(); - cs.mode = s.mode; - cs.radius = s.radius; - cs.strength = s.strength; - cs.iterations = s.iterations; - cs.points = new ArrayList<>(); - if (s.points != null) { - for (LiquifyPointData p : s.points) { - LiquifyPointData cp = new LiquifyPointData(); - cp.x = p.x; - cp.y = p.y; - cp.pressure = p.pressure; - cs.points.add(cp); - } - } - copy.liquifyStrokes.add(cs); - } - } - copy.parameters = new ArrayList<>(); - if (this.parameters != null) { - for (ParameterData p : this.parameters) { - copy.parameters.add(p.copy()); - } - } - return copy; - } - - /** - * 内部类:序列化变形器数据结构 - */ - public static class DeformerData implements Serializable { - private static final long serialVersionUID = 1L; - public String type; // 例如 "VertexDeformer" - public String name; - public Map properties; // 由 Deformer.serialization 填充 - } - - /** - * 内部类:液化笔划数据(Serializable) - * 每个笔划有若干点以及笔划级别参数(mode/radius/strength/iterations) - */ - public static class LiquifyStrokeData implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - public String mode = ModelPart.LiquifyMode.PUSH.name(); - public float radius = 50.0f; - public float strength = 0.5f; - public int iterations = 1; - public List points = new ArrayList<>(); - } - - public static class LiquifyPointData implements Serializable { - private static final long serialVersionUID = 1L; - public float x; - public float y; - public float pressure = 1.0f; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartPoseData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartPoseData.java deleted file mode 100644 index a9d3ff2..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartPoseData.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.data; - -import com.chuangzhou.vivid2D.render.model.util.ModelPose; - -import java.io.Serializable; - -/** - * 部件姿态数据序列化类 - * - * @author tzdwindows 7 - */ -public class PartPoseData implements Serializable { - private static final long serialVersionUID = 1L; - - public String partName; - public float posX, posY; - public float rotation; - public float scaleX, scaleY; - public float opacity; - public boolean visible; - public float colorR, colorG, colorB; - - public PartPoseData() { - } - - public PartPoseData(String partName, ModelPose.PartPose partPose) { - this.partName = partName; - this.posX = partPose.getPosition().x; - this.posY = partPose.getPosition().y; - this.rotation = partPose.getRotation(); - this.scaleX = partPose.getScale().x; - this.scaleY = partPose.getScale().y; - this.opacity = partPose.getOpacity(); - this.visible = partPose.isVisible(); - - org.joml.Vector3f color = partPose.getColor(); - this.colorR = color.x; - this.colorG = color.y; - this.colorB = color.z; - } - - public ModelPose.PartPose toPartPose() { - return new ModelPose.PartPose( - new org.joml.Vector2f(posX, posY), - rotation, - new org.joml.Vector2f(scaleX, scaleY), - opacity, - visible, - new org.joml.Vector3f(colorR, colorG, colorB) - ); - } - - public PartPoseData copy() { - PartPoseData copy = new PartPoseData(); - copy.partName = this.partName; - copy.posX = this.posX; - copy.posY = this.posY; - copy.rotation = this.rotation; - copy.scaleX = this.scaleX; - copy.scaleY = this.scaleY; - copy.opacity = this.opacity; - copy.visible = this.visible; - copy.colorR = this.colorR; - copy.colorG = this.colorG; - copy.colorB = this.colorB; - return copy; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/PoseData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/PoseData.java deleted file mode 100644 index 83b5053..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/PoseData.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.data; - -import com.chuangzhou.vivid2D.render.model.util.ModelPose; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -/** - * 姿态数据序列化类 - * - * @author tzdwindows 7 - */ -public class PoseData implements Serializable { - private static final long serialVersionUID = 1L; - - public String name; - public float blendTime; - public boolean isDefaultPose; - public List partPoses; - - public PoseData() { - this.partPoses = new ArrayList<>(); - } - - public PoseData(ModelPose pose) { - this.name = pose.getName(); - this.blendTime = pose.getBlendTime(); - this.isDefaultPose = pose.isDefaultPose(); - this.partPoses = new ArrayList<>(); - - // 序列化所有部件姿态 - for (String partName : pose.getPartNames()) { - ModelPose.PartPose partPose = pose.getPartPose(partName); - partPoses.add(new PartPoseData(partName, partPose)); - } - } - - public ModelPose toModelPose() { - ModelPose pose = new ModelPose(name); - pose.setBlendTime(blendTime); - pose.setDefaultPose(isDefaultPose); - - // 反序列化部件姿态 - for (PartPoseData partPoseData : partPoses) { - pose.setPartPose(partPoseData.partName, partPoseData.toPartPose()); - } - - return pose; - } - - public PoseData copy() { - PoseData copy = new PoseData(); - copy.name = this.name; - copy.blendTime = this.blendTime; - copy.isDefaultPose = this.isDefaultPose; - copy.partPoses = new ArrayList<>(); - for (PartPoseData partPose : this.partPoses) { - copy.partPoses.add(partPose.copy()); - } - return copy; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/TextureData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/TextureData.java deleted file mode 100644 index 32af8c1..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/TextureData.java +++ /dev/null @@ -1,412 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.data; - -import com.chuangzhou.vivid2D.render.model.util.Texture; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; - -public class TextureData implements Serializable { - private static final Logger logger = LoggerFactory.getLogger(TextureData.class); - private static final long serialVersionUID = 1L; - - public String name; - public String filePath; - public byte[] imageData; - public int width; - public int height; - public Texture.TextureFormat format; - public Texture.TextureFilter minFilter; - public Texture.TextureFilter magFilter; - public Texture.TextureWrap wrapS; - public Texture.TextureWrap wrapT; - public boolean mipmapsEnabled; - public Map metadata; - - public TextureData() { - this.minFilter = Texture.TextureFilter.LINEAR; - this.magFilter = Texture.TextureFilter.LINEAR; - this.wrapS = Texture.TextureWrap.CLAMP_TO_EDGE; - this.wrapT = Texture.TextureWrap.CLAMP_TO_EDGE; - this.mipmapsEnabled = false; - this.metadata = new HashMap<>(); - } - - public TextureData(Texture texture) { - this(); - this.name = texture.getName(); - this.width = texture.getWidth(); - this.height = texture.getHeight(); - this.format = texture.getFormat(); - this.minFilter = texture.getMinFilter(); - this.magFilter = texture.getMagFilter(); - this.wrapS = texture.getWrapS(); - this.wrapT = texture.getWrapT(); - this.mipmapsEnabled = texture.isMipmapsEnabled(); - - if (texture.hasPixelData()) { - this.imageData = texture.getPixelData(); - //System.out.println("Using cached pixel data for texture: " + texture.getName()); - } else { - //System.out.println("No cached data for texture: " + texture.getName() + ", extracting from GPU"); - this.imageData = extractTextureData(texture); - } - } - - private byte[] extractFromTextureInternal(Texture texture) { - if (texture.hasPixelData()) { - return texture.getPixelData(); - } - throw new RuntimeException("No OpenGL context and no internal pixel data available"); - } - - /** - * 从纹理提取图像数据 - */ - private byte[] extractTextureData(Texture texture) { - try { - // 确保有OpenGL上下文 - if (!org.lwjgl.opengl.GL.getCapabilities().OpenGL45) { - System.err.println("OpenGL context not available for texture extraction"); - // 尝试使用纹理的内部数据(如果有) - return extractFromTextureInternal(texture); - } - - java.nio.ByteBuffer pixelData = texture.extractTextureData(); - - if (pixelData == null || pixelData.remaining() == 0) { - System.err.println("Texture data extraction returned null or empty buffer"); - throw new RuntimeException("Failed to extract texture data"); - } - - // 验证数据大小 - int expectedSize = width * height * format.getComponents(); - if (pixelData.remaining() != expectedSize) { - System.err.println("Texture data size mismatch. Expected: " + expectedSize + - ", Got: " + pixelData.remaining()); - throw new RuntimeException("Texture data size mismatch"); - } - - byte[] data = new byte[pixelData.remaining()]; - pixelData.get(data); - - // 释放Native Memory - org.lwjgl.system.MemoryUtil.memFree(pixelData); - - return data; - - } catch (Exception e) { - logger.error("Critical error extracting texture data: {}", e.getMessage()); - throw new RuntimeException("Failed to extract texture data for serialization", e); - } - } - - /** - * 创建占位符纹理数据 - */ - private byte[] createPlaceholderTextureData() { - int components = format.getComponents(); - int dataSize = width * height * components; - byte[] data = new byte[dataSize]; - - // 创建简单的渐变纹理作为占位符 - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int index = (y * width + x) * components; - if (components >= 1) data[index] = (byte) ((x * 255) / width); // R - if (components >= 2) data[index + 1] = (byte) ((y * 255) / height); // G - if (components >= 3) data[index + 2] = (byte) 128; // B - if (components >= 4) data[index + 3] = (byte) 255; // A - } - } - return data; - } - - public Texture toTexture() { - try { - Texture texture = null; - - if (imageData != null && imageData.length > 0) { - try { - java.nio.ByteBuffer buffer = java.nio.ByteBuffer.allocateDirect(imageData.length); - buffer.put(imageData); - buffer.flip(); - - texture = new Texture(name, width, height, format, buffer); - } catch (Exception e) { - logger.error("Failed to create texture from image data: {}", e.getMessage()); - } - } - - // 如果从图像数据创建失败,尝试从文件创建 - if (texture == null && filePath != null && !filePath.isEmpty()) { - try { - texture = loadTextureFromFile(filePath); - } catch (Exception e) { - logger.error("Failed to create texture from file: {}", e.getMessage()); - } - } - - // 如果以上方法都失败,创建空纹理 - if (texture == null) { - try { - texture = new Texture(name, width, height, format); - } catch (Exception e) { - logger.error("Failed to create empty texture: {}", e.getMessage()); - throw e; - } - } - - // 应用纹理参数 - if (texture != null) { - try { - texture.setMinFilter(minFilter); - texture.setMagFilter(magFilter); - texture.setWrapS(wrapS); - texture.setWrapT(wrapT); - - if (mipmapsEnabled) { - texture.generateMipmaps(); - } - } catch (Exception e) { - logger.error("Failed to apply texture parameters: {}", e.getMessage()); - } - } - - return texture; - - } catch (Exception e) { - logger.error("Critical error in toTexture() for '{}': {}", name, e.getMessage()); - e.printStackTrace(); - return createSimpleFallbackTexture(); - } - } - - /** - * 创建简单的后备纹理 - */ - private Texture createSimpleFallbackTexture() { - try { - // 创建一个非常简单的纯色纹理 - return Texture.createSolidColor(name + "_simple_fallback", 64, 64, 0xFFFFFF00); // 黄色 - } catch (Exception e) { - logger.error("Even fallback texture creation failed: {}", e.getMessage()); - return null; - } - } - - /** - * 从文件加载纹理 - */ - private Texture loadTextureFromFile(String filePath) { - try { - // 使用Texture类的静态方法从文件加载 - Texture texture = Texture.createFromFile(name, filePath); - - // 应用保存的纹理参数 - texture.setMinFilter(minFilter); - texture.setMagFilter(magFilter); - texture.setWrapS(wrapS); - texture.setWrapT(wrapT); - - if (mipmapsEnabled) { - texture.generateMipmaps(); - } - - return texture; - - } catch (Exception e) { - logger.error("Failed to load texture from file: {} - {}", filePath, e.getMessage()); - return createFallbackTexture(); - } - } - - /** - * 获取纹理数据的估计内存使用量(字节) - */ - public long getEstimatedMemoryUsage() { - long baseMemory = (long) width * height * format.getComponents(); - - // 如果启用了mipmaps,加上mipmaps的内存 - if (mipmapsEnabled) { - return baseMemory * 4L / 3L; // mipmaps大约增加1/3内存 - } - - return baseMemory; - } - - /** - * 将纹理数据保存到图像文件 - */ - public boolean saveToFile(String filePath, String format) { - if (imageData == null) { - logger.error("No image data to save"); - return false; - } - - try { - // 创建临时纹理并保存 - Texture tempTexture = this.toTexture(); - boolean success = tempTexture.saveToFile(filePath, format); - tempTexture.dispose(); // 清理临时纹理 - return success; - - } catch (Exception e) { - logger.error("Failed to save texture data to file: {}", e.getMessage()); - return false; - } - } - - public boolean saveToFile(String filePath) { - return saveToFile(filePath, "png"); // 默认保存为PNG - } - - /** - * 从文件路径创建纹理数据 - */ - public static TextureData fromFile(String name, String filePath) { - TextureData data = new TextureData(); - data.name = name; - data.filePath = filePath; - - // 预加载图像信息 - try { - Texture.ImageInfo info = Texture.getImageInfo(filePath); - data.width = info.width; - data.height = info.height; - data.format = Texture.getTextureFormat(info.components); - } catch (Exception e) { - System.err.println("Failed to get image info: " + e.getMessage()); - // 设置默认值 - data.width = 64; - data.height = 64; - data.format = Texture.TextureFormat.RGBA; - } - - return data; - } - - /** - * 从内存数据创建纹理数据 - */ - public static TextureData fromMemory(String name, byte[] imageData, int width, int height, Texture.TextureFormat format) { - TextureData data = new TextureData(); - data.name = name; - data.setImageData(imageData, width, height, format); - return data; - } - - /** - * 验证纹理数据的完整性 - */ - public boolean validate() { - if (name == null || name.trim().isEmpty()) { - return false; - } - - if (width <= 0 || height <= 0) { - return false; - } - - if (format == null) { - return false; - } - - // 检查图像数据大小是否匹配 - if (imageData != null) { - int expectedSize = width * height * format.getComponents(); - if (imageData.length != expectedSize) { - System.err.println("Texture data size mismatch. Expected: " + expectedSize + ", Got: " + imageData.length); - return false; - } - } - - return true; - } - - /** - * 创建后备纹理(当主要方法失败时使用) - */ - private Texture createFallbackTexture() { - try { - // 创建一个棋盘格纹理作为后备 - return Texture.createCheckerboard( - name + "_fallback", - Math.max(32, width), - Math.max(32, height), - 8, - 0xFFFF0000, // 红色 - 0xFF0000FF // 蓝色 - ); - } catch (Exception e) { - // 如果连后备纹理都创建失败,抛出异常 - logger.error("Failed to create fallback texture: {}", e.getMessage()); - throw new RuntimeException("Failed to create fallback texture", e); - } - } - - /** - * 设置文件路径(用于从文件加载纹理) - */ - public void setFilePath(String filePath) { - this.filePath = filePath; - // 清除imageData,因为我们将从文件加载 - this.imageData = null; - } - - /** - * 设置图像数据(用于从内存数据创建纹理) - */ - public void setImageData(byte[] imageData, int width, int height, Texture.TextureFormat format) { - this.imageData = imageData; - this.width = width; - this.height = height; - this.format = format; - // 清除filePath,因为我们将使用内存数据 - this.filePath = null; - } - - /** - * 添加元数据 - */ - public void addMetadata(String key, String value) { - this.metadata.put(key, value); - } - - /** - * 获取元数据 - */ - public String getMetadata(String key) { - return this.metadata.get(key); - } - - public TextureData copy() { - TextureData copy = new TextureData(); - copy.name = this.name; - copy.filePath = this.filePath; - copy.imageData = this.imageData != null ? this.imageData.clone() : null; - copy.width = this.width; - copy.height = this.height; - copy.format = this.format; - copy.minFilter = this.minFilter; - copy.magFilter = this.magFilter; - copy.wrapS = this.wrapS; - copy.wrapT = this.wrapT; - copy.mipmapsEnabled = this.mipmapsEnabled; - copy.metadata = new HashMap<>(this.metadata); - return copy; - } - - @Override - public String toString() { - return "TextureData{" + - "name='" + name + '\'' + - ", size=" + width + "x" + height + - ", format=" + format + - ", hasImageData=" + (imageData != null) + - ", filePath=" + (filePath != null ? "'" + filePath + "'" : "null") + - '}'; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexData.java deleted file mode 100644 index 8e8dfa1..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexData.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.data; - -import com.chuangzhou.vivid2D.render.model.util.Vertex; -import com.chuangzhou.vivid2D.render.model.util.VertexTag; -import org.joml.Vector2f; - -import java.io.Serial; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -public class VertexData implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - - public float posX, posY; - public float uvX, uvY; - public float originalPosX, originalPosY; - public VertexTag tag; - public boolean selected; - public String name; - public boolean isDelete; - public int index; - public List controlledTriangles; - - public VertexData() { - this.controlledTriangles = new ArrayList<>(); - } - - public VertexData(Vertex vertex) { - this.posX = vertex.position.x; - this.posY = vertex.position.y; - this.uvX = vertex.uv.x; - this.uvY = vertex.uv.y; - this.originalPosX = vertex.originalPosition.x; - this.originalPosY = vertex.originalPosition.y; - this.tag = vertex.getTag(); - this.selected = vertex.isSelected(); - this.name = vertex.getName(); - this.isDelete = vertex.isDelete(); - this.index = vertex.getIndex(); - this.controlledTriangles = new ArrayList<>(vertex.getControlledTriangles()); - } - - public Vertex toVertex() { - Vertex vertex = new Vertex( - new Vector2f(posX, posY), - new Vector2f(uvX, uvY), - new Vector2f(originalPosX, originalPosY) - ); - vertex.setTag(this.tag); - vertex.setSelected(this.selected); - vertex.setName(this.name); - vertex.setIndex(this.index); - vertex.setControlledTriangles(this.controlledTriangles); - if (this.isDelete) { - vertex.delete(); - } - return vertex; - } - - /** - * 创建此对象的深拷贝。 - * Creates a deep copy of this object. - * @return a new, independent VertexData instance. - */ - public VertexData copy() { - VertexData c = new VertexData(); - c.posX = this.posX; - c.posY = this.posY; - c.uvX = this.uvX; - c.uvY = this.uvY; - c.originalPosX = this.originalPosX; - c.originalPosY = this.originalPosY; - c.tag = this.tag; // Enums are immutable, direct copy is fine - c.selected = this.selected; - c.name = this.name; // Strings are immutable, direct copy is fine - c.isDelete = this.isDelete; - c.index = this.index; - // Create a new list instance to ensure the copy is independent - c.controlledTriangles = new ArrayList<>(this.controlledTriangles); - return c; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexListData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexListData.java deleted file mode 100644 index d55abcb..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/VertexListData.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.data; - -import com.chuangzhou.vivid2D.render.model.util.Vertex; -import com.chuangzhou.vivid2D.render.model.util.VertexList; - -import java.io.Serial; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -public class VertexListData implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - - public String name; - public List vertices; - public int[] indices; - - public VertexListData() { - this.vertices = new ArrayList<>(); - } - - public VertexListData(VertexList vertexList) { - this.name = vertexList.getName(); - this.indices = vertexList.getIndices(); - this.vertices = vertexList.vertices.stream() - .map(VertexData::new) - .collect(Collectors.toList()); - } - - public VertexList toVertexList() { - List restoredVertices = this.vertices.stream() - .map(VertexData::toVertex) - .collect(Collectors.toList()); - return new VertexList(this.name, restoredVertices, this.indices); - } - - /** - * 创建此对象的深拷贝。 - * Creates a deep copy of this object. - * @return a new, independent VertexListData instance. - */ - public VertexListData copy() { - VertexListData c = new VertexListData(); - c.name = this.name; - // The indices array must be cloned to be independent - c.indices = this.indices != null ? this.indices.clone() : null; - // Create a new list and fill it with copies of each VertexData - if (this.vertices != null) { - c.vertices = this.vertices.stream() - .map(VertexData::copy) // Call copy on each element - .collect(Collectors.toList()); - } - return c; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/RotationDeformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/RotationDeformer.java deleted file mode 100644 index b70cef5..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/RotationDeformer.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.transform; - -import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import org.joml.Vector2f; - -import java.util.Map; - -/** - * 旋转变形器 - 围绕中心点旋转顶点 - */ -public class RotationDeformer extends Deformer { - private float baseAngle = 0.0f; - private float angleRange = (float) Math.PI; // ±90度范围 - private float currentAngle = 0.0f; - - public RotationDeformer(String name) { - super(name); - } - - public RotationDeformer(String name, Vector2f center, float radius) { - super(name); - this.getRange().setCenter(center); - this.getRange().setRadius(radius); - } - - @Override - public void applyToMesh(Mesh2D mesh) { - if (!enabled || weight <= 0.0f || currentAngle == 0.0f) { - return; - } - - float[] vertices = mesh.getVertices(); // 获取顶点数组副本 - Vector2f center = getRange().getCenter(); - float cos = (float) Math.cos(currentAngle); - float sin = (float) Math.sin(currentAngle); - boolean modified = false; - - for (int i = 0; i < mesh.getVertexCount(); i++) { - int baseIndex = i * 2; - float originalX = vertices[baseIndex]; - float originalY = vertices[baseIndex + 1]; - - // 计算相对于中心的坐标 - float dx = originalX - center.x; - float dy = originalY - center.y; - - // 应用旋转 - float rotatedX = dx * cos - dy * sin; - float rotatedY = dx * sin + dy * cos; - - float deformedX = center.x + rotatedX; - float deformedY = center.y + rotatedY; - - // 应用变形权重 - float deformationWeight = computeDeformationWeight(originalX, originalY); - blendVertexPosition(vertices, i, originalX, originalY, deformedX, deformedY, deformationWeight); - - modified = true; - } - - if (modified) { - // 更新网格顶点数据 - for (int i = 0; i < mesh.getVertexCount(); i++) { - int baseIndex = i * 2; - mesh.setVertex(i, vertices[baseIndex], vertices[baseIndex + 1]); - } - } - } - - @Override - public void apply(float value) { - // value 范围 [0, 1] 映射到 [baseAngle - angleRange/2, baseAngle + angleRange/2] - this.currentAngle = baseAngle + (value - 0.5f) * angleRange; - } - - @Override - public void reset() { - this.currentAngle = baseAngle; - } - - @Override - public void serialization(Map map) { - map.put("baseAngle", String.valueOf(baseAngle)); - map.put("angleRange", String.valueOf(angleRange)); - map.put("currentAngle", String.valueOf(currentAngle)); - } - - @Override - public void deserialize(Map map) { - if (map == null) { - return; - } - baseAngle = Float.parseFloat(map.get("baseAngle")); - angleRange = Float.parseFloat(map.get("angleRange")); - currentAngle = Float.parseFloat(map.get("currentAngle")); - } - - // Getter/Setter - public float getBaseAngle() { - return baseAngle; - } - - public void setBaseAngle(float baseAngle) { - this.baseAngle = baseAngle; - } - - public float getAngleRange() { - return angleRange; - } - - public void setAngleRange(float angleRange) { - this.angleRange = angleRange; - } - - public float getCurrentAngle() { - return currentAngle; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/ScaleDeformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/ScaleDeformer.java deleted file mode 100644 index 0be693d..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/ScaleDeformer.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.transform; - -import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.SaveVector2f; -import org.joml.Vector2f; - -import java.util.Map; - -/** - * 缩放变形器 - 围绕中心点缩放顶点 - * - * @author tzdwindows 7 - */ -public class ScaleDeformer extends Deformer { - private Vector2f baseScale = new Vector2f(1.0f, 1.0f); - private Vector2f scaleRange = new Vector2f(0.5f, 0.5f); // 缩放范围 - private Vector2f currentScale = new Vector2f(1.0f, 1.0f); - - public ScaleDeformer(String name) { - super(name); - } - - @Override - public void applyToMesh(Mesh2D mesh) { - if (!enabled || weight <= 0.0f || - (currentScale.x == 1.0f && currentScale.y == 1.0f)) { - return; - } - - float[] vertices = mesh.getVertices(); // 获取顶点数组副本 - Vector2f center = getRange().getCenter(); - boolean modified = false; - - for (int i = 0; i < mesh.getVertexCount(); i++) { - int baseIndex = i * 2; - float originalX = vertices[baseIndex]; - float originalY = vertices[baseIndex + 1]; - - // 计算相对于中心的坐标 - float dx = originalX - center.x; - float dy = originalY - center.y; - - // 应用缩放 - float deformedX = center.x + dx * currentScale.x; - float deformedY = center.y + dy * currentScale.y; - - // 应用变形权重 - float deformationWeight = computeDeformationWeight(originalX, originalY); - blendVertexPosition(vertices, i, originalX, originalY, deformedX, deformedY, deformationWeight); - - modified = true; - } - - if (modified) { - // 更新网格顶点数据 - for (int i = 0; i < mesh.getVertexCount(); i++) { - int baseIndex = i * 2; - mesh.setVertex(i, vertices[baseIndex], vertices[baseIndex + 1]); - } - } - } - - @Override - public void apply(float value) { - // value 范围 [0, 1] 映射到缩放范围 - float scaleX = baseScale.x + (value - 0.5f) * scaleRange.x; - float scaleY = baseScale.y + (value - 0.5f) * scaleRange.y; - - this.currentScale.set(scaleX, scaleY); - } - - @Override - public void reset() { - this.currentScale.set(baseScale); - } - - @Override - public void serialization(Map map) { - map.put("baseScale", SaveVector2f.toString(baseScale)); - map.put("scaleRange", SaveVector2f.toString(scaleRange)); - map.put("currentScale", SaveVector2f.toString(currentScale)); - } - - - @Override - public void deserialize(Map map) { - if (map == null) { - return; - } - baseScale = SaveVector2f.fromString(map.get("baseScale")); - scaleRange = SaveVector2f.fromString(map.get("scaleRange")); - currentScale = SaveVector2f.fromString(map.get("currentScale")); - } - - // Getter/Setter - public Vector2f getBaseScale() { - return new Vector2f(baseScale); - } - - public void setBaseScale(Vector2f baseScale) { - this.baseScale.set(baseScale); - } - - public Vector2f getScaleRange() { - return new Vector2f(scaleRange); - } - - public void setScaleRange(Vector2f scaleRange) { - this.scaleRange.set(scaleRange); - } - - public Vector2f getCurrentScale() { - return new Vector2f(currentScale); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/VertexDeformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/VertexDeformer.java deleted file mode 100644 index 73b83ef..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/VertexDeformer.java +++ /dev/null @@ -1,350 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.transform; - -import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import org.joml.Vector2f; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * 顶点位置变形器 - 直接修改顶点位置 - * 使用高效的数据结构存储变形数据 - * - * @author tzdwindows 7 - */ -public class VertexDeformer extends Deformer { - // 使用更高效的数据结构 - private final Map vertexDeformations; - private final List vertexIndexList; // 用于快速迭代 - - private float currentValue = 0.0f; - - public VertexDeformer(String name) { - super(name); - this.vertexDeformations = new HashMap<>(); - this.vertexIndexList = new ArrayList<>(); - } - - /** - * 顶点变形数据内部类 - */ - private record VertexDeformation(float originalX, float originalY, float targetX, float targetY) { - } - - /** - * 添加顶点变形目标 - */ - public void addVertexDeformation(int vertexIndex, float originalX, float originalY, float targetX, float targetY) { - VertexDeformation deformation = new VertexDeformation(originalX, originalY, targetX, targetY); - - // 使用HashMap避免重复顶点索引 - if (!vertexDeformations.containsKey(vertexIndex)) { - vertexIndexList.add(vertexIndex); - } - vertexDeformations.put(vertexIndex, deformation); - } - - /** - * 添加顶点变形目标(使用Vector2f) - */ - public void addVertexDeformation(int vertexIndex, Vector2f originalPos, Vector2f targetPos) { - addVertexDeformation(vertexIndex, originalPos.x, originalPos.y, targetPos.x, targetPos.y); - } - - /** - * 批量添加顶点变形 - */ - public void addVertexDeformations(Map deformations) { - for (Map.Entry entry : deformations.entrySet()) { - addVertexDeformation(entry.getKey(), - entry.getValue().originalX, entry.getValue().originalY, - entry.getValue().targetX, entry.getValue().targetY); - } - } - - /** - * 移除顶点变形 - */ - public boolean removeVertexDeformation(int vertexIndex) { - VertexDeformation removed = vertexDeformations.remove(vertexIndex); - if (removed != null) { - vertexIndexList.remove((Integer) vertexIndex); // 注意要移除对象而不是索引 - return true; - } - return false; - } - - /** - * 清空所有顶点变形 - */ - public void clearVertexDeformations() { - vertexDeformations.clear(); - vertexIndexList.clear(); - } - - /** - * 检查是否包含指定顶点的变形 - */ - public boolean containsVertexDeformation(int vertexIndex) { - return vertexDeformations.containsKey(vertexIndex); - } - - /** - * 获取顶点变形数量 - */ - public int getVertexDeformationCount() { - return vertexDeformations.size(); - } - - /** - * 获取指定顶点的变形数据 - */ - public VertexDeformation getVertexDeformation(int vertexIndex) { - return vertexDeformations.get(vertexIndex); - } - - /** - * 获取所有受影响的顶点索引 - */ - public List getAffectedVertexIndices() { - return new ArrayList<>(vertexIndexList); - } - - @Override - public void applyToMesh(Mesh2D mesh) { - if (!enabled || weight <= 0.0f || vertexDeformations.isEmpty()) { - return; - } - - float[] vertices = mesh.getVertices(); // 获取顶点数组副本 - boolean modified = false; - - // 使用预存的索引列表进行快速迭代 - for (int vertexIndex : vertexIndexList) { - if (vertexIndex < 0 || vertexIndex >= mesh.getVertexCount()) { - continue; - } - - VertexDeformation deformation = vertexDeformations.get(vertexIndex); - if (deformation == null) { - continue; - } - - // 获取当前顶点位置 - int vertexBaseIndex = vertexIndex * 2; - float currentX = vertices[vertexBaseIndex]; - float currentY = vertices[vertexBaseIndex + 1]; - - // 计算变形位置 - float deformedX = deformation.originalX + - (deformation.targetX - deformation.originalX) * currentValue; - float deformedY = deformation.originalY + - (deformation.targetY - deformation.originalY) * currentValue; - - // 应用变形权重 - float deformationWeight = computeDeformationWeight(currentX, currentY); - blendVertexPosition(vertices, vertexIndex, currentX, currentY, - deformedX, deformedY, deformationWeight); - - modified = true; - } - - if (modified) { - // 批量更新网格顶点数据 - updateMeshVertices(mesh, vertices); - } - } - - /** - * 批量更新网格顶点(优化性能) - */ - private void updateMeshVertices(Mesh2D mesh, float[] vertices) { - // 只更新受影响的顶点,而不是全部顶点 - for (int vertexIndex : vertexIndexList) { - if (vertexIndex < 0 || vertexIndex >= mesh.getVertexCount()) { - continue; - } - int vertexBaseIndex = vertexIndex * 2; - mesh.setVertex(vertexIndex, vertices[vertexBaseIndex], vertices[vertexBaseIndex + 1]); - } - } - - @Override - public void apply(float value) { - this.currentValue = Math.max(0.0f, Math.min(1.0f, value)); - } - - @Override - public void reset() { - this.currentValue = 0.0f; - } - - @Override - public void serialization(Map map) { - // 保存基本属性 - map.put(name + ".enabled", Boolean.toString(this.enabled)); - map.put(name + ".weight", Float.toString(this.weight)); - map.put(name + ".currentValue", Float.toString(this.currentValue)); - - // 序列化顶点变形为紧凑字符串 - // 格式示例(每个条目用分号分隔): - // index|origX,origY|targetX,targetY;index2|... - StringBuilder sb = new StringBuilder(256); - for (int vertexIndex : vertexIndexList) { - VertexDeformation d = vertexDeformations.get(vertexIndex); - if (d == null) continue; - if (sb.length() > 0) sb.append(';'); - sb.append(vertexIndex).append('|') - .append(d.originalX()).append(',').append(d.originalY()).append('|') - .append(d.targetX()).append(',').append(d.targetY()); - } - map.put(name + ".vertexDeformations", sb.toString()); - } - - @Override - public void deserialize(Map map) { - if (map == null) { - return; - } - // 解析基本属性(容错) - try { - String enabledKey = map.get(name + ".enabled"); - if (enabledKey != null) this.enabled = Boolean.parseBoolean(enabledKey); - } catch (Exception ignored) { - } - - try { - String weightKey = map.get(name + ".weight"); - if (weightKey != null) this.weight = Float.parseFloat(weightKey); - } catch (Exception ignored) { - } - - try { - String curKey = map.get(name + ".currentValue"); - if (curKey != null) this.currentValue = Float.parseFloat(curKey); - } catch (Exception ignored) { - } - - // 清空已有数据 - this.vertexDeformations.clear(); - this.vertexIndexList.clear(); - - String data = map.get(name + ".vertexDeformations"); - if (data == null || data.trim().isEmpty()) return; - - // 格式: index|origX,origY|targetX,targetY;... - String[] entries = data.split(";"); - for (String entry : entries) { - if (entry == null) continue; - entry = entry.trim(); - if (entry.isEmpty()) continue; - try { - // split into three parts by '|' - String[] parts = entry.split("\\|"); - if (parts.length != 3) continue; - - int index = Integer.parseInt(parts[0].trim()); - - String[] orig = parts[1].split(","); - String[] targ = parts[2].split(","); - if (orig.length != 2 || targ.length != 2) continue; - - float ox = Float.parseFloat(orig[0].trim()); - float oy = Float.parseFloat(orig[1].trim()); - float tx = Float.parseFloat(targ[0].trim()); - float ty = Float.parseFloat(targ[1].trim()); - - addVertexDeformation(index, ox, oy, tx, ty); - } catch (Exception ex) { - // 忽略单条解析错误,继续解析下一条 - // 可在调试时打印日志: ex.printStackTrace(); - } - } - } - - - /** - * 设置当前值并立即应用到指定网格 - */ - public void applyToMesh(float value, Mesh2D mesh) { - apply(value); - applyToMesh(mesh); - } - - /** - * 插值动画到目标值 - */ - public void animateTo(float targetValue, float duration) { - // 这里可以实现动画插值逻辑 - // 实际项目中可以使用动画系统 - this.currentValue = targetValue; - } - - public float getCurrentValue() { - return currentValue; - } - - /** - * 创建顶点变形器的深拷贝 - */ - public VertexDeformer copy() { - VertexDeformer copy = new VertexDeformer(this.name + "_copy"); - copy.enabled = this.enabled; - copy.weight = this.weight; - copy.currentValue = this.currentValue; - - // 深拷贝变形数据 - for (Map.Entry entry : this.vertexDeformations.entrySet()) { - VertexDeformation deformation = entry.getValue(); - copy.addVertexDeformation(entry.getKey(), - deformation.originalX, deformation.originalY, - deformation.targetX, deformation.targetY); - } - - return copy; - } - - /** - * 从原始网格自动提取原始位置 - */ - public void extractOriginalPositionsFromMesh(Mesh2D mesh) { - for (int vertexIndex : vertexIndexList) { - if (vertexIndex < 0 || vertexIndex >= mesh.getVertexCount()) { - continue; - } - - Vector2f currentPos = mesh.getVertex(vertexIndex); - VertexDeformation deformation = vertexDeformations.get(vertexIndex); - - if (deformation != null) { - // 更新原始位置为当前网格位置 - addVertexDeformation(vertexIndex, - currentPos.x, currentPos.y, - deformation.targetX, deformation.targetY); - } - } - } - - /** - * 反转变形方向(交换原始位置和目标位置) - */ - public void reverseDeformation() { - Map reversed = new HashMap<>(); - - for (Map.Entry entry : vertexDeformations.entrySet()) { - VertexDeformation original = entry.getValue(); - VertexDeformation reversedDeformation = new VertexDeformation( - original.targetX, original.targetY, - original.originalX, original.originalY - ); - reversed.put(entry.getKey(), reversedDeformation); - } - - this.vertexDeformations.clear(); - this.vertexDeformations.putAll(reversed); - this.currentValue = 1.0f - this.currentValue; // 反转当前值 - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/WaveDeformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/WaveDeformer.java deleted file mode 100644 index 88a2fb4..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/WaveDeformer.java +++ /dev/null @@ -1,342 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.transform; - -import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import org.joml.Vector2f; - -import java.util.Map; - -/** - * 波浪变形器 - 创建波浪效果的顶点变形 - */ -public class WaveDeformer extends Deformer { - private float amplitude = 10.0f; // 波幅 - private float frequency = 0.1f; // 频率 - private float phase = 0.0f; // 相位 - private float waveAngle = 0.0f; // 波传播方向角度 - private float currentTime = 0.0f; - - public WaveDeformer(String name) { - super(name); - } - - @Override - public void applyToMesh(Mesh2D mesh) { - if (!enabled || weight <= 0.0f || amplitude == 0.0f) { - return; - } - - float[] vertices = mesh.getVertices(); // 获取顶点数组副本 - Vector2f center = getRange().getCenter(); - float cosDir = (float) Math.cos(waveAngle); - float sinDir = (float) Math.sin(waveAngle); - boolean modified = false; - - for (int i = 0; i < mesh.getVertexCount(); i++) { - int baseIndex = i * 2; - float originalX = vertices[baseIndex]; - float originalY = vertices[baseIndex + 1]; - - // 计算在波传播方向上的投影距离 - float projDistance = (originalX - center.x) * cosDir + - (originalY - center.y) * sinDir; - - // 计算波浪偏移 - float waveOffset = amplitude * - (float) Math.sin(frequency * projDistance + phase + currentTime); - - // 垂直于波传播方向的偏移 - float deformedX = originalX - sinDir * waveOffset; - float deformedY = originalY + cosDir * waveOffset; - - // 应用变形权重 - float deformationWeight = computeDeformationWeight(originalX, originalY); - blendVertexPosition(vertices, i, originalX, originalY, deformedX, deformedY, deformationWeight); - - modified = true; - } - - if (modified) { - // 更新网格顶点数据 - for (int i = 0; i < mesh.getVertexCount(); i++) { - int baseIndex = i * 2; - mesh.setVertex(i, vertices[baseIndex], vertices[baseIndex + 1]); - } - } - } - - @Override - public void apply(float value) { - // 根据配置的驱动参数类型,决定如何应用value - // 这里假设通过参数名称或配置来决定控制哪个波浪参数 - - // 方案1: 根据当前激活的驱动参数类型来应用 - if (!parameterValues.isEmpty()) { - // 如果有多个参数,可以按优先级或特定逻辑处理 - // 这里简单取第一个参数的值 - String firstParam = drivenParameters.iterator().next(); - applyByParameterName(firstParam, value); - } else { - // 默认行为:控制波幅 - applyAmplitude(value); - } - } - - /** - * 根据参数名称应用不同的波浪参数控制 - */ - private void applyByParameterName(String paramName, float value) { - paramName = paramName.toLowerCase(); - - if (paramName.contains("amplitude") || paramName.contains("amp")) { - applyAmplitude(value); - } else if (paramName.contains("frequency") || paramName.contains("freq")) { - applyFrequency(value); - } else if (paramName.contains("phase") || paramName.contains("offset")) { - applyPhase(value); - } else if (paramName.contains("angle") || paramName.contains("direction")) { - applyWaveAngle(value); - } else if (paramName.contains("time") || paramName.contains("speed")) { - applyTimeSpeed(value); - } else if (paramName.contains("weight") || paramName.contains("intensity")) { - applyWeight(value); - } else { - // 默认控制波幅 - applyAmplitude(value); - } - } - - /** - * 控制波幅 - value [0,1] 映射到 [0, maxAmplitude] - */ - private void applyAmplitude(float value) { - float maxAmplitude = 50.0f; // 最大波幅 - this.amplitude = value * maxAmplitude; - } - - /** - * 控制频率 - value [0,1] 映射到 [minFrequency, maxFrequency] - */ - private void applyFrequency(float value) { - float minFrequency = 0.01f; // 最小频率 - float maxFrequency = 0.5f; // 最大频率 - this.frequency = minFrequency + value * (maxFrequency - minFrequency); - } - - /** - * 控制相位 - value [0,1] 映射到 [0, 2π] - */ - private void applyPhase(float value) { - this.phase = value * (float) (2.0f * Math.PI); - } - - /** - * 控制波传播方向 - value [0,1] 映射到 [0, 2π] - */ - private void applyWaveAngle(float value) { - this.waveAngle = value * (float) (2.0f * Math.PI); - } - - /** - * 控制时间速度 - value [0,1] 映射到时间乘数 [0.1, 5.0] - */ - private void applyTimeSpeed(float value) { - // 这个需要在外部update方法中使用timeMultiplier - // 这里先存储,在update中使用 - this.timeMultiplier = 0.1f + value * 4.9f; - } - - /** - * 控制变形器权重 - value [0,1] 直接设置权重 - */ - private void applyWeight(float value) { - setWeight(value); - } - - // 添加时间乘数字段 - private float timeMultiplier = 1.0f; - - /** - * 更新波浪动画(使用时间乘数) - */ - public void update(float deltaTime) { - this.currentTime += deltaTime * timeMultiplier; - } - - // 添加参数配置方法,允许外部指定控制模式 - public enum ControlMode { - AMPLITUDE, // 控制波幅 - FREQUENCY, // 控制频率 - PHASE, // 控制相位 - WAVE_ANGLE, // 控制波方向 - TIME_SPEED, // 控制动画速度 - WEIGHT // 控制变形器权重 - } - - private ControlMode controlMode = ControlMode.AMPLITUDE; - - /** - * 设置控制模式 - */ - public void setControlMode(ControlMode mode) { - this.controlMode = mode; - } - - /** - * 根据设置的控制模式应用参数 - */ - public void applyWithMode(float value, ControlMode mode) { - switch (mode) { - case AMPLITUDE: - applyAmplitude(value); - break; - case FREQUENCY: - applyFrequency(value); - break; - case PHASE: - applyPhase(value); - break; - case WAVE_ANGLE: - applyWaveAngle(value); - break; - case TIME_SPEED: - applyTimeSpeed(value); - break; - case WEIGHT: - applyWeight(value); - break; - default: - applyAmplitude(value); - } - } - - /** - * 批量应用多个参数 - */ - public void applyParameters(float amplitudeValue, float frequencyValue, float phaseValue, - float angleValue, float speedValue, float weightValue) { - applyAmplitude(amplitudeValue); - applyFrequency(frequencyValue); - applyPhase(phaseValue); - applyWaveAngle(angleValue); - applyTimeSpeed(speedValue); - applyWeight(weightValue); - } - - /** - * 使用配置对象应用参数 - */ - public void applyFromConfig(WaveConfig config) { - this.amplitude = config.amplitude; - this.frequency = config.frequency; - this.phase = config.phase; - this.waveAngle = config.waveAngle; - this.timeMultiplier = config.timeMultiplier; - setWeight(config.weight); - } - - /** - * 波浪配置类 - */ - public static class WaveConfig { - public float amplitude = 10.0f; - public float frequency = 0.1f; - public float phase = 0.0f; - public float waveAngle = 0.0f; - public float timeMultiplier = 1.0f; - public float weight = 1.0f; - - public WaveConfig() { - } - - public WaveConfig(float amplitude, float frequency, float phase, - float waveAngle, float timeMultiplier, float weight) { - this.amplitude = amplitude; - this.frequency = frequency; - this.phase = phase; - this.waveAngle = waveAngle; - this.timeMultiplier = timeMultiplier; - this.weight = weight; - } - } - - @Override - public void reset() { - this.currentTime = 0.0f; - this.amplitude = 10.0f; - } - - @Override - public void serialization(Map map) { - if (map == null) { - return; - } - map.put("amplitude", String.valueOf(amplitude)); - map.put("frequency", String.valueOf(frequency)); - map.put("phase", String.valueOf(phase)); - map.put("waveAngle", String.valueOf(waveAngle)); - map.put("timeMultiplier", String.valueOf(timeMultiplier)); - } - - @Override - public void deserialize(Map map) { - amplitude = Float.parseFloat(map.get("amplitude")); - frequency = Float.parseFloat(map.get("frequency")); - phase = Float.parseFloat(map.get("phase")); - waveAngle = Float.parseFloat(map.get("waveAngle")); - timeMultiplier = Float.parseFloat(map.get("timeMultiplier")); - } - - public float getTimeMultiplier() { - return timeMultiplier; - } - - public void setTimeMultiplier(float timeMultiplier) { - this.timeMultiplier = timeMultiplier; - } - - public ControlMode getControlMode() { - return controlMode; - } - - // Getter/Setter - public float getAmplitude() { - return amplitude; - } - - public void setAmplitude(float amplitude) { - this.amplitude = amplitude; - } - - public float getFrequency() { - return frequency; - } - - public void setFrequency(float frequency) { - this.frequency = frequency; - } - - public float getPhase() { - return phase; - } - - public void setPhase(float phase) { - this.phase = phase; - } - - public float getWaveAngle() { - return waveAngle; - } - - public void setWaveAngle(float waveAngle) { - this.waveAngle = waveAngle; - } - - public float getCurrentTime() { - return currentTime; - } - - public void setCurrentTime(float currentTime) { - this.currentTime = currentTime; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationClip.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationClip.java deleted file mode 100644 index 227fa1d..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationClip.java +++ /dev/null @@ -1,776 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 动画剪辑类,用于管理2D模型的完整动画序列 - * 支持关键帧动画、曲线编辑、事件标记和动画混合 - * - * @author tzdwindows 7 - */ -public class AnimationClip { - // ==================== 剪辑属性 ==================== - private final String name; - private final UUID uuid; - private float duration; - private float framesPerSecond; - private boolean looping; - - // ==================== 动画数据 ==================== - private final Map curves; - private final List eventMarkers; - private final Map defaultValues; - - // ==================== 元数据 ==================== - private String author; - private String description; - private final long creationTime; - private long lastModifiedTime; - private Map userData; - - // ==================== 构造器 ==================== - - public AnimationClip(String name) { - this(name, 1.0f, 60.0f); - } - - public AnimationClip(String name, float duration, float fps) { - this.name = name; - this.uuid = UUID.randomUUID(); - this.duration = Math.max(0.0f, duration); - this.framesPerSecond = Math.max(1.0f, fps); - this.looping = true; - - this.curves = new ConcurrentHashMap<>(); - this.eventMarkers = new ArrayList<>(); - this.defaultValues = new ConcurrentHashMap<>(); - - this.author = "Unknown"; - this.description = ""; - this.creationTime = System.currentTimeMillis(); - this.lastModifiedTime = creationTime; - this.userData = new ConcurrentHashMap<>(); - } - - // ==================== 曲线管理 ==================== - - /** - * 添加动画曲线 - */ - public AnimationCurve addCurve(String parameterId) { - return addCurve(parameterId, 0.0f); - } - - public AnimationCurve addCurve(String parameterId, float defaultValue) { - AnimationCurve curve = new AnimationCurve(parameterId, defaultValue); - curves.put(parameterId, curve); - defaultValues.put(parameterId, defaultValue); - markModified(); - return curve; - } - - /** - * 获取动画曲线 - */ - public AnimationCurve getCurve(String parameterId) { - return curves.get(parameterId); - } - - /** - * 移除动画曲线 - */ - public boolean removeCurve(String parameterId) { - boolean removed = curves.remove(parameterId) != null; - defaultValues.remove(parameterId); - if (removed) markModified(); - return removed; - } - - /** - * 检查是否存在曲线 - */ - public boolean hasCurve(String parameterId) { - return curves.containsKey(parameterId); - } - - /** - * 获取所有曲线参数ID - */ - public Set getCurveParameterIds() { - return Collections.unmodifiableSet(curves.keySet()); - } - - // ==================== 关键帧管理 ==================== - - /** - * 添加关键帧 - */ - public Keyframe addKeyframe(String parameterId, float time, float value) { - return addKeyframe(parameterId, time, value, InterpolationType.LINEAR); - } - - public Keyframe addKeyframe(String parameterId, float time, float value, - InterpolationType interpolation) { - AnimationCurve curve = getOrCreateCurve(parameterId); - Keyframe keyframe = curve.addKeyframe(time, value, interpolation); - updateDurationIfNeeded(time); - markModified(); - return keyframe; - } - - /** - * 移除关键帧 - */ - public boolean removeKeyframe(String parameterId, float time) { - AnimationCurve curve = curves.get(parameterId); - if (curve != null) { - boolean removed = curve.removeKeyframe(time); - if (removed) markModified(); - return removed; - } - return false; - } - - /** - * 获取关键帧 - */ - public Keyframe getKeyframe(String parameterId, float time) { - AnimationCurve curve = curves.get(parameterId); - return curve != null ? curve.getKeyframe(time) : null; - } - - /** - * 获取参数在指定时间的所有关键帧 - */ - public List getKeyframes(String parameterId) { - AnimationCurve curve = curves.get(parameterId); - return curve != null ? curve.getKeyframes() : Collections.emptyList(); - } - - // ==================== 采样系统 ==================== - - /** - * 采样动画在指定时间的参数值 - */ - public Map sample(float time) { - Map result = new HashMap<>(); - - for (Map.Entry entry : curves.entrySet()) { - String paramId = entry.getKey(); - AnimationCurve curve = entry.getValue(); - float value = curve.sample(time); - result.put(paramId, value); - } - - return result; - } - - /** - * 采样单个参数在指定时间的值 - */ - public float sampleParameter(String parameterId, float time) { - AnimationCurve curve = curves.get(parameterId); - return curve != null ? curve.sample(time) : defaultValues.getOrDefault(parameterId, 0.0f); - } - - /** - * 采样动画在指定时间的参数值(应用循环) - */ - public Map sampleLooped(float time) { - float effectiveTime = time; - if (looping && duration > 0) { - effectiveTime = time % duration; - } else { - effectiveTime = Math.min(time, duration); - } - return sample(effectiveTime); - } - - // ==================== 事件标记管理 ==================== - - /** - * 添加事件标记 - */ - public AnimationEventMarker addEventMarker(String name, float time) { - return addEventMarker(name, time, null); - } - - public AnimationEventMarker addEventMarker(String name, float time, Runnable action) { - AnimationEventMarker marker = new AnimationEventMarker(name, time, action); - - // 按时间排序插入 - int index = 0; - while (index < eventMarkers.size() && eventMarkers.get(index).getTime() < time) { - index++; - } - eventMarkers.add(index, marker); - - updateDurationIfNeeded(time); - markModified(); - return marker; - } - - /** - * 移除事件标记 - */ - public boolean removeEventMarker(String name) { - return eventMarkers.removeIf(marker -> marker.getName().equals(name)); - } - - /** - * 获取指定时间范围内的事件标记 - */ - public List getEventMarkersInRange(float startTime, float endTime) { - List result = new ArrayList<>(); - for (AnimationEventMarker marker : eventMarkers) { - if (marker.getTime() >= startTime && marker.getTime() <= endTime) { - result.add(marker); - } - } - return result; - } - - /** - * 触发指定时间的事件标记 - */ - public void triggerEventMarkers(float time, float tolerance) { - for (AnimationEventMarker marker : eventMarkers) { - if (Math.abs(marker.getTime() - time) <= tolerance && !marker.isTriggered()) { - marker.trigger(); - } - } - } - - /** - * 重置所有事件标记状态 - */ - public void resetEventMarkers() { - for (AnimationEventMarker marker : eventMarkers) { - marker.reset(); - } - } - - // ==================== 工具方法 ==================== - - /** - * 获取或创建曲线 - */ - private AnimationCurve getOrCreateCurve(String parameterId) { - return curves.computeIfAbsent(parameterId, k -> { - float defaultValue = defaultValues.getOrDefault(parameterId, 0.0f); - return new AnimationCurve(parameterId, defaultValue); - }); - } - - /** - * 更新动画时长(如果需要) - */ - private void updateDurationIfNeeded(float time) { - if (time > duration) { - duration = time; - markModified(); - } - } - - /** - * 标记为已修改 - */ - private void markModified() { - lastModifiedTime = System.currentTimeMillis(); - } - - /** - * 计算帧数 - */ - public int getFrameCount() { - return (int) Math.ceil(duration * framesPerSecond); - } - - /** - * 时间转换为帧索引 - */ - public int timeToFrame(float time) { - return (int) (time * framesPerSecond); - } - - /** - * 帧索引转换为时间 - */ - public float frameToTime(int frame) { - return frame / framesPerSecond; - } - - /** - * 检查时间是否在动画范围内 - */ - public boolean isTimeInRange(float time) { - return time >= 0 && time <= duration; - } - - /** - * 获取动画的边界值(最小/最大值) - */ - public Map getValueBounds() { - Map bounds = new HashMap<>(); - - for (Map.Entry entry : curves.entrySet()) { - String paramId = entry.getKey(); - AnimationCurve curve = entry.getValue(); - float[] minMax = curve.getValueRange(); - bounds.put(paramId, minMax); - } - - return bounds; - } - - /** - * 创建动画剪辑的深拷贝 - */ - public AnimationClip copy() { - AnimationClip copy = new AnimationClip(name + "_copy", duration, framesPerSecond); - copy.looping = this.looping; - copy.author = this.author; - copy.description = this.description; - - // 深拷贝曲线 - for (Map.Entry entry : this.curves.entrySet()) { - copy.curves.put(entry.getKey(), entry.getValue().copy()); - } - - // 深拷贝默认值 - copy.defaultValues.putAll(this.defaultValues); - - // 深拷贝事件标记 - for (AnimationEventMarker marker : this.eventMarkers) { - copy.eventMarkers.add(marker.copy()); - } - - // 深拷贝用户数据 - copy.userData.putAll(this.userData); - - return copy; - } - - /** - * 合并另一个动画剪辑 - */ - public void merge(AnimationClip other) { - if (other == null) return; - - // 合并曲线 - for (Map.Entry entry : other.curves.entrySet()) { - String paramId = entry.getKey(); - AnimationCurve otherCurve = entry.getValue(); - - if (this.curves.containsKey(paramId)) { - // 合并到现有曲线 - AnimationCurve thisCurve = this.curves.get(paramId); - for (Keyframe keyframe : otherCurve.getKeyframes()) { - thisCurve.addKeyframe(keyframe.getTime(), keyframe.getValue(), - keyframe.getInterpolation()); - } - } else { - // 添加新曲线 - this.curves.put(paramId, otherCurve.copy()); - } - } - - // 合并事件标记 - for (AnimationEventMarker marker : other.eventMarkers) { - this.addEventMarker(marker.getName() + "_merged", marker.getTime(), - marker.getAction()); - } - - // 更新时长 - this.duration = Math.max(this.duration, other.duration); - - markModified(); - } - - // ==================== 内部类 ==================== - - /** - * 动画曲线类 - */ - public static class AnimationCurve { - private final String parameterId; - private final List keyframes; - private final float defaultValue; - - public AnimationCurve(String parameterId, float defaultValue) { - this.parameterId = parameterId; - this.keyframes = new ArrayList<>(); - this.defaultValue = defaultValue; - } - - /** - * 添加关键帧 - */ - public Keyframe addKeyframe(float time, float value) { - return addKeyframe(time, value, InterpolationType.LINEAR); - } - - public Keyframe addKeyframe(float time, float value, InterpolationType interpolation) { - Keyframe keyframe = new Keyframe(time, value, interpolation); - - // 移除相同时间的关键帧(如果有) - removeKeyframe(time); - - // 按时间排序插入 - int index = 0; - while (index < keyframes.size() && keyframes.get(index).getTime() < time) { - index++; - } - keyframes.add(index, keyframe); - - return keyframe; - } - - /** - * 移除关键帧 - */ - public boolean removeKeyframe(float time) { - return keyframes.removeIf(kf -> Math.abs(kf.getTime() - time) < 0.0001f); - } - - /** - * 获取关键帧 - */ - public Keyframe getKeyframe(float time) { - for (Keyframe kf : keyframes) { - if (Math.abs(kf.getTime() - time) < 0.0001f) { - return kf; - } - } - return null; - } - - /** - * 采样曲线值 - */ - public float sample(float time) { - if (keyframes.isEmpty()) { - return defaultValue; - } - - // 在第一个关键帧之前 - if (time <= keyframes.get(0).getTime()) { - return keyframes.get(0).getValue(); - } - - // 在最后一个关键帧之后 - if (time >= keyframes.get(keyframes.size() - 1).getTime()) { - return keyframes.get(keyframes.size() - 1).getValue(); - } - - // 找到包围时间的关键帧 - for (int i = 0; i < keyframes.size() - 1; i++) { - Keyframe kf1 = keyframes.get(i); - Keyframe kf2 = keyframes.get(i + 1); - - if (time >= kf1.getTime() && time <= kf2.getTime()) { - return interpolate(kf1, kf2, time); - } - } - - return defaultValue; - } - - /** - * 插值计算 - */ - private float interpolate(Keyframe kf1, Keyframe kf2, float time) { - float t = (time - kf1.getTime()) / (kf2.getTime() - kf1.getTime()); - - switch (kf1.getInterpolation()) { - case LINEAR: - return lerp(kf1.getValue(), kf2.getValue(), t); - case STEP: - return kf1.getValue(); - case SMOOTH: - return smoothLerp(kf1.getValue(), kf2.getValue(), t); - case EASE_IN: - return easeInLerp(kf1.getValue(), kf2.getValue(), t); - case EASE_OUT: - return easeOutLerp(kf1.getValue(), kf2.getValue(), t); - case EASE_IN_OUT: - return easeInOutLerp(kf1.getValue(), kf2.getValue(), t); - default: - return kf1.getValue(); - } - } - - private float lerp(float a, float b, float t) { - return a + (b - a) * t; - } - - private float smoothLerp(float a, float b, float t) { - float t2 = t * t; - float t3 = t2 * t; - return a * (2 * t3 - 3 * t2 + 1) + b * (-2 * t3 + 3 * t2); - } - - private float easeInLerp(float a, float b, float t) { - return a + (b - a) * (t * t); - } - - private float easeOutLerp(float a, float b, float t) { - return a + (b - a) * (1 - (1 - t) * (1 - t)); - } - - private float easeInOutLerp(float a, float b, float t) { - return a + (b - a) * ((t < 0.5f) ? 2 * t * t : 1 - (2 * (1 - t) * (1 - t)) / 2); - } - - /** - * 获取值范围 - */ - public float[] getValueRange() { - if (keyframes.isEmpty()) { - return new float[]{defaultValue, defaultValue}; - } - - float min = Float.MAX_VALUE; - float max = Float.MIN_VALUE; - - for (Keyframe kf : keyframes) { - min = Math.min(min, kf.getValue()); - max = Math.max(max, kf.getValue()); - } - - return new float[]{min, max}; - } - - /** - * 创建曲线深拷贝 - */ - public AnimationCurve copy() { - AnimationCurve copy = new AnimationCurve(parameterId, defaultValue); - for (Keyframe kf : this.keyframes) { - copy.keyframes.add(kf.copy()); - } - return copy; - } - - // Getter方法 - public String getParameterId() { - return parameterId; - } - - public List getKeyframes() { - return Collections.unmodifiableList(keyframes); - } - - public float getDefaultValue() { - return defaultValue; - } - } - - /** - * 关键帧类 - */ - public static class Keyframe { - private final float time; - private final float value; - private final InterpolationType interpolation; - - public Keyframe(float time, float value, InterpolationType interpolation) { - this.time = time; - this.value = value; - this.interpolation = interpolation; - } - - public Keyframe copy() { - return new Keyframe(time, value, interpolation); - } - - // Getter方法 - public float getTime() { - return time; - } - - public float getValue() { - return value; - } - - public InterpolationType getInterpolation() { - return interpolation; - } - - @Override - public String toString() { - return String.format("Keyframe{time=%.2f, value=%.2f, interpolation=%s}", - time, value, interpolation); - } - } - - /** - * 事件标记类 - */ - public static class AnimationEventMarker { - private final String name; - private final float time; - private final Runnable action; - private boolean triggered; - - public AnimationEventMarker(String name, float time, Runnable action) { - this.name = name; - this.time = time; - this.action = action; - this.triggered = false; - } - - public void trigger() { - if (!triggered && action != null) { - action.run(); - triggered = true; - } - } - - public void reset() { - triggered = false; - } - - public AnimationEventMarker copy() { - return new AnimationEventMarker(name, time, action); - } - - // Getter方法 - public String getName() { - return name; - } - - public float getTime() { - return time; - } - - public Runnable getAction() { - return action; - } - - public boolean isTriggered() { - return triggered; - } - - @Override - public String toString() { - return String.format("EventMarker{name='%s', time=%.2f}", name, time); - } - } - - /** - * 插值类型枚举 - */ - public enum InterpolationType { - LINEAR, // 线性插值 - STEP, // 步进插值 - SMOOTH, // 平滑插值(三次Hermite) - EASE_IN, // 缓入 - EASE_OUT, // 缓出 - EASE_IN_OUT // 缓入缓出 - } - - // ==================== Getter/Setter ==================== - - public String getName() { - return name; - } - - public UUID getUuid() { - return uuid; - } - - public float getDuration() { - return duration; - } - - public void setDuration(float duration) { - this.duration = Math.max(0.0f, duration); - markModified(); - } - - public float getFramesPerSecond() { - return framesPerSecond; - } - - public void setFramesPerSecond(float framesPerSecond) { - this.framesPerSecond = Math.max(1.0f, framesPerSecond); - markModified(); - } - - public boolean isLooping() { - return looping; - } - - public void setLooping(boolean looping) { - this.looping = looping; - } - - public Map getCurves() { - return Collections.unmodifiableMap(curves); - } - - public List getEventMarkers() { - return Collections.unmodifiableList(eventMarkers); - } - - public Map getDefaultValues() { - return Collections.unmodifiableMap(defaultValues); - } - - public String getAuthor() { - return author; - } - - public void setAuthor(String author) { - this.author = author; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public long getCreationTime() { - return creationTime; - } - - public long getLastModifiedTime() { - return lastModifiedTime; - } - - public Map getUserData() { - return Collections.unmodifiableMap(userData); - } - - public void setUserData(Map userData) { - this.userData = new ConcurrentHashMap<>(userData); - } - - // ==================== Object 方法 ==================== - - @Override - public String toString() { - return String.format( - "AnimationClip{name='%s', duration=%.2f, curves=%d, events=%d, looping=%s}", - name, duration, curves.size(), eventMarkers.size(), looping - ); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - AnimationClip that = (AnimationClip) o; - return uuid.equals(that.uuid); - } - - @Override - public int hashCode() { - return Objects.hash(uuid); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationLayer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationLayer.java deleted file mode 100644 index d42103a..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/AnimationLayer.java +++ /dev/null @@ -1,834 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import com.chuangzhou.vivid2D.render.model.Model2D; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 动画层类,用于管理2D模型的动画层和动画混合 - * 支持多层动画叠加、权重控制、混合模式等高级功能 - * - * @author tzdwindows 7 - */ -public class AnimationLayer { - // ==================== 层属性 ==================== - private final String name; - private final UUID uuid; - private float weight; - private boolean enabled; - private BlendMode blendMode; - private int priority; - - // ==================== 动画数据 ==================== - private final Map tracks; - private final List clips; - private AnimationClip currentClip; - private float playbackSpeed; - private boolean looping; - - // ==================== 状态管理 ==================== - private float currentTime; - private boolean playing; - private boolean paused; - private final Map parameterOverrides; - - // ==================== 事件系统 ==================== - private final List eventListeners; - private final Map> events; - - // ==================== 构造器 ==================== - - public AnimationLayer(String name) { - this(name, 1.0f); - } - - public AnimationLayer(String name, float weight) { - this.name = name; - this.uuid = UUID.randomUUID(); - this.weight = Math.max(0.0f, Math.min(1.0f, weight)); - this.enabled = true; - this.blendMode = BlendMode.OVERRIDE; - this.priority = 0; - - this.tracks = new ConcurrentHashMap<>(); - this.clips = new ArrayList<>(); - this.playbackSpeed = 1.0f; - this.looping = true; - - this.currentTime = 0.0f; - this.playing = false; - this.paused = false; - this.parameterOverrides = new ConcurrentHashMap<>(); - - this.eventListeners = new ArrayList<>(); - this.events = new ConcurrentHashMap<>(); - } - - // ==================== 轨道管理 ==================== - - /** - * 添加动画轨道 - */ - public AnimationTrack addTrack(String parameterId) { - AnimationTrack track = new AnimationTrack(parameterId); - tracks.put(parameterId, track); - return track; - } - - /** - * 获取动画轨道 - */ - public AnimationTrack getTrack(String parameterId) { - return tracks.get(parameterId); - } - - /** - * 移除动画轨道 - */ - public boolean removeTrack(String parameterId) { - return tracks.remove(parameterId) != null; - } - - /** - * 检查是否存在轨道 - */ - public boolean hasTrack(String parameterId) { - return tracks.containsKey(parameterId); - } - - // ==================== 剪辑管理 ==================== - - /** - * 添加动画剪辑 - */ - public void addClip(AnimationClip clip) { - clips.add(clip); - } - - /** - * 移除动画剪辑 - */ - public boolean removeClip(AnimationClip clip) { - return clips.remove(clip); - } - - /** - * 播放指定剪辑 - */ - public void playClip(String clipName) { - for (AnimationClip clip : clips) { - if (clip.getName().equals(clipName)) { - playClip(clip); - return; - } - } - throw new IllegalArgumentException("Animation clip not found: " + clipName); - } - - public void playClip(AnimationClip clip) { - this.currentClip = clip; - this.currentTime = 0.0f; - this.playing = true; - this.paused = false; - - notifyAnimationStarted(clip); - } - - /** - * 停止播放 - */ - public void stop() { - this.playing = false; - this.paused = false; - this.currentTime = 0.0f; - - if (currentClip != null) { - notifyAnimationStopped(currentClip); - } - } - - /** - * 暂停播放 - */ - public void pause() { - if (playing && !paused) { - paused = true; - notifyAnimationPaused(currentClip); - } - } - - /** - * 恢复播放 - */ - public void resume() { - if (playing && paused) { - paused = false; - notifyAnimationResumed(currentClip); - } - } - - // ==================== 更新系统 ==================== - - /** - * 更新动画层 - */ - public void update(float deltaTime, Model2D model) { - if (!enabled || weight <= 0.0f) { - return; - } - - // 更新播放时间 - if (playing && !paused) { - currentTime += deltaTime * playbackSpeed; - - // 检查循环 - if (currentClip != null && currentTime >= currentClip.getDuration()) { - if (looping) { - currentTime %= currentClip.getDuration(); - notifyAnimationLooped(currentClip); - } else { - stop(); - notifyAnimationCompleted(currentClip); - return; - } - } - - // 检查事件 - checkEvents(); - } - - // 应用动画 - applyAnimation(model); - } - - /** - * 应用动画到模型 - */ - private void applyAnimation(Model2D model) { - if (currentClip != null) { - // 应用剪辑动画 - applyClipAnimation(model); - } else { - // 应用轨道动画 - applyTrackAnimation(model); - } - } - - /** - * 应用剪辑动画 - */ - private void applyClipAnimation(Model2D model) { - Map animatedValues = currentClip.sample(currentTime); - - for (Map.Entry entry : animatedValues.entrySet()) { - String paramId = entry.getKey(); - float value = entry.getValue(); - - // 应用权重和混合模式 - float finalValue = applyBlending(model, paramId, value); - - // 设置参数值 - model.setParameterValue(paramId, finalValue); - } - } - - /** - * 应用轨道动画 - */ - private void applyTrackAnimation(Model2D model) { - for (AnimationTrack track : tracks.values()) { - if (track.isEnabled()) { - float value = track.sample(currentTime); - String paramId = track.getParameterId(); - - // 应用权重和混合模式 - float finalValue = applyBlending(model, paramId, value); - - // 设置参数值 - model.setParameterValue(paramId, finalValue); - } - } - } - - /** - * 应用混合模式 - */ - private float applyBlending(Model2D model, String paramId, float newValue) { - float currentValue = model.getParameterValue(paramId); - float overrideValue = parameterOverrides.getOrDefault(paramId, Float.NaN); - - if (!Float.isNaN(overrideValue)) { - return overrideValue; - } - - switch (blendMode) { - case OVERRIDE: - return blendOverride(currentValue, newValue); - case ADDITIVE: - return blendAdditive(currentValue, newValue); - case MULTIPLICATIVE: - return blendMultiplicative(currentValue, newValue); - case AVERAGE: - return blendAverage(currentValue, newValue); - default: - return newValue; - } - } - - private float blendOverride(float current, float target) { - return current + (target - current) * weight; - } - - private float blendAdditive(float current, float target) { - return current + target * weight; - } - - private float blendMultiplicative(float current, float target) { - return current * (1.0f + (target - 1.0f) * weight); - } - - private float blendAverage(float current, float target) { - return (current * (1.0f - weight)) + (target * weight); - } - - // ==================== 事件系统 ==================== - - /** - * 添加动画事件 - */ - public void addEvent(String eventName, float time, Runnable action) { - AnimationEvent event = new AnimationEvent(eventName, time, action); - events.computeIfAbsent(eventName, k -> new ArrayList<>()).add(event); - } - - /** - * 检查并触发事件 - */ - private void checkEvents() { - if (currentClip == null) return; - - for (List eventList : events.values()) { - for (AnimationEvent event : eventList) { - if (!event.isTriggered() && currentTime >= event.getTime()) { - event.trigger(); - notifyEventTriggered(event); - } - } - } - } - - /** - * 重置所有事件状态 - */ - public void resetEvents() { - for (List eventList : events.values()) { - for (AnimationEvent event : eventList) { - event.reset(); - } - } - } - - // ==================== 参数覆盖 ==================== - - /** - * 设置参数覆盖值 - */ - public void setParameterOverride(String parameterId, float value) { - parameterOverrides.put(parameterId, value); - } - - /** - * 清除参数覆盖 - */ - public void clearParameterOverride(String parameterId) { - parameterOverrides.remove(parameterId); - } - - /** - * 清除所有参数覆盖 - */ - public void clearAllOverrides() { - parameterOverrides.clear(); - } - - // ==================== 事件监听器 ==================== - - /** - * 添加事件监听器 - */ - public void addEventListener(AnimationEventListener listener) { - eventListeners.add(listener); - } - - /** - * 移除事件监听器 - */ - public boolean removeEventListener(AnimationEventListener listener) { - return eventListeners.remove(listener); - } - - private void notifyAnimationStarted(AnimationClip clip) { - for (AnimationEventListener listener : eventListeners) { - listener.onAnimationStarted(this, clip); - } - } - - private void notifyAnimationStopped(AnimationClip clip) { - for (AnimationEventListener listener : eventListeners) { - listener.onAnimationStopped(this, clip); - } - } - - private void notifyAnimationPaused(AnimationClip clip) { - for (AnimationEventListener listener : eventListeners) { - listener.onAnimationPaused(this, clip); - } - } - - private void notifyAnimationResumed(AnimationClip clip) { - for (AnimationEventListener listener : eventListeners) { - listener.onAnimationResumed(this, clip); - } - } - - private void notifyAnimationCompleted(AnimationClip clip) { - for (AnimationEventListener listener : eventListeners) { - listener.onAnimationCompleted(this, clip); - } - } - - private void notifyAnimationLooped(AnimationClip clip) { - for (AnimationEventListener listener : eventListeners) { - listener.onAnimationLooped(this, clip); - } - } - - private void notifyEventTriggered(AnimationEvent event) { - for (AnimationEventListener listener : eventListeners) { - listener.onEventTriggered(this, event); - } - } - - // ==================== 工具方法 ==================== - - /** - * 获取当前播放进度(0-1) - */ - public float getProgress() { - if (currentClip == null || currentClip.getDuration() == 0) { - return 0.0f; - } - return currentTime / currentClip.getDuration(); - } - - /** - * 设置播放进度 - */ - public void setProgress(float progress) { - if (currentClip != null) { - currentTime = progress * currentClip.getDuration(); - } - } - - /** - * 跳转到指定时间 - */ - public void seek(float time) { - currentTime = Math.max(0.0f, time); - if (currentClip != null) { - currentTime = Math.min(currentTime, currentClip.getDuration()); - } - } - - /** - * 创建层的深拷贝 - */ - public AnimationLayer copy() { - AnimationLayer copy = new AnimationLayer(name + "_copy", weight); - copy.enabled = this.enabled; - copy.blendMode = this.blendMode; - copy.priority = this.priority; - copy.playbackSpeed = this.playbackSpeed; - copy.looping = this.looping; - - // 拷贝轨道 - for (AnimationTrack track : this.tracks.values()) { - copy.tracks.put(track.getParameterId(), track.copy()); - } - - // 拷贝剪辑(引用,因为剪辑通常是共享的) - copy.clips.addAll(this.clips); - - // 拷贝参数覆盖 - copy.parameterOverrides.putAll(this.parameterOverrides); - - return copy; - } - - // ==================== Getter/Setter ==================== - - public String getName() { - return name; - } - - public UUID getUuid() { - return uuid; - } - - public float getWeight() { - return weight; - } - - public void setWeight(float weight) { - this.weight = Math.max(0.0f, Math.min(1.0f, weight)); - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public BlendMode getBlendMode() { - return blendMode; - } - - public void setBlendMode(BlendMode blendMode) { - this.blendMode = blendMode; - } - - public int getPriority() { - return priority; - } - - public void setPriority(int priority) { - this.priority = priority; - } - - public Map getTracks() { - return Collections.unmodifiableMap(tracks); - } - - public List getClips() { - return Collections.unmodifiableList(clips); - } - - public AnimationClip getCurrentClip() { - return currentClip; - } - - public float getPlaybackSpeed() { - return playbackSpeed; - } - - public void setPlaybackSpeed(float playbackSpeed) { - this.playbackSpeed = Math.max(0.0f, playbackSpeed); - } - - public boolean isLooping() { - return looping; - } - - public void setLooping(boolean looping) { - this.looping = looping; - } - - public float getCurrentTime() { - return currentTime; - } - - public boolean isPlaying() { - return playing; - } - - public boolean isPaused() { - return paused; - } - - public Map getParameterOverrides() { - return Collections.unmodifiableMap(parameterOverrides); - } - - // ==================== 枚举和内部类 ==================== - - /** - * 混合模式枚举 - */ - public enum BlendMode { - OVERRIDE, // 覆盖混合 - ADDITIVE, // 叠加混合 - MULTIPLICATIVE, // 乘法混合 - AVERAGE // 平均混合 - } - - /** - * 动画轨道类 - */ - public static class AnimationTrack { - private final String parameterId; - private final List keyframes; - private boolean enabled; - private InterpolationType interpolation; - - public AnimationTrack(String parameterId) { - this.parameterId = parameterId; - this.keyframes = new ArrayList<>(); - this.enabled = true; - this.interpolation = InterpolationType.LINEAR; - } - - public void addKeyframe(float time, float value) { - addKeyframe(time, value, interpolation); - } - - public void addKeyframe(float time, float value, InterpolationType interpolation) { - Keyframe keyframe = new Keyframe(time, value, interpolation); - - // 按时间排序插入 - int index = 0; - while (index < keyframes.size() && keyframes.get(index).getTime() < time) { - index++; - } - keyframes.add(index, keyframe); - } - - public float sample(float time) { - if (keyframes.isEmpty()) { - return 0.0f; - } - - // 在第一个关键帧之前 - if (time <= keyframes.get(0).getTime()) { - return keyframes.get(0).getValue(); - } - - // 在最后一个关键帧之后 - if (time >= keyframes.get(keyframes.size() - 1).getTime()) { - return keyframes.get(keyframes.size() - 1).getValue(); - } - - // 找到包围时间的关键帧 - for (int i = 0; i < keyframes.size() - 1; i++) { - Keyframe kf1 = keyframes.get(i); - Keyframe kf2 = keyframes.get(i + 1); - - if (time >= kf1.getTime() && time <= kf2.getTime()) { - return interpolate(kf1, kf2, time); - } - } - - return 0.0f; - } - - private float interpolate(Keyframe kf1, Keyframe kf2, float time) { - float t = (time - kf1.getTime()) / (kf2.getTime() - kf1.getTime()); - - switch (kf1.getInterpolation()) { - case LINEAR: - return kf1.getValue() + (kf2.getValue() - kf1.getValue()) * t; - case STEP: - return kf1.getValue(); - case SMOOTH: - float t2 = t * t; - float t3 = t2 * t; - return kf1.getValue() * (2 * t3 - 3 * t2 + 1) + - kf2.getValue() * (-2 * t3 + 3 * t2); - case EASE_IN: - return kf1.getValue() + (kf2.getValue() - kf1.getValue()) * (t * t); - case EASE_OUT: - return kf1.getValue() + (kf2.getValue() - kf1.getValue()) * (1 - (1 - t) * (1 - t)); - default: - return kf1.getValue(); - } - } - - public AnimationTrack copy() { - AnimationTrack copy = new AnimationTrack(parameterId); - copy.enabled = this.enabled; - copy.interpolation = this.interpolation; - for (Keyframe kf : this.keyframes) { - copy.keyframes.add(kf.copy()); - } - return copy; - } - - // Getter/Setter - public String getParameterId() { - return parameterId; - } - - public List getKeyframes() { - return Collections.unmodifiableList(keyframes); - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public InterpolationType getInterpolation() { - return interpolation; - } - - public void setInterpolation(InterpolationType interpolation) { - this.interpolation = interpolation; - } - } - - /** - * 关键帧类 - */ - public static class Keyframe { - private final float time; - private final float value; - private final InterpolationType interpolation; - - public Keyframe(float time, float value, InterpolationType interpolation) { - this.time = time; - this.value = value; - this.interpolation = interpolation; - } - - public Keyframe copy() { - return new Keyframe(time, value, interpolation); - } - - // Getter - public float getTime() { - return time; - } - - public float getValue() { - return value; - } - - public InterpolationType getInterpolation() { - return interpolation; - } - } - - /** - * 插值类型枚举 - */ - public enum InterpolationType { - LINEAR, // 线性插值 - STEP, // 步进插值 - SMOOTH, // 平滑插值 - EASE_IN, // 缓入 - EASE_OUT // 缓出 - } - - /** - * 动画事件类 - */ - public static class AnimationEvent { - private final String name; - private final float time; - private final Runnable action; - private boolean triggered; - - public AnimationEvent(String name, float time, Runnable action) { - this.name = name; - this.time = time; - this.action = action; - this.triggered = false; - } - - public void trigger() { - if (!triggered && action != null) { - action.run(); - triggered = true; - } - } - - public void reset() { - triggered = false; - } - - // Getter - public String getName() { - return name; - } - - public float getTime() { - return time; - } - - public boolean isTriggered() { - return triggered; - } - } - - /** - * 动画事件监听器接口 - */ - public interface AnimationEventListener { - void onAnimationStarted(AnimationLayer layer, AnimationClip clip); - - void onAnimationStopped(AnimationLayer layer, AnimationClip clip); - - void onAnimationPaused(AnimationLayer layer, AnimationClip clip); - - void onAnimationResumed(AnimationLayer layer, AnimationClip clip); - - void onAnimationCompleted(AnimationLayer layer, AnimationClip clip); - - void onAnimationLooped(AnimationLayer layer, AnimationClip clip); - - void onEventTriggered(AnimationLayer layer, AnimationEvent event); - } - - /** - * 简单的动画事件监听器适配器 - */ - public static abstract class AnimationEventAdapter implements AnimationEventListener { - @Override - public void onAnimationStarted(AnimationLayer layer, AnimationClip clip) { - } - - @Override - public void onAnimationStopped(AnimationLayer layer, AnimationClip clip) { - } - - @Override - public void onAnimationPaused(AnimationLayer layer, AnimationClip clip) { - } - - @Override - public void onAnimationResumed(AnimationLayer layer, AnimationClip clip) { - } - - @Override - public void onAnimationCompleted(AnimationLayer layer, AnimationClip clip) { - } - - @Override - public void onAnimationLooped(AnimationLayer layer, AnimationClip clip) { - } - - @Override - public void onEventTriggered(AnimationLayer layer, AnimationEvent event) { - } - } - - // ==================== Object 方法 ==================== - - @Override - public String toString() { - return "AnimationLayer{" + - "name='" + name + '\'' + - ", weight=" + weight + - ", enabled=" + enabled + - ", blendMode=" + blendMode + - ", tracks=" + tracks.size() + - ", clips=" + clips.size() + - ", playing=" + playing + - '}'; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/BoundingBox.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/BoundingBox.java deleted file mode 100644 index 501bf03..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/BoundingBox.java +++ /dev/null @@ -1,649 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; -import org.joml.Matrix3f; -import org.joml.Vector2f; - -import java.util.Objects; - -/** - * 2D边界框类,用于表示和管理2D对象的轴对齐边界框(AABB) - * 支持变换、合并、相交检测等操作 - * - * @author tzdwindows 7 - */ -public class BoundingBox { - // ==================== 边界数据 ==================== - private float minX; - private float minY; - private float maxX; - private float maxY; - - // ==================== 状态标记 ==================== - private boolean valid; - - // ==================== 构造器 ==================== - - /** - * 创建未初始化的边界框 - */ - public BoundingBox() { - reset(); - } - - /** - * 从最小/最大值创建边界框 - */ - public BoundingBox(float minX, float minY, float maxX, float maxY) { - set(minX, minY, maxX, maxY); - } - - /** - * 从两个点创建边界框 - */ - public BoundingBox(Vector2f point1, Vector2f point2) { - set(point1, point2); - } - - /** - * 拷贝构造器 - */ - public BoundingBox(BoundingBox other) { - set(other); - } - - /** - * 从点数组创建边界框 - */ - public BoundingBox(Vector2f[] points) { - set(points); - } - - /** - * 从顶点数组创建边界框 [x0, y0, x1, y1, ...] - */ - public BoundingBox(float[] vertices) { - set(vertices); - } - - // ==================== 设置方法 ==================== - - /** - * 重置为无效状态 - */ - public void reset() { - minX = Float.MAX_VALUE; - minY = Float.MAX_VALUE; - maxX = -Float.MAX_VALUE; - maxY = -Float.MAX_VALUE; - valid = false; - } - - /** - * 设置边界值 - */ - public void set(float minX, float minY, float maxX, float maxY) { - if (minX > maxX || minY > maxY) { - throw new IllegalArgumentException("Min values must be less than or equal to max values"); - } - - this.minX = minX; - this.minY = minY; - this.maxX = maxX; - this.maxY = maxY; - this.valid = true; - } - - /** - * 从两个点设置边界框 - */ - public void set(Vector2f point1, Vector2f point2) { - reset(); - expand(point1); - expand(point2); - } - - /** - * 从另一个边界框设置 - */ - public void set(BoundingBox other) { - if (!other.isValid()) { - reset(); - return; - } - - this.minX = other.minX; - this.minY = other.minY; - this.maxX = other.maxX; - this.maxY = other.maxY; - this.valid = true; - } - - /** - * 从点数组设置边界框 - */ - public void set(Vector2f[] points) { - reset(); - if (points != null) { - for (Vector2f point : points) { - if (point != null) { - expand(point); - } - } - } - } - - /** - * 从顶点数组设置边界框 [x0, y0, x1, y1, ...] - */ - public void set(float[] vertices) { - reset(); - if (vertices != null) { - if (vertices.length % 2 != 0) { - throw new IllegalArgumentException("Vertices array must have even length"); - } - for (int i = 0; i < vertices.length; i += 2) { - expand(vertices[i], vertices[i + 1]); - } - } - } - - // ==================== 扩展方法 ==================== - - /** - * 扩展边界框以包含点 - */ - public void expand(float x, float y) { - if (!valid) { - minX = maxX = x; - minY = maxY = y; - valid = true; - } else { - minX = Math.min(minX, x); - minY = Math.min(minY, y); - maxX = Math.max(maxX, x); - maxY = Math.max(maxY, y); - } - } - - - public void expand(Vector2f point) { - if (point != null) { - expand(point.x, point.y); - } - } - - /** - * 扩展边界框以包含另一个边界框 - */ - public void expand(BoundingBox other) { - if (!other.isValid()) { - return; - } - - if (!valid) { - set(other); - } else { - minX = Math.min(minX, other.minX); - minY = Math.min(minY, other.minY); - maxX = Math.max(maxX, other.maxX); - maxY = Math.max(maxY, other.maxY); - } - } - - /** - * 扩展边界框以包含点数组 - */ - public void expand(Vector2f[] points) { - if (points != null) { - for (Vector2f point : points) { - if (point != null) { - expand(point); - } - } - } - } - - /** - * 扩展边界框以包含顶点数组 [x0, y0, x1, y1, ...] - */ - public void expand(float[] vertices) { - if (vertices != null) { - if (vertices.length % 2 != 0) { - throw new IllegalArgumentException("Vertices array must have even length"); - } - for (int i = 0; i < vertices.length; i += 2) { - expand(vertices[i], vertices[i + 1]); - } - } - } - - // ==================== 变换方法 ==================== - - /** - * 应用矩阵变换到边界框 - */ - public BoundingBox transform(Matrix3f matrix) { - if (!valid) { - return new BoundingBox(); - } - - // 变换边界框的四个角点 - Vector2f[] corners = getCorners(); - BoundingBox result = new BoundingBox(); - - for (Vector2f corner : corners) { - Vector2f transformed = Matrix3fUtils.transformPoint(matrix, corner); - result.expand(transformed); - } - - return result; - } - - /** - * 应用平移变换 - */ - public BoundingBox translate(float dx, float dy) { - if (!valid) { - return new BoundingBox(); - } - - return new BoundingBox( - minX + dx, minY + dy, - maxX + dx, maxY + dy - ); - } - - public BoundingBox translate(Vector2f translation) { - return translate(translation.x, translation.y); - } - - /** - * 应用缩放变换 - */ - public BoundingBox scale(float sx, float sy) { - if (!valid) { - return new BoundingBox(); - } - - return new BoundingBox( - minX * sx, minY * sy, - maxX * sx, maxY * sy - ); - } - - public BoundingBox scale(float scale) { - return scale(scale, scale); - } - - public BoundingBox scale(Vector2f scale) { - return scale(scale.x, scale.y); - } - - // ==================== 几何计算 ==================== - - /** - * 获取边界框的四个角点 - */ - public Vector2f[] getCorners() { - if (!valid) { - return new Vector2f[0]; - } - - return new Vector2f[]{ - new Vector2f(minX, minY), // 左下 - new Vector2f(maxX, minY), // 右下 - new Vector2f(maxX, maxY), // 右上 - new Vector2f(minX, maxY) // 左上 - }; - } - - /** - * 获取边界框中心点 - */ - public Vector2f getCenter() { - if (!valid) { - return new Vector2f(); - } - - return new Vector2f( - (minX + maxX) * 0.5f, - (minY + maxY) * 0.5f - ); - } - - /** - * 获取边界框尺寸 - */ - public Vector2f getSize() { - if (!valid) { - return new Vector2f(); - } - - return new Vector2f(getWidth(), getHeight()); - } - - /** - * 获取边界框半尺寸(半径) - */ - public Vector2f getHalfSize() { - if (!valid) { - return new Vector2f(); - } - - return new Vector2f(getWidth() * 0.5f, getHeight() * 0.5f); - } - - /** - * 计算边界框面积 - */ - public float getArea() { - if (!valid) { - return 0.0f; - } - - return getWidth() * getHeight(); - } - - /** - * 计算边界框周长 - */ - public float getPerimeter() { - if (!valid) { - return 0.0f; - } - - return 2.0f * (getWidth() + getHeight()); - } - - // ==================== 相交检测 ==================== - - /** - * 检查是否包含点 - */ - public boolean contains(float x, float y) { - if (!valid) { - return false; - } - - return x >= minX && x <= maxX && y >= minY && y <= maxY; - } - - public boolean contains(Vector2f point) { - if (point == null) return false; - return contains(point.x, point.y); - } - - /** - * 检查是否完全包含另一个边界框 - */ - public boolean contains(BoundingBox other) { - if (!valid || !other.isValid()) { - return false; - } - - return other.minX >= minX && other.maxX <= maxX && - other.minY >= minY && other.maxY <= maxY; - } - - /** - * 检查是否与另一个边界框相交 - */ - public boolean intersects(BoundingBox other) { - if (!valid || !other.isValid()) { - return false; - } - - return !(other.maxX < minX || other.minX > maxX || - other.maxY < minY || other.minY > maxY); - } - - /** - * 计算与另一个边界框的交集 - */ - public BoundingBox intersection(BoundingBox other) { - if (!intersects(other)) { - return new BoundingBox(); // 返回无效边界框 - } - - return new BoundingBox( - Math.max(minX, other.minX), - Math.max(minY, other.minY), - Math.min(maxX, other.maxX), - Math.min(maxY, other.maxY) - ); - } - - /** - * 计算与另一个边界框的并集 - */ - public BoundingBox union(BoundingBox other) { - BoundingBox result = new BoundingBox(this); - result.expand(other); - return result; - } - - /** - * 计算两个边界框的合并边界框 - */ - public static BoundingBox merge(BoundingBox box1, BoundingBox box2) { - return box1.union(box2); - } - - // ==================== 工具方法 ==================== - - /** - * 对边界框进行膨胀(扩展固定距离) - */ - public BoundingBox inflate(float amount) { - return inflate(amount, amount); - } - - public BoundingBox inflate(float dx, float dy) { - if (!valid) { - return new BoundingBox(); - } - - return new BoundingBox( - minX - dx, minY - dy, - maxX + dx, maxY + dy - ); - } - - /** - * 对边界框进行收缩(缩小固定距离) - */ - public BoundingBox deflate(float amount) { - return deflate(amount, amount); - } - - public BoundingBox deflate(float dx, float dy) { - if (!valid) { - return new BoundingBox(); - } - - float newMinX = minX + dx; - float newMinY = minY + dy; - float newMaxX = maxX - dx; - float newMaxY = maxY - dy; - - // 检查收缩后是否仍然有效 - if (newMinX > newMaxX || newMinY > newMaxY) { - return new BoundingBox(); // 返回无效边界框 - } - - return new BoundingBox(newMinX, newMinY, newMaxX, newMaxY); - } - - /** - * 将边界框对齐到网格 - */ - public BoundingBox alignToGrid(float gridSize) { - if (!valid) { - return new BoundingBox(); - } - - float alignedMinX = (float) Math.floor(minX / gridSize) * gridSize; - float alignedMinY = (float) Math.floor(minY / gridSize) * gridSize; - float alignedMaxX = (float) Math.ceil(maxX / gridSize) * gridSize; - float alignedMaxY = (float) Math.ceil(maxY / gridSize) * gridSize; - - return new BoundingBox(alignedMinX, alignedMinY, alignedMaxX, alignedMaxY); - } - - /** - * 计算到点的最近距离 - */ - public float distanceTo(float x, float y) { - if (!valid) { - return Float.MAX_VALUE; - } - - if (contains(x, y)) { - return 0.0f; - } - - float dx = Math.max(Math.max(minX - x, 0), x - maxX); - float dy = Math.max(Math.max(minY - y, 0), y - maxY); - - return (float) Math.sqrt(dx * dx + dy * dy); - } - - public float distanceTo(Vector2f point) { - if (point == null) return Float.MAX_VALUE; - return distanceTo(point.x, point.y); - } - - // ==================== Getter方法 ==================== - - public float getMinX() { - return minX; - } - - public float getMinY() { - return minY; - } - - public float getMaxX() { - return maxX; - } - - public float getMaxY() { - return maxY; - } - - public float getWidth() { - return valid ? maxX - minX : 0.0f; - } - - public float getHeight() { - return valid ? maxY - minY : 0.0f; - } - - public float getLeft() { - return minX; - } - - public float getRight() { - return maxX; - } - - public float getBottom() { - return minY; - } - - public float getTop() { - return maxY; - } - - public boolean isValid() { - return valid; - } - - // ==================== 静态工厂方法 ==================== - - /** - * 从点数组创建边界框 - */ - public static BoundingBox fromPoints(Vector2f[] points) { - return new BoundingBox(points); - } - - /** - * 从顶点数组创建边界框 - */ - public static BoundingBox fromVertices(float[] vertices) { - return new BoundingBox(vertices); - } - - /** - * 创建包含所有边界框的合并边界框 - */ - public static BoundingBox mergeAll(BoundingBox... boxes) { - BoundingBox result = new BoundingBox(); - for (BoundingBox box : boxes) { - if (box != null && box.isValid()) { - result.expand(box); - } - } - return result; - } - - // ==================== Object方法 ==================== - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - BoundingBox that = (BoundingBox) o; - - if (valid != that.valid) return false; - if (!valid) return true; // 两个无效边界框视为相等 - - return Float.compare(that.minX, minX) == 0 && - Float.compare(that.minY, minY) == 0 && - Float.compare(that.maxX, maxX) == 0 && - Float.compare(that.maxY, maxY) == 0; - } - - @Override - public int hashCode() { - if (!valid) { - return Objects.hash(valid); - } - return Objects.hash(minX, minY, maxX, maxY, valid); - } - - @Override - public String toString() { - if (!valid) { - return "BoundingBox{INVALID}"; - } - - return String.format("BoundingBox{min=(%.2f, %.2f), max=(%.2f, %.2f), size=(%.2f, %.2f)}", - minX, minY, maxX, maxY, getWidth(), getHeight()); - } - - /** - * 创建边界框的深拷贝 - */ - public BoundingBox copy() { - return new BoundingBox(this); - } - - /** - * 获取包围盒的中心点 X 坐标。 - * @return 中心点的 X 坐标 - */ - public float getCenterX() { - return (this.minX + this.maxX) * 0.5f; - } - - /** - * 获取包围盒的中心点 Y 坐标。 - * @return 中心点的 Y 坐标 - */ - public float getCenterY() { - return (this.minY + this.maxY) * 0.5f; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Deformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Deformer.java deleted file mode 100644 index 5a95b9d..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Deformer.java +++ /dev/null @@ -1,304 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import org.joml.Vector2f; - -import java.util.*; - -/** - * 2D网格变形器基类 - * 支持顶点变形、参数驱动动画等特性 - * - * @author tzdwindows 7 - */ -public abstract class Deformer { - // ==================== 基础属性 ==================== - protected String name; - protected String id; - protected boolean enabled = true; - protected float weight = 1.0f; - - // ==================== 驱动参数 ==================== - protected final Set drivenParameters; - protected final Map parameterValues; - - // ==================== 变形范围 ==================== - protected DeformationRange range; - protected BlendMode blendMode = BlendMode.REPLACE; - - // ==================== 构造器 ==================== - - public Deformer() { - this("unnamed"); - } - - public Deformer(String name) { - this.name = name; - this.id = UUID.randomUUID().toString(); - this.drivenParameters = new HashSet<>(); - this.parameterValues = new HashMap<>(); - this.range = new DeformationRange(); - } - - // ==================== 抽象方法 ==================== - - /** - * 应用变形到指定网格 - */ - public abstract void applyToMesh(Mesh2D mesh); - - /** - * 应用参数值到变形器 - */ - public abstract void apply(float value); - - /** - * 重置变形器状态 - */ - public abstract void reset(); - - /** - * 序列化参数 - */ - public abstract void serialization(Map map); - - /** - * 序列化参数 - */ - public abstract void deserialize(Map map); - - // ==================== 参数驱动系统 ==================== - - /** - * 检查是否由指定参数驱动 - */ - public boolean isDrivenBy(String paramId) { - return drivenParameters.contains(paramId); - } - - /** - * 添加驱动参数 - */ - public void addDrivenParameter(String paramId) { - drivenParameters.add(paramId); - } - - /** - * 移除驱动参数 - */ - public void removeDrivenParameter(String paramId) { - drivenParameters.remove(paramId); - parameterValues.remove(paramId); - } - - /** - * 设置参数值 - */ - public void setParameterValue(String paramId, float value) { - if (drivenParameters.contains(paramId)) { - parameterValues.put(paramId, value); - } - } - - /** - * 获取参数值 - */ - public float getParameterValue(String paramId) { - return parameterValues.getOrDefault(paramId, 0.0f); - } - - /** - * 应用所有参数到变形器 - */ - public void applyAllParameters() { - for (Map.Entry entry : parameterValues.entrySet()) { - apply(entry.getValue()); - } - } - - // ==================== 工具方法 ==================== - - /** - * 计算变形权重(考虑全局权重和范围衰减) - */ - protected float computeDeformationWeight(float x, float y) { - if (!enabled || weight <= 0.0f) { - return 0.0f; - } - - float rangeWeight = range.computeWeight(x, y); - return weight * rangeWeight; - } - - /** - * 混合顶点位置 - */ - protected void blendVertexPosition(float[] vertices, int vertexIndex, - float originalX, float originalY, - float deformedX, float deformedY, float weight) { - if (weight <= 0.0f) { - return; // 保持原位置 - } - - int baseIndex = vertexIndex * 2; - - if (weight >= 1.0f) { - vertices[baseIndex] = deformedX; - vertices[baseIndex + 1] = deformedY; - return; - } - - switch (blendMode) { - case ADDITIVE: - vertices[baseIndex] += (deformedX - originalX) * weight; - vertices[baseIndex + 1] += (deformedY - originalY) * weight; - break; - case MULTIPLY: - vertices[baseIndex] *= (1.0f + (deformedX / originalX - 1.0f) * weight); - vertices[baseIndex + 1] *= (1.0f + (deformedY / originalY - 1.0f) * weight); - break; - case REPLACE: - default: - vertices[baseIndex] = originalX + (deformedX - originalX) * weight; - vertices[baseIndex + 1] = originalY + (deformedY - originalY) * weight; - break; - } - } - - // ==================== Getter/Setter ==================== - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getId() { - return id; - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public float getWeight() { - return weight; - } - - public void setWeight(float weight) { - this.weight = Math.max(0.0f, Math.min(1.0f, weight)); - } - - public Set getDrivenParameters() { - return new HashSet<>(drivenParameters); - } - - public DeformationRange getRange() { - return range; - } - - public void setRange(DeformationRange range) { - this.range = range; - } - - public BlendMode getBlendMode() { - return blendMode; - } - - public void setBlendMode(BlendMode blendMode) { - this.blendMode = blendMode; - } - - // ==================== 枚举和内部类 ==================== - - /** - * 变形混合模式 - */ - public enum BlendMode { - REPLACE, // 替换原始位置 - ADDITIVE, // 叠加变形 - MULTIPLY // 乘法变形 - } - - /** - * 变形范围控制 - */ - public static class DeformationRange { - private final Vector2f center = new Vector2f(0, 0); - private float radius = 100.0f; - private float innerRadius = 0.0f; - private float falloff = 2.0f; - - public DeformationRange() { - } - - public DeformationRange(Vector2f center, float radius) { - this.center.set(center); - this.radius = radius; - } - - /** - * 计算顶点在变形范围内的权重 - */ - public float computeWeight(float x, float y) { - float dx = x - center.x; - float dy = y - center.y; - float distance = (float) Math.sqrt(dx * dx + dy * dy); - - if (distance <= innerRadius) { - return 1.0f; - } - - if (distance >= radius) { - return 0.0f; - } - - // 使用平滑衰减函数 - float normalized = (distance - innerRadius) / (radius - innerRadius); - return (float) Math.pow(1.0f - normalized, falloff); - } - - // Getter/Setter - public Vector2f getCenter() { - return new Vector2f(center); - } - - public void setCenter(Vector2f center) { - this.center.set(center); - } - - public void setCenter(float x, float y) { - this.center.set(x, y); - } - - public float getRadius() { - return radius; - } - - public void setRadius(float radius) { - this.radius = radius; - } - - public float getInnerRadius() { - return innerRadius; - } - - public void setInnerRadius(float innerRadius) { - this.innerRadius = innerRadius; - } - - public float getFalloff() { - return falloff; - } - - public void setFalloff(float falloff) { - this.falloff = falloff; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/LightSource.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/LightSource.java deleted file mode 100644 index 853aad1..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/LightSource.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import org.joml.Vector2f; -import org.joml.Vector3f; - -import java.awt.*; - -/** - * 光源系统 - * - * @author tzdwindows 7 - */ -public class LightSource { - private final Vector2f position; - private final Vector3f color; - private final float intensity; - private boolean enabled = true; - private boolean isAmbient = false; // 是否为环境光 - - // ---- 辉光(glow / bloom-like)支持 ---- - private boolean isGlow = false; // 是否产生辉光 - private Vector2f glowDirection = new Vector2f(0f, 0f); // 方向性辉光方向(可为 0 向量表示无方向) - private float glowIntensity = 0f; // 辉光的强度系数(影响亮度) - private float glowRadius = 50f; // 辉光影响半径(像素/单位) - private float glowAmount = 1.0f; // 辉光权重 / 整体强度放大器 - - public LightSource(Vector2f pos, Color color, float intensity) { - this.position = pos; - this.color = colorToVector3f(color); - this.intensity = intensity; - } - - // 环境光构造函数 - public LightSource(Color color, float intensity) { - this.position = new Vector2f(0, 0); - this.color = colorToVector3f(color); - this.intensity = intensity; - this.isAmbient = true; - } - - public static Vector3f colorToVector3f(Color color) { - if (color == null) return new Vector3f(1, 1, 1); - return new Vector3f( - color.getRed() / 255.0f, - color.getGreen() / 255.0f, - color.getBlue() / 255.0f - ); - } - - public static Color vector3fToColor(Vector3f colorVec) { - if (colorVec == null) return new java.awt.Color(255, 255, 255); - - float r = Math.min(1.0f, Math.max(0.0f, colorVec.x)); - float g = Math.min(1.0f, Math.max(0.0f, colorVec.y)); - float b = Math.min(1.0f, Math.max(0.0f, colorVec.z)); - - int red = (int) (r * 255 + 0.5f); - int green = (int) (g * 255 + 0.5f); - int blue = (int) (b * 255 + 0.5f); - - return new Color(red, green, blue); - } - - public Vector2f getPosition() { - return position; - } - - public Vector3f getColor() { - return color; - } - - public float getIntensity() { - return intensity; - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - // 判断是否为环境光 - public boolean isAmbient() { - return isAmbient; - } - - public void setAmbient(boolean ambient) { - this.isAmbient = ambient; - } - - // ---- 辉光相关的 getter / setter ---- - public boolean isGlow() { - return isGlow; - } - - public void setGlow(boolean glow) { - this.isGlow = glow; - } - - public Vector2f getGlowDirection() { - return glowDirection; - } - - public void setGlowDirection(Vector2f glowDirection) { - this.glowDirection = glowDirection != null ? glowDirection : new Vector2f(0f, 0f); - } - - public float getGlowIntensity() { - return glowIntensity; - } - - public void setGlowIntensity(float glowIntensity) { - this.glowIntensity = glowIntensity; - } - - public float getGlowRadius() { - return glowRadius; - } - - public void setGlowRadius(float glowRadius) { - this.glowRadius = glowRadius; - } - - public float getGlowAmount() { - return glowAmount; - } - - public void setGlowAmount(float glowAmount) { - this.glowAmount = glowAmount; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelPose.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelPose.java deleted file mode 100644 index 7ae78b3..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/ModelPose.java +++ /dev/null @@ -1,451 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import org.joml.Vector2f; -import org.joml.Vector3f; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -/** - * 模型姿态类 - 用于存储和管理2D模型的部件变换状态 - * 支持动画系统、姿态保存/恢复、姿态混合等功能 - * - * @author tzdwindows 7 - */ -public class ModelPose { - - // ================== 内部类:部件姿态 ================== - - /** - * 单个部件的姿态数据 - */ - public static class PartPose { - private final Vector2f position; - private float rotation; - private final Vector2f scale; - private float opacity; - private boolean visible; - private final Vector3f color; // RGB颜色乘数 - - public PartPose() { - this(new Vector2f(0, 0), 0.0f, new Vector2f(1, 1), 1.0f, true, new Vector3f(1, 1, 1)); - } - - public PartPose(Vector2f position, float rotation, Vector2f scale, - float opacity, boolean visible, Vector3f color) { - this.position = new Vector2f(position); - this.rotation = rotation; - this.scale = new Vector2f(scale); - this.opacity = opacity; - this.visible = visible; - this.color = new Vector3f(color); - } - - public PartPose(PartPose other) { - this.position = new Vector2f(other.position); - this.rotation = other.rotation; - this.scale = new Vector2f(other.scale); - this.opacity = other.opacity; - this.visible = other.visible; - this.color = new Vector3f(other.color); - } - - // ================== 线性插值方法 ================== - - /** - * 在两个部件姿态间进行线性插值 - */ - public static PartPose lerp(PartPose a, PartPose b, float alpha) { - alpha = Math.max(0.0f, Math.min(1.0f, alpha)); // 钳制到[0,1] - - Vector2f pos = new Vector2f(a.position).lerp(b.position, alpha); - float rot = a.rotation + (b.rotation - a.rotation) * alpha; - Vector2f scl = new Vector2f(a.scale).lerp(b.scale, alpha); - float opa = a.opacity + (b.opacity - a.opacity) * alpha; - Vector3f col = new Vector3f(a.color).lerp(b.color, alpha); - - // 可见性:当alpha>0.5时使用b的可见性 - boolean vis = alpha < 0.5f ? a.visible : b.visible; - - return new PartPose(pos, rot, scl, opa, vis, col); - } - - /** - * 带旋转正确插值的线性插值(处理360°边界) - */ - public static PartPose lerpWithRotation(PartPose a, PartPose b, float alpha) { - alpha = Math.max(0.0f, Math.min(1.0f, alpha)); - - Vector2f pos = new Vector2f(a.position).lerp(b.position, alpha); - - // 处理旋转插值的角度环绕问题 - float shortestAngle = ((b.rotation - a.rotation) % 360 + 540) % 360 - 180; - float rot = a.rotation + shortestAngle * alpha; - - Vector2f scl = new Vector2f(a.scale).lerp(b.scale, alpha); - float opa = a.opacity + (b.opacity - a.opacity) * alpha; - Vector3f col = new Vector3f(a.color).lerp(b.color, alpha); - boolean vis = alpha < 0.5f ? a.visible : b.visible; - - return new PartPose(pos, rot, scl, opa, vis, col); - } - - // ================== Getter和Setter ================== - - public Vector2f getPosition() { - return new Vector2f(position); - } - - public void setPosition(Vector2f position) { - this.position.set(position); - } - - public float getRotation() { - return rotation; - } - - public void setRotation(float rotation) { - this.rotation = rotation; - } - - public Vector2f getScale() { - return new Vector2f(scale); - } - - public void setScale(Vector2f scale) { - this.scale.set(scale); - } - - public float getOpacity() { - return opacity; - } - - public void setOpacity(float opacity) { - this.opacity = opacity; - } - - public boolean isVisible() { - return visible; - } - - public void setVisible(boolean visible) { - this.visible = visible; - } - - public Vector3f getColor() { - return new Vector3f(color); - } - - public void setColor(Vector3f color) { - this.color.set(color); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PartPose partPose = (PartPose) o; - return Float.compare(partPose.rotation, rotation) == 0 && - Float.compare(partPose.opacity, opacity) == 0 && - visible == partPose.visible && - Objects.equals(position, partPose.position) && - Objects.equals(scale, partPose.scale) && - Objects.equals(color, partPose.color); - } - - @Override - public int hashCode() { - return Objects.hash(position, rotation, scale, opacity, visible, color); - } - - @Override - public String toString() { - return String.format("PartPose{pos=(%.2f,%.2f), rot=%.2f, scale=(%.2f,%.2f), opacity=%.2f, visible=%s, color=(%.2f,%.2f,%.2f)}", - position.x, position.y, rotation, scale.x, scale.y, opacity, visible, color.x, color.y, color.z); - } - } - - // ================== ModelPose 主体 ================== - - private String name; - private final Map partPoses; - private float blendTime = 0.3f; // 默认混合时间(秒) - private boolean isDefaultPose = false; - - // ================== 构造函数 ================== - - public ModelPose() { - this("Unnamed Pose"); - } - - public ModelPose(String name) { - this.name = name; - this.partPoses = new HashMap<>(); - } - - public ModelPose(ModelPose other) { - this.name = other.name + " (Copy)"; - this.partPoses = new HashMap<>(); - this.blendTime = other.blendTime; - this.isDefaultPose = other.isDefaultPose; - - // 深拷贝所有部件姿态 - for (Map.Entry entry : other.partPoses.entrySet()) { - this.partPoses.put(entry.getKey(), new PartPose(entry.getValue())); - } - } - - // ================== 姿态管理方法 ================== - - /** - * 设置指定部件的姿态 - */ - public void setPartPose(String partName, PartPose pose) { - partPoses.put(partName, new PartPose(pose)); - } - - /** - * 获取指定部件的姿态(如果不存在则创建默认姿态) - */ - public PartPose getPartPose(String partName) { - return partPoses.computeIfAbsent(partName, k -> new PartPose()); - } - - /** - * 检查是否包含指定部件的姿态 - */ - public boolean hasPartPose(String partName) { - return partPoses.containsKey(partName); - } - - /** - * 移除指定部件的姿态 - */ - public PartPose removePartPose(String partName) { - return partPoses.remove(partName); - } - - /** - * 获取所有部件名称 - */ - public java.util.Set getPartNames() { - return partPoses.keySet(); - } - - /** - * 清空所有部件姿态 - */ - public void clear() { - partPoses.clear(); - } - - /** - * 获取部件数量 - */ - public int getPartCount() { - return partPoses.size(); - } - - // ================== 便捷方法 ================== - - /** - * 设置部件位置 - */ - public void setPartPosition(String partName, Vector2f position) { - getPartPose(partName).setPosition(position); - } - - /** - * 设置部件旋转(角度) - */ - public void setPartRotation(String partName, float rotation) { - getPartPose(partName).setRotation(rotation); - } - - /** - * 设置部件缩放 - */ - public void setPartScale(String partName, Vector2f scale) { - getPartPose(partName).setScale(scale); - } - - /** - * 设置部件不透明度 - */ - public void setPartOpacity(String partName, float opacity) { - getPartPose(partName).setOpacity(opacity); - } - - /** - * 设置部件可见性 - */ - public void setPartVisible(String partName, boolean visible) { - getPartPose(partName).setVisible(visible); - } - - /** - * 设置部件颜色 - */ - public void setPartColor(String partName, Vector3f color) { - getPartPose(partName).setColor(color); - } - - // ================== 姿态混合 ================== - - /** - * 在两个姿态间进行线性插值 - */ - public static ModelPose lerp(ModelPose a, ModelPose b, float alpha, String resultName) { - ModelPose result = new ModelPose(resultName); - result.setBlendTime(a.blendTime + (b.blendTime - a.blendTime) * alpha); - - // 合并两个姿态的所有部件 - java.util.Set allParts = new java.util.HashSet<>(); - allParts.addAll(a.getPartNames()); - allParts.addAll(b.getPartNames()); - - for (String partName : allParts) { - PartPose poseA = a.partPoses.get(partName); - PartPose poseB = b.partPoses.get(partName); - - if (poseA != null && poseB != null) { - // 两个姿态都有该部件:插值 - result.setPartPose(partName, PartPose.lerpWithRotation(poseA, poseB, alpha)); - } else if (poseA != null) { - // 只有姿态A有该部件:根据alpha决定是否保留 - if (alpha < 0.5f) { - result.setPartPose(partName, new PartPose(poseA)); - } - } else if (poseB != null) { - // 只有姿态B有该部件:根据alpha决定是否保留 - if (alpha >= 0.5f) { - result.setPartPose(partName, new PartPose(poseB)); - } - } - } - - return result; - } - - /** - * 将当前姿态与另一个姿态混合 - */ - public void blendWith(ModelPose other, float alpha) { - Map newPoses = new HashMap<>(); - java.util.Set allParts = new java.util.HashSet<>(); - allParts.addAll(this.getPartNames()); - allParts.addAll(other.getPartNames()); - - for (String partName : allParts) { - PartPose thisPose = this.partPoses.get(partName); - PartPose otherPose = other.partPoses.get(partName); - - if (thisPose != null && otherPose != null) { - newPoses.put(partName, PartPose.lerpWithRotation(thisPose, otherPose, alpha)); - } else if (thisPose != null && alpha < 0.5f) { - newPoses.put(partName, new PartPose(thisPose)); - } else if (otherPose != null && alpha >= 0.5f) { - newPoses.put(partName, new PartPose(otherPose)); - } - } - - this.partPoses.clear(); - this.partPoses.putAll(newPoses); - } - - // ================== 预设姿态工厂方法 ================== - - /** - * 创建默认姿态(所有部件在原点,无旋转,正常缩放) - */ - public static ModelPose createDefaultPose() { - ModelPose pose = new ModelPose("Default Pose"); - pose.isDefaultPose = true; - return pose; - } - - /** - * 创建隐藏姿态(所有部件不可见) - */ - public static ModelPose createHiddenPose() { - ModelPose pose = new ModelPose("Hidden Pose"); - pose.isDefaultPose = false; - // 不预先添加任何部件,使用时自动创建为隐藏状态 - return pose; - } - - /** - * 创建缩放姿态(统一缩放所有部件) - */ - public static ModelPose createScaledPose(float scaleX, float scaleY) { - ModelPose pose = new ModelPose("Scaled Pose"); - pose.isDefaultPose = false; - // 不预先添加部件,使用时自动创建带缩放的姿态 - return pose; - } - - // ================== Getter和Setter ================== - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public float getBlendTime() { - return blendTime; - } - - public void setBlendTime(float blendTime) { - this.blendTime = Math.max(0, blendTime); - } - - public boolean isDefaultPose() { - return isDefaultPose; - } - - public void setDefaultPose(boolean defaultPose) { - isDefaultPose = defaultPose; - } - - // ================== 工具方法 ================== - - /** - * 检查姿态是否为空(不包含任何部件姿态) - */ - public boolean isEmpty() { - return partPoses.isEmpty(); - } - - /** - * 获取姿态的简要描述 - */ - public String getDescription() { - return String.format("ModelPose{name='%s', parts=%d, blendTime=%.2fs}", - name, partPoses.size(), blendTime); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ModelPose modelPose = (ModelPose) o; - return Float.compare(modelPose.blendTime, blendTime) == 0 && - isDefaultPose == modelPose.isDefaultPose && - Objects.equals(name, modelPose.name) && - Objects.equals(partPoses, modelPose.partPoses); - } - - @Override - public int hashCode() { - return Objects.hash(name, partPoses, blendTime, isDefaultPose); - } - - @Override - public String toString() { - return getDescription(); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/PhysicsSystem.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/PhysicsSystem.java deleted file mode 100644 index 01c0d58..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/PhysicsSystem.java +++ /dev/null @@ -1,1286 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import org.joml.Vector2f; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 2D物理系统,用于处理模型的物理模拟 - * 支持弹簧系统、碰撞检测、重力等物理效果 - *

- * 调整点: - * - 使用半隐式(symplectic)Euler 积分,替代之前的纯 Verlet(更稳定且直观) - * - 在约束迭代后同步速度(根据位置变化计算),避免约束把位置推回后速度不一致导致僵化 - * - 在碰撞处理里同时更新位置与速度(冲量响应),并避免在碰撞后覆盖冲量 - * - 约束迭代次数可调,布料等需要多个迭代(默认 3) - * - * @author tzdwindows 7 - */ -public class PhysicsSystem { - // ==================== 物理参数 ==================== - private final Vector2f gravity; - private float airResistance; - private float timeScale; - private boolean enabled; - - // ==================== 物理组件 ==================== - private final Map particles; - private final List springs; - private final List constraints; - private final List colliders; - - // ==================== 状态管理 ==================== - private boolean initialized; - private long lastUpdateTime; - private float accumulatedTime; - private final int maxSubSteps; - private final float fixedTimeStep; - - // ==================== 性能统计 ==================== - private int updateCount; - private float averageUpdateTime; - - // ==================== 风力参数 ==================== - private final Vector2f windForce; - private boolean windEnabled; - - // ==================== 构造器 ==================== - - public PhysicsSystem() { - this.gravity = new Vector2f(0.0f, -98.0f); // 默认重力(单位可调) - this.airResistance = 0.1f; - this.timeScale = 1.0f; - this.enabled = true; - - this.particles = new ConcurrentHashMap<>(); - this.springs = new ArrayList<>(); - this.constraints = new ArrayList<>(); - this.colliders = new ArrayList<>(); - - this.initialized = false; - this.lastUpdateTime = System.nanoTime(); - this.accumulatedTime = 0.0f; - this.maxSubSteps = 5; - this.fixedTimeStep = 1.0f / 60.0f; // 60 FPS物理更新 - - this.updateCount = 0; - this.averageUpdateTime = 0.0f; - - this.windForce = new Vector2f(0.0f, 0.0f); // 默认无风 - this.windEnabled = false; - } - - // ==================== 初始化方法 ==================== - - /** - * 初始化物理系统 - */ - public void initialize() { - if (initialized) return; - - reset(); - initialized = true; - } - - /** - * 重置物理系统 - */ - public void reset() { - particles.clear(); - springs.clear(); - constraints.clear(); - colliders.clear(); - - lastUpdateTime = System.nanoTime(); - accumulatedTime = 0.0f; - updateCount = 0; - averageUpdateTime = 0.0f; - } - - // ==================== 粒子管理 ==================== - - /** - * 添加物理粒子 - */ - public PhysicsParticle addParticle(String id, Vector2f position, float mass) { - PhysicsParticle particle = new PhysicsParticle(id, position, mass); - particles.put(id, particle); - return particle; - } - - /** - * 从模型部件创建粒子 - */ - public PhysicsParticle addParticleFromModelPart(String id, ModelPart part, float mass) { - Vector2f position = part.getPosition(); - PhysicsParticle particle = addParticle(id, position, mass); - particle.setUserData(part); - return particle; - } - - /** - * 移除粒子 - */ - public boolean removeParticle(String id) { - // 移除相关的弹簧和约束 - springs.removeIf(spring -> - spring.getParticleA().getId().equals(id) || - spring.getParticleB().getId().equals(id)); - - constraints.removeIf(constraint -> - constraint.getParticle().getId().equals(id)); - - return particles.remove(id) != null; - } - - /** - * 获取粒子 - */ - public PhysicsParticle getParticle(String id) { - return particles.get(id); - } - - // ==================== 弹簧管理 ==================== - - /** - * 添加弹簧 - */ - public PhysicsSpring addSpring(String id, PhysicsParticle a, PhysicsParticle b, - float restLength, float stiffness, float damping) { - PhysicsSpring spring = new PhysicsSpring(id, a, b, restLength, stiffness, damping); - springs.add(spring); - return spring; - } - - /** - * 添加弹簧(自动计算自然长度) - */ - public PhysicsSpring addSpring(String id, PhysicsParticle a, PhysicsParticle b, - float stiffness, float damping) { - float restLength = a.getPosition().distance(b.getPosition()); - return addSpring(id, a, b, restLength, stiffness, damping); - } - - /** - * 移除弹簧 - */ - public boolean removeSpring(PhysicsSpring spring) { - return springs.remove(spring); - } - - // ==================== 约束管理 ==================== - - /** - * 添加位置约束 - */ - public PhysicsConstraint addPositionConstraint(PhysicsParticle particle, Vector2f targetPosition) { - PhysicsConstraint constraint = new PositionConstraint(particle, targetPosition); - constraints.add(constraint); - return constraint; - } - - /** - * 添加距离约束 - */ - public PhysicsConstraint addDistanceConstraint(PhysicsParticle particle, PhysicsParticle target, - float maxDistance) { - PhysicsConstraint constraint = new DistanceConstraint(particle, target, maxDistance); - constraints.add(constraint); - return constraint; - } - - /** - * 移除约束 - */ - public boolean removeConstraint(PhysicsConstraint constraint) { - return constraints.remove(constraint); - } - - // ==================== 碰撞管理 ==================== - - /** - * 添加圆形碰撞体 - */ - public PhysicsCollider addCircleCollider(String id, Vector2f center, float radius) { - PhysicsCollider collider = new CircleCollider(id, center, radius); - colliders.add(collider); - return collider; - } - - /** - * 添加矩形碰撞体 - */ - public PhysicsCollider addRectangleCollider(String id, Vector2f center, float width, float height) { - PhysicsCollider collider = new RectangleCollider(id, center, width, height); - colliders.add(collider); - return collider; - } - - /** - * 移除碰撞体 - */ - public boolean removeCollider(PhysicsCollider collider) { - return colliders.remove(collider); - } - - // ==================== 风力方法 ==================== - - /** - * 设置风力 - */ - public void setWindForce(float x, float y) { - this.windForce.set(x, y); - this.windEnabled = (x != 0.0f || y != 0.0f); - } - - /** - * 设置风力 - */ - public void setWindForce(Vector2f windForce) { - setWindForce(windForce.x, windForce.y); - } - - /** - * 获取当前风力 - */ - public Vector2f getWindForce() { - return new Vector2f(windForce); - } - - /** - * 启用/禁用风力 - */ - public void setWindEnabled(boolean enabled) { - this.windEnabled = enabled; - } - - /** - * 检查风力是否启用 - */ - public boolean isWindEnabled() { - return windEnabled; - } - - // ==================== 更新系统 ==================== - - /** - * 更新物理系统 - */ - public void update(float deltaTime, Model2D model) { - if (!enabled || !initialized) return; - - long startTime = System.nanoTime(); - - // 应用时间缩放 - float scaledDeltaTime = deltaTime * timeScale; - accumulatedTime += scaledDeltaTime; - - // 固定时间步长更新 - int numSubSteps = 0; - while (accumulatedTime >= fixedTimeStep && numSubSteps < maxSubSteps) { - updatePhysics(fixedTimeStep); - accumulatedTime -= fixedTimeStep; - numSubSteps++; - } - - // 应用物理结果到模型 - applyToModel(model); - - // 更新性能统计 - updatePerformanceStats(System.nanoTime() - startTime); - } - - /** - * 物理模拟更新(主流程) - */ - private void updatePhysics(float deltaTime) { - // 1) 清除上一帧的力累加器(将在本帧重新累加) - for (PhysicsParticle particle : particles.values()) { - particle.clearForces(); - } - - // 2) 应用全局力(重力、风) - applyGravity(); - applyWind(); - - // 3) 弹簧加入力 - for (PhysicsSpring spring : springs) { - spring.applyForce(deltaTime); - } - - // 4) 通过半隐式 Euler 更新速度与位置 - for (PhysicsParticle particle : particles.values()) { - if (particle.isMovable()) { - particle.update(deltaTime); - } - } - - // 5) 对约束做若干次迭代(布料等需要多次投影) - int constraintIterations = 3; // 可调(1-5)——越多布料越不僵化,但耗时增加 - for (int iter = 0; iter < constraintIterations; iter++) { - for (PhysicsConstraint constraint : constraints) { - if (constraint.isEnabled()) { - constraint.apply(deltaTime); - } - } - - // 每次迭代后处理静态碰撞体的穿透(避免强穿透) - for (PhysicsCollider collider : colliders) { - if (!collider.isEnabled()) continue; - for (PhysicsParticle particle : particles.values()) { - if (!particle.isMovable()) continue; - if (collider.collidesWith(particle)) { - collider.resolveCollision(particle, deltaTime); - } - } - } - } - - // 6) 在约束迭代后,根据位置变化同步速度,避免约束把位置推回后速度不一致导致僵化 - syncVelocitiesFromPositions(deltaTime); - - // 7) 粒子间碰撞(位置修正 + 速度冲量) - handleParticleCollisions(deltaTime); - - // 8) 应用空气阻力(现在直接作用于速度) - applyAirResistance(deltaTime); - } - - /** - * 根据当前位置与上一帧位置同步速度:v = (x - x_prev) / dt - * 此操作应在约束投影之后调用,且在不希望覆盖碰撞冲量的情况下不要在碰撞之后再次调用 - */ - private void syncVelocitiesFromPositions(float deltaTime) { - if (deltaTime <= 0.0f) return; - for (PhysicsParticle particle : particles.values()) { - if (!particle.isMovable()) continue; - Vector2f pos = particle.position; // package-private access within outer class - Vector2f prev = particle.previousPosition; - // velocity = (pos - prev) / dt - particle.velocity.set(pos).sub(prev).div(deltaTime); - } - } - - /** - * 应用重力 - */ - private void applyGravity() { - for (PhysicsParticle particle : particles.values()) { - if (particle.isMovable() && particle.isAffectedByGravity()) { - Vector2f gravityForce = new Vector2f(gravity).mul(particle.getMass()); - particle.addForce(gravityForce); - } - } - } - - /** - * 应用风力 - */ - private void applyWind() { - if (!windEnabled || windForce.lengthSquared() == 0.0f) { - return; - } - - for (PhysicsParticle particle : particles.values()) { - if (particle.isMovable() && particle.isAffectedByWind()) { - particle.addForce(new Vector2f(windForce)); - } - } - } - - /** - * 应用空气阻力(对速度的阻尼) - */ - private void applyAirResistance(float deltaTime) { - if (airResistance <= 0.0f) return; - - // 简单阻尼: v *= 1 / (1 + k*dt) (数值稳定) - float factor = 1.0f / (1.0f + airResistance * deltaTime); - - for (PhysicsParticle particle : particles.values()) { - if (!particle.isMovable()) continue; - - particle.velocity.mul(factor); - } - } - - /** - * 粒子间碰撞(位置修正 + 速度冲量) - */ - private void handleParticleCollisions(float deltaTime) { - List particleList = new ArrayList<>(particles.values()); - - for (int i = 0; i < particleList.size(); i++) { - PhysicsParticle p1 = particleList.get(i); - if (!p1.isMovable()) continue; - - for (int j = i + 1; j < particleList.size(); j++) { - PhysicsParticle p2 = particleList.get(j); - if (!p2.isMovable()) continue; - - Vector2f pos1 = p1.getPosition(); - Vector2f pos2 = p2.getPosition(); - Vector2f delta = new Vector2f(pos2).sub(pos1); - float dist = delta.length(); - float minDist = p1.getRadius() + p2.getRadius(); - - if (dist < minDist && dist > 1e-6f) { - Vector2f normal = new Vector2f(delta).div(dist); - float penetration = minDist - dist; - - // 按逆质量比例分离位置(静态/不可移动的情况 inverseMass=0) - float invM1 = p1.getInverseMass(); - float invM2 = p2.getInverseMass(); - float invSum = invM1 + invM2; - if (invSum <= 0.0f) continue; - - Vector2f correction = new Vector2f(normal).mul(penetration / invSum); - p1.translatePosition(new Vector2f(correction).mul(-invM1)); - p2.translatePosition(new Vector2f(correction).mul(invM2)); - - // 计算相对速度在法线方向上的分量 - Vector2f v1 = p1.getVelocity(); - Vector2f v2 = p2.getVelocity(); - Vector2f relVel = new Vector2f(v2).sub(v1); - float velAlongNormal = relVel.dot(normal); - - // 如果朝向彼此(velAlongNormal < 0)才处理冲量 - if (velAlongNormal < 0.0f) { - float restitution = 0.5f; // 可调 - float j2 = -(1.0f + restitution) * velAlongNormal; - j2 /= invSum; - - Vector2f impulse = new Vector2f(normal).mul(j2); - // 更新速度 - v1.sub(new Vector2f(impulse).mul(invM1)); - v2.add(new Vector2f(impulse).mul(invM2)); - p1.setVelocity(v1); - p2.setVelocity(v2); - } - } - } - } - } - - /** - * 应用物理结果到模型 - */ - private void applyToModel(Model2D model) { - for (PhysicsParticle particle : particles.values()) { - Object userData = particle.getUserData(); - if (userData instanceof ModelPart part) { - part.setPosition(particle.getPosition()); - - // 可选:根据速度设置旋转 - if (particle.getVelocity().lengthSquared() > 0.1f) { - float angle = (float) Math.atan2(particle.getVelocity().y, particle.getVelocity().x); - part.setRotation(angle); - } - } - } - } - - // ==================== 性能统计 ==================== - - /** - * 更新性能统计 - */ - private void updatePerformanceStats(long nanoTime) { - float millis = nanoTime / 1_000_000.0f; - - // 指数移动平均 - if (updateCount == 0) { - averageUpdateTime = millis; - } else { - averageUpdateTime = averageUpdateTime * 0.95f + millis * 0.05f; - } - - updateCount++; - } - - /** - * 获取性能报告 - */ - public PhysicsPerformanceReport getPerformanceReport() { - return new PhysicsPerformanceReport( - particles.size(), - springs.size(), - constraints.size(), - colliders.size(), - averageUpdateTime, - updateCount - ); - } - - // ==================== Getter/Setter ==================== - - public Vector2f getGravity() { - return new Vector2f(gravity); - } - - public void setGravity(float x, float y) { - gravity.set(x, y); - } - - public void setGravity(Vector2f gravity) { - this.gravity.set(gravity); - } - - public float getAirResistance() { - return airResistance; - } - - public void setAirResistance(float airResistance) { - this.airResistance = Math.max(0.0f, airResistance); - } - - public float getTimeScale() { - return timeScale; - } - - public void setTimeScale(float timeScale) { - this.timeScale = Math.max(0.0f, timeScale); - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public boolean isInitialized() { - return initialized; - } - - public Map getParticles() { - return Collections.unmodifiableMap(particles); - } - - public List getSprings() { - return Collections.unmodifiableList(springs); - } - - public List getConstraints() { - return Collections.unmodifiableList(constraints); - } - - public List getColliders() { - return Collections.unmodifiableList(colliders); - } - - /** - * 检查是否有活跃的物理效果 - * 返回true表示当前有物理效果正在影响模型 - */ - public boolean hasActivePhysics() { - if (!enabled || !initialized) { - return false; - } - - // 检查是否有可移动的粒子 - boolean hasMovableParticles = particles.values().stream() - .anyMatch(particle -> particle.isMovable() && particle.isAffectedByGravity()); - - if (!hasMovableParticles) { - return false; - } - - // 检查是否有活跃的弹簧 - boolean hasActiveSprings = springs.stream() - .anyMatch(spring -> spring.isEnabled() && - (spring.getParticleA().isMovable() || spring.getParticleB().isMovable())); - - // 检查粒子是否有显著的运动 - boolean hasSignificantMotion = particles.values().stream() - .anyMatch(particle -> { - if (!particle.isMovable()) return false; - - // 检查速度是否超过阈值 - float speedSquared = particle.getVelocity().lengthSquared(); - if (speedSquared > 0.1f) { // 速度阈值,可调整 - return true; - } - - // 检查位置是否显著变化(相对于前一帧) - Vector2f positionDelta = new Vector2f(particle.getPosition()) - .sub(particle.getPreviousPosition()); // 现在可以正常使用了 - float positionDeltaSquared = positionDelta.lengthSquared(); - // 位置变化阈值,可调整 - return positionDeltaSquared > 0.001f; - }); - - // 检查是否有活跃的约束 - boolean hasActiveConstraints = constraints.stream() - .anyMatch(constraint -> constraint.isEnabled() && - constraint.getParticle().isMovable()); - - return hasActiveSprings || hasSignificantMotion || hasActiveConstraints; - } - - // ==================== 内部类 ==================== - - /** - * 物理粒子类(半隐式 Euler) - */ - public static class PhysicsParticle { - private final String id; - private final Vector2f position; - private final Vector2f previousPosition; - private final Vector2f velocity; - private final Vector2f acceleration; - private final Vector2f forceAccumulator; - private final float mass; - private final float inverseMass; - private float radius; - private boolean movable; - private boolean affectedByGravity; - private boolean affectedByWind; - private Object userData; - - public PhysicsParticle(String id, Vector2f position, float mass) { - this.id = id; - this.position = new Vector2f(position); - this.previousPosition = new Vector2f(position); - this.velocity = new Vector2f(); - this.acceleration = new Vector2f(); - this.forceAccumulator = new Vector2f(); - // 允许质量为 0 表示无限质量(不可移动),但为了避免除零,保留逆质量为 0 的逻辑 - if (mass <= 0.0f) { - this.mass = Float.POSITIVE_INFINITY; - this.inverseMass = 0.0f; - } else { - this.mass = mass; - this.inverseMass = 1.0f / this.mass; - } - this.radius = 2.0f; // 默认半径 - this.movable = true; - this.affectedByGravity = true; - this.affectedByWind = true; - } - - public Vector2f getPreviousPosition() { - return new Vector2f(previousPosition); - } - - /** - * 半隐式 Euler 更新: - * a = F / m - * v += a * dt - * x += v * dt - */ - public void update(float deltaTime) { - if (!movable) return; - - // 计算加速度: a = F / m - acceleration.set(forceAccumulator).mul(inverseMass); - - // v_{n+1} = v_n + a * dt - velocity.add(new Vector2f(acceleration).mul(deltaTime)); - - // previousPosition 存放上一帧位置,用于 syncVelocities 时计算 - previousPosition.set(position); - - // x_{n+1} = x_n + v_{n+1} * dt - position.add(new Vector2f(velocity).mul(deltaTime)); - - // 清除力累加器(下一帧会重新累加) - forceAccumulator.set(0.0f, 0.0f); - } - - public void addForce(Vector2f force) { - // 如果是无限质量(inverseMass == 0),不累加力 - if (inverseMass == 0.0f) return; - forceAccumulator.add(force); - } - - public void clearForces() { - forceAccumulator.set(0.0f, 0.0f); - } - - // ----- 新增/保留的方法,辅助碰撞与约束使用 ----- - - /** - * 直接平移当前位置(用于位置修正) - */ - public void translatePosition(Vector2f delta) { - this.position.add(delta); - } - - /** - * 直接设置 previousPosition(用于在碰撞后根据新速度修正状态) - */ - public void setPreviousPosition(Vector2f prev) { - this.previousPosition.set(prev); - } - - /** - * 将新的速度应用到粒子(用于碰撞后的速度设置) - */ - public void applyNewVelocity(Vector2f newVelocity) { - this.velocity.set(newVelocity); - // 不直接修改 previousPosition,这样 syncVelocities 在下次迭代会根据位置更新速度 - } - - // Getter/Setter 方法 - public String getId() { - return id; - } - - public Vector2f getPosition() { - return new Vector2f(position); - } - - public void setPosition(Vector2f position) { - this.position.set(position); - } - - public Vector2f getVelocity() { - return new Vector2f(velocity); - } - - public void setVelocity(Vector2f velocity) { - this.velocity.set(velocity); - } - - public Vector2f getAcceleration() { - return new Vector2f(acceleration); - } - - public float getMass() { - if (Float.isInfinite(mass)) return Float.POSITIVE_INFINITY; - return mass; - } - - public float getInverseMass() { - return inverseMass; - } - - public float getRadius() { - return radius; - } - - public void setRadius(float radius) { - this.radius = radius; - } - - public boolean isMovable() { - return movable; - } - - public void setMovable(boolean movable) { - this.movable = movable; - } - - public boolean isAffectedByGravity() { - return affectedByGravity; - } - - public void setAffectedByGravity(boolean affectedByGravity) { - this.affectedByGravity = affectedByGravity; - } - - public Object getUserData() { - return userData; - } - - public void setUserData(Object userData) { - this.userData = userData; - } - - public boolean isAffectedByWind() { - return affectedByWind; - } - - public void setAffectedByWind(boolean affectedByWind) { - this.affectedByWind = affectedByWind; - } - } - - /** - * 物理弹簧类 - */ - public static class PhysicsSpring { - private final String id; - private final PhysicsParticle particleA; - private final PhysicsParticle particleB; - private final float restLength; - private final float stiffness; - private final float damping; - private boolean enabled; - - public PhysicsSpring(String id, PhysicsParticle a, PhysicsParticle b, - float restLength, float stiffness, float damping) { - this.id = id; - this.particleA = a; - this.particleB = b; - this.restLength = restLength; - this.stiffness = stiffness; - this.damping = damping; - this.enabled = true; - } - - public void applyForce(float deltaTime) { - if (!enabled) return; - - Vector2f delta = new Vector2f(particleB.getPosition()).sub(particleA.getPosition()); - float currentLength = delta.length(); - - if (currentLength < 0.0001f) return; // 避免除以零 - - // 方向单位向量 - Vector2f dir = new Vector2f(delta).div(currentLength); - - // 胡克定律: F = -k * (currentLength - restLength) - float stretch = currentLength - restLength; - Vector2f springForce = new Vector2f(dir).mul(stiffness * stretch); - - // 阻尼力: F_damp = -damping * relativeVelocity projected onto spring direction - Vector2f relativeVelocity = new Vector2f(particleB.getVelocity()).sub(particleA.getVelocity()); - float velocityAlongSpring = relativeVelocity.dot(dir); - Vector2f dampingForce = new Vector2f(dir).mul(damping * velocityAlongSpring); - - // 合力作用在两端(方向相反) - Vector2f totalForce = new Vector2f(springForce).sub(dampingForce); - - if (particleA.isMovable()) { - particleA.addForce(new Vector2f(totalForce).mul(-1.0f)); - } - - if (particleB.isMovable()) { - particleB.addForce(totalForce); - } - } - - // Getter/Setter 方法 - public String getId() { - return id; - } - - public PhysicsParticle getParticleA() { - return particleA; - } - - public PhysicsParticle getParticleB() { - return particleB; - } - - public float getRestLength() { - return restLength; - } - - public float getStiffness() { - return stiffness; - } - - public float getDamping() { - return damping; - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - } - - /** - * 物理约束接口 - */ - public interface PhysicsConstraint { - void apply(float deltaTime); - - PhysicsParticle getParticle(); - - boolean isEnabled(); - - void setEnabled(boolean enabled); - } - - /** - * 位置约束 - */ - public static class PositionConstraint implements PhysicsConstraint { - private final PhysicsParticle particle; - private final Vector2f targetPosition; - private float strength; - private boolean enabled; - - public PositionConstraint(PhysicsParticle particle, Vector2f targetPosition) { - this.particle = particle; - this.targetPosition = new Vector2f(targetPosition); - this.strength = 0.5f; - this.enabled = true; - } - - @Override - public void apply(float deltaTime) { - if (!enabled || !particle.isMovable()) return; - - Vector2f currentPos = particle.getPosition(); - Vector2f delta = new Vector2f(targetPosition).sub(currentPos); - Vector2f correction = new Vector2f(delta).mul(strength); - - // 直接设置位置(后续会同步速度) - particle.setPosition(new Vector2f(currentPos).add(correction)); - } - - // Getter/Setter 方法 - @Override - public PhysicsParticle getParticle() { - return particle; - } - - @Override - public boolean isEnabled() { - return enabled; - } - - @Override - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public Vector2f getTargetPosition() { - return new Vector2f(targetPosition); - } - - public void setTargetPosition(Vector2f targetPosition) { - this.targetPosition.set(targetPosition); - } - - public float getStrength() { - return strength; - } - - public void setStrength(float strength) { - this.strength = Math.max(0.0f, Math.min(1.0f, strength)); - } - } - - /** - * 距离约束 - */ - public static class DistanceConstraint implements PhysicsConstraint { - private final PhysicsParticle particle; - private final PhysicsParticle target; - private final float maxDistance; - private boolean enabled; - - public DistanceConstraint(PhysicsParticle particle, PhysicsParticle target, float maxDistance) { - this.particle = particle; - this.target = target; - this.maxDistance = maxDistance; - this.enabled = true; - } - - @Override - public void apply(float deltaTime) { - if (!enabled || !particle.isMovable()) return; - - Vector2f delta = new Vector2f(particle.getPosition()).sub(target.getPosition()); - float distance = delta.length(); - - if (distance > maxDistance) { - Vector2f correction = new Vector2f(delta).normalize().mul(distance - maxDistance); - particle.setPosition(new Vector2f(particle.getPosition()).sub(correction)); - } - } - - // Getter/Setter 方法 - @Override - public PhysicsParticle getParticle() { - return particle; - } - - @Override - public boolean isEnabled() { - return enabled; - } - - @Override - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public PhysicsParticle getTarget() { - return target; - } - - public float getMaxDistance() { - return maxDistance; - } - } - - /** - * 物理碰撞体接口 - */ - public interface PhysicsCollider { - boolean collidesWith(PhysicsParticle particle); - - void resolveCollision(PhysicsParticle particle, float deltaTime); - - String getId(); - - boolean isEnabled(); - - void setEnabled(boolean enabled); - } - - /** - * 圆形碰撞体 - */ - public static class CircleCollider implements PhysicsCollider { - private final String id; - private final Vector2f center; - private final float radius; - private boolean enabled; - - public CircleCollider(String id, Vector2f center, float radius) { - this.id = id; - this.center = new Vector2f(center); - this.radius = radius; - this.enabled = true; - } - - @Override - public boolean collidesWith(PhysicsParticle particle) { - float distance = particle.getPosition().distance(center); - return distance < (radius + particle.getRadius()); - } - - @Override - public void resolveCollision(PhysicsParticle particle, float deltaTime) { - Vector2f toParticle = new Vector2f(particle.getPosition()).sub(center); - float distance = toParticle.length(); - float combined = (radius + particle.getRadius()); - float overlap = combined - distance; - - if (overlap > 0.0001f) { - // 如果中心距离几乎为零,选一个任意法线 - Vector2f normal; - if (distance < 0.0001f) { - normal = new Vector2f(0.0f, 1.0f); - } else { - normal = new Vector2f(toParticle).div(distance); - } - - // 将粒子推动到边界外(静态碰撞体假定为不可移动 -> 全部位移分配给粒子) - particle.translatePosition(new Vector2f(normal).mul(overlap + 0.001f)); - - // 速度响应(与静态体碰撞) - Vector2f v = particle.getVelocity(); - float dot = v.dot(normal); - if (dot < 0) { - float restitution = 0.6f; - Vector2f vPrime = new Vector2f(v).sub(new Vector2f(normal).mul((1 + restitution) * dot)); - particle.setVelocity(vPrime); - } - } - } - - // Getter/Setter 方法 - @Override - public String getId() { - return id; - } - - @Override - public boolean isEnabled() { - return enabled; - } - - @Override - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public Vector2f getCenter() { - return new Vector2f(center); - } - - public void setCenter(Vector2f center) { - this.center.set(center); - } - - public float getRadius() { - return radius; - } - } - - /** - * 矩形碰撞体(轴对齐矩形) - */ - public static class RectangleCollider implements PhysicsCollider { - private final String id; - private final Vector2f center; - private final float width; - private final float height; - private boolean enabled; - - public RectangleCollider(String id, Vector2f center, float width, float height) { - this.id = id; - this.center = new Vector2f(center); - this.width = width; - this.height = height; - this.enabled = true; - } - - @Override - public boolean collidesWith(PhysicsParticle particle) { - Vector2f particlePos = particle.getPosition(); - float left = center.x - width / 2; - float right = center.x + width / 2; - float bottom = center.y - height / 2; - float top = center.y + height / 2; - - // 扩展边界考虑粒子半径 - left -= particle.getRadius(); - right += particle.getRadius(); - bottom -= particle.getRadius(); - top += particle.getRadius(); - - return particlePos.x >= left && particlePos.x <= right && - particlePos.y >= bottom && particlePos.y <= top; - } - - @Override - public void resolveCollision(PhysicsParticle particle, float deltaTime) { - Vector2f particlePos = particle.getPosition(); - float left = center.x - width / 2; - float right = center.x + width / 2; - float bottom = center.y - height / 2; - float top = center.y + height / 2; - - // 计算最近点 - float closestX = Math.max(left, Math.min(particlePos.x, right)); - float closestY = Math.max(bottom, Math.min(particlePos.y, top)); - - Vector2f closestPoint = new Vector2f(closestX, closestY); - Vector2f normal = new Vector2f(particlePos).sub(closestPoint); - - float dist = normal.length(); - float overlap = particle.getRadius() - dist; - - if (dist < 0.0001f) { - // 粒子在矩形内部且中心重合,选 Y 方向 - normal.set(0.0f, 1.0f); - dist = 1.0f; - } else { - normal.div(dist); // 单位向量 - } - - if (overlap > 0.0001f) { - // 将粒子推出矩形 - particle.translatePosition(new Vector2f(normal).mul(overlap + 0.001f)); - - // 速度响应(与静态矩形碰撞) - Vector2f v = particle.getVelocity(); - float dot = v.dot(normal); - if (dot < 0) { - float restitution = 0.6f; - Vector2f vPrime = new Vector2f(v).sub(new Vector2f(normal).mul((1 + restitution) * dot)); - particle.setVelocity(vPrime); - } - } - } - - // Getter/Setter 方法 - @Override - public String getId() { - return id; - } - - @Override - public boolean isEnabled() { - return enabled; - } - - @Override - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public Vector2f getCenter() { - return new Vector2f(center); - } - - public void setCenter(Vector2f center) { - this.center.set(center); - } - - public float getWidth() { - return width; - } - - public float getHeight() { - return height; - } - } - - /** - * 物理性能报告 - */ - public static class PhysicsPerformanceReport { - private final int particleCount; - private final int springCount; - private final int constraintCount; - private final int colliderCount; - private final float averageUpdateTime; - private final int totalUpdates; - - public PhysicsPerformanceReport(int particleCount, int springCount, int constraintCount, - int colliderCount, float averageUpdateTime, int totalUpdates) { - this.particleCount = particleCount; - this.springCount = springCount; - this.constraintCount = constraintCount; - this.colliderCount = colliderCount; - this.averageUpdateTime = averageUpdateTime; - this.totalUpdates = totalUpdates; - } - - // Getter 方法 - public int getParticleCount() { - return particleCount; - } - - public int getSpringCount() { - return springCount; - } - - public int getConstraintCount() { - return constraintCount; - } - - public int getColliderCount() { - return colliderCount; - } - - public float getAverageUpdateTime() { - return averageUpdateTime; - } - - public int getTotalUpdates() { - return totalUpdates; - } - - @Override - public String toString() { - return String.format( - "Physics Performance: %d particles, %d springs, %d constraints, %d colliders, " + - "Avg update: %.2fms, Total updates: %d", - particleCount, springCount, constraintCount, colliderCount, - averageUpdateTime, totalUpdates - ); - } - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/PuppetPin.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/PuppetPin.java deleted file mode 100644 index 9d0cb1a..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/PuppetPin.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import org.joml.Vector2f; - -import java.util.HashMap; -import java.util.Map; - -/** - * @author tzdwindows 7 - */ -public class PuppetPin { - private static int NEXT_ID = 0; - - // 衰减类型枚举 - public enum FalloffType { - LINEAR, // 线性衰减 - SMOOTH, // 平滑衰减 - SHARP, // 锐利衰减 - CONSTANT // 恒定衰减 - } - - private int id; - private final Vector2f position; - private final Vector2f originalPosition; - private final Vector2f uv; - private float influenceRadius = 100.0f; - private boolean selected = false; - private String name; - private FalloffType falloffType = FalloffType.SMOOTH; // 默认使用平滑衰减 - - // 权重贴图(顶点索引 -> 权重值) - private final Map weightMap = new HashMap<>(); - - public PuppetPin(float x, float y, float u, float v) { - this.id = NEXT_ID++; - this.position = new Vector2f(x, y); - this.originalPosition = new Vector2f(x, y); - this.uv = new Vector2f(u, v); - this.name = "Pin_" + id; - } - - // 计算顶点权重(基于距离的衰减) - public float calculateWeight(Vector2f vertexPos, int vertexIndex) { - float distance = position.distance(vertexPos); - - if (distance > influenceRadius) { - return 0.0f; - } - - float normalizedDistance = distance / influenceRadius; - float weight = 0.0f; - - // 根据衰减类型计算权重 - switch (falloffType) { - case LINEAR: - weight = 1.0f - normalizedDistance; - break; - case SMOOTH: - weight = (float) (1.0f - Math.pow(normalizedDistance, 2)); - break; - case SHARP: - weight = (float) (1.0f - Math.pow(normalizedDistance, 0.5f)); - break; - case CONSTANT: - weight = 1.0f; - break; - } - - return weight; - } - - // 预计算所有顶点权重 - public void precomputeWeights(Mesh2D mesh) { - weightMap.clear(); - for (int i = 0; i < mesh.getVertexCount(); i++) { - Vector2f vertexPos = mesh.getVertex(i); - float weight = calculateWeight(vertexPos, i); - if (weight > 0.01f) { // 只存储有影响的权重 - weightMap.put(i, weight); - } - } - } - - // ==================== 新增方法 ==================== - - /** - * 获取衰减类型 - */ - public FalloffType getFalloffType() { - return falloffType; - } - - /** - * 设置衰减类型 - */ - public void setFalloffType(FalloffType falloffType) { - this.falloffType = falloffType; - } - - /** - * 设置原始位置 - */ - public void setOriginalPosition(float x, float y) { - this.originalPosition.set(x, y); - } - - /** - * 设置原始位置 - */ - public void setOriginalPosition(Vector2f pos) { - this.originalPosition.set(pos); - } - - /** - * 获取UV坐标 - */ - public Vector2f getUV() { - return new Vector2f(uv); - } - - /** - * 设置UV坐标 - */ - public void setUV(float u, float v) { - this.uv.set(u, v); - } - - /** - * 设置UV坐标 - */ - public void setUV(Vector2f uv) { - this.uv.set(uv); - } - - // ==================== 原有方法 ==================== - - // Getters and Setters - public Vector2f getPosition() { - return new Vector2f(position); - } - - public void setPosition(float x, float y) { - this.position.set(x, y); - } - - public void setPosition(Vector2f pos) { - this.position.set(pos); - } - - public Vector2f getOriginalPosition() { - return new Vector2f(originalPosition); - } - - public float getInfluenceRadius() { - return influenceRadius; - } - - public void setInfluenceRadius(float radius) { - this.influenceRadius = radius; - } - - public boolean isSelected() { - return selected; - } - - public void setSelected(boolean selected) { - this.selected = selected; - } - - public int getId() { - return id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Map getWeightMap() { - return new HashMap<>(weightMap); - } - - public void move(float dx, float dy) { - position.add(dx, dy); - } - - public void saveAsOriginal() { - originalPosition.set(position); - } - - public void resetToOriginal() { - position.set(originalPosition); - } - - public void setId(int id) { - this.id = id; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/SaveVector2f.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/SaveVector2f.java deleted file mode 100644 index b4e8574..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/SaveVector2f.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import org.joml.Vector2f; - -/** - * 工具类:用于在 Vector2f 和字符串之间进行转换。 - * 例如: - * - toString(new Vector2f(1.5f, -2.0f)) => "1.5,-2.0" - * - fromString("1.5,-2.0") => new Vector2f(1.5f, -2.0f) - * - * @author tzdwindows 7 - */ -public class SaveVector2f { - - /** - * 将 Vector2f 转换为字符串。 - * 格式: "x,y" - */ - public static String toString(Vector2f vec) { - if (vec == null) { - return "0,0"; - } - return vec.x + "," + vec.y; - } - - /** - * 从字符串解析为 Vector2f。 - * 允许的格式: - * - "x,y" - * - "(x,y)" - * - " x , y " - * 若格式错误则返回 (0,0) - */ - public static Vector2f fromString(String str) { - if (str == null || str.trim().isEmpty()) { - return new Vector2f(); - } - - str = str.trim(); - - // 去掉括号 - if (str.startsWith("(") && str.endsWith(")")) { - str = str.substring(1, str.length() - 1); - } - - String[] parts = str.split(","); - if (parts.length != 2) { - return new Vector2f(); - } - - try { - float x = Float.parseFloat(parts[0].trim()); - float y = Float.parseFloat(parts[1].trim()); - return new Vector2f(x, y); - } catch (NumberFormatException e) { - return new Vector2f(); - } - } - - /** - * 安全解析(带默认值) - */ - public static Vector2f fromString(String str, Vector2f defaultValue) { - Vector2f parsed = fromString(str); - if (parsed.equals(new Vector2f(0, 0)) && (str == null || str.isEmpty())) { - return new Vector2f(defaultValue); - } - return parsed; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java deleted file mode 100644 index 9a96d28..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java +++ /dev/null @@ -1,1322 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import org.lwjgl.opengl.*; -import org.lwjgl.stb.STBImage; -import org.lwjgl.stb.STBImageWrite; -import org.lwjgl.system.MemoryUtil; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.nio.ByteBuffer; -import java.nio.IntBuffer; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -/** - * 纹理类,使用 LWJGL OpenGL API 实现完整的纹理管理 - * - * @author tzdwindows 7 - */ -public class Texture { - // ==================== 纹理属性 ==================== - private final int textureId; - private final String name; - private final int width; - private final int height; - private final TextureFormat format; - private final TextureType type; - - // ==================== 纹理参数 ==================== - private TextureFilter minFilter = TextureFilter.LINEAR; - private TextureFilter magFilter = TextureFilter.LINEAR; - private TextureWrap wrapS = TextureWrap.CLAMP_TO_EDGE; - private TextureWrap wrapT = TextureWrap.CLAMP_TO_EDGE; - private boolean mipmapsEnabled = false; - private boolean textureCreated = false; - - // ==================== 状态管理 ==================== - private boolean disposed = false; - private final long creationTime; - private int previousActiveTexture = -1; - - // ==================== 静态管理 ==================== - private static final Map TEXTURE_CACHE = new HashMap<>(); - private static boolean openGLChecked = false; - - // ==================== 像素数据缓存支持 ==================== - private byte[] pixelDataCache = null; - - // ==================== 枚举定义 ==================== - - public enum TextureFormat { - RGB(3, GL11.GL_RGB, GL11.GL_RGB), - RGBA(4, GL11.GL_RGBA, GL11.GL_RGBA), - ALPHA(1, GL11.GL_ALPHA, GL11.GL_ALPHA), - LUMINANCE(1, GL11.GL_LUMINANCE, GL11.GL_LUMINANCE), - LUMINANCE_ALPHA(2, GL11.GL_LUMINANCE_ALPHA, GL11.GL_LUMINANCE_ALPHA), - RED(1, GL30.GL_RED, GL30.GL_RED), - RG(2, GL30.GL_RG, GL30.GL_RG); - - private final int components; - private final int glInternalFormat; - private final int glFormat; - - TextureFormat(int components, int glInternalFormat, int glFormat) { - this.components = components; - this.glInternalFormat = glInternalFormat; - this.glFormat = glFormat; - } - - public int getComponents() { - return components; - } - - public int getGLInternalFormat() { - return glInternalFormat; - } - - public int getGLFormat() { - return glFormat; - } - } - - public enum TextureType { - UNSIGNED_BYTE(GL11.GL_UNSIGNED_BYTE), - BYTE(GL11.GL_BYTE), - UNSIGNED_SHORT(GL11.GL_UNSIGNED_SHORT), - SHORT(GL11.GL_SHORT), - UNSIGNED_INT(GL11.GL_UNSIGNED_INT), - INT(GL11.GL_INT), - FLOAT(GL11.GL_FLOAT); - - private final int glType; - - TextureType(int glType) { - this.glType = glType; - } - - public int getGLType() { - return glType; - } - } - - public enum TextureFilter { - NEAREST(GL11.GL_NEAREST), - LINEAR(GL11.GL_LINEAR), - NEAREST_MIPMAP_NEAREST(GL11.GL_NEAREST_MIPMAP_NEAREST), - LINEAR_MIPMAP_NEAREST(GL11.GL_LINEAR_MIPMAP_NEAREST), - NEAREST_MIPMAP_LINEAR(GL11.GL_NEAREST_MIPMAP_LINEAR), - LINEAR_MIPMAP_LINEAR(GL11.GL_LINEAR_MIPMAP_LINEAR); - - private final int glFilter; - - TextureFilter(int glFilter) { - this.glFilter = glFilter; - } - - public int getGLFilter() { - return glFilter; - } - } - - public enum TextureWrap { - REPEAT(GL11.GL_REPEAT), - MIRRORED_REPEAT(GL14.GL_MIRRORED_REPEAT), - CLAMP_TO_EDGE(GL12.GL_CLAMP_TO_EDGE), - CLAMP_TO_BORDER(GL13.GL_CLAMP_TO_BORDER); - - private final int glWrap; - - TextureWrap(int glWrap) { - this.glWrap = glWrap; - } - - public int getGLWrap() { - return glWrap; - } - } - - // ==================== STB 图像加载 ==================== - - static { - STBImage.stbi_set_flip_vertically_on_load(true); - } - - - public Texture(String name, int width, int height, TextureFormat format) { - this(name, width, height, format, TextureType.UNSIGNED_BYTE); - } - - public Texture(String name, int width, int height, TextureFormat format, TextureType type) { - checkOpenGLCapabilities(); - this.textureId = generateTextureId(); - this.name = name; - this.width = width; - this.height = height; - this.format = format; - this.type = type; - this.creationTime = System.currentTimeMillis(); - - // 创建空的纹理对象 - createTextureObject(); - applyTextureParameters(); - } - - public Texture(String name, int width, int height, TextureFormat format, ByteBuffer pixelData) { - this(name, width, height, format); - uploadData(pixelData); - } - - public Texture(String name, int width, int height, TextureFormat format, int[] pixelData) { - this(name, width, height, format); - uploadData(pixelData); - } - - // ==================== OpenGL 能力检查 ==================== - - /** - * 检查 OpenGL 能力 - */ - private static void checkOpenGLCapabilities() { - if (!openGLChecked) { - if (!GL.getCapabilities().OpenGL11) { - throw new RuntimeException("OpenGL 1.1 is required but not supported"); - } - openGLChecked = true; - } - } - - // ==================== 纹理数据管理 ==================== - - /** - * 创建纹理对象 - */ - private void createTextureObject() { - if (textureCreated) return; - - GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId); - - // 分配纹理存储 - 使用兼容性更好的方法 - GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, format.getGLInternalFormat(), - width, height, 0, format.getGLFormat(), type.getGLType(), - (ByteBuffer) null); - - textureCreated = true; - GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0); - - checkGLError("createTextureObject"); - } - - /** - * 上传字节缓冲区数据到纹理 - */ - public void uploadData(ByteBuffer pixelData) { - if (disposed) { - throw new IllegalStateException("Cannot upload data to disposed texture: " + name); - } - - if (pixelData == null) { - throw new IllegalArgumentException("Pixel data cannot be null"); - } - - int expectedSize = width * height * format.getComponents(); - if (pixelData.remaining() < expectedSize) { - throw new IllegalArgumentException( - String.format("Pixel data buffer too small for texture dimensions. Expected %d, got %d", - expectedSize, pixelData.remaining())); - } - - bind(0); - - if (!textureCreated) { - createTextureObject(); - } - - // 关键:确保以 1 字节对齐上传,防止行对齐导致的数据错位(很多白图/花屏来自此) - int prevUnpack = GL11.glGetInteger(GL11.GL_UNPACK_ALIGNMENT); - GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1); - - try { - // 上传纹理数据 - GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, width, height, - format.getGLFormat(), type.getGLType(), pixelData); - - // 检查OpenGL错误 - checkGLError("glTexSubImage2D"); - - // 缓存像素数据 - cachePixelDataFromBuffer(pixelData); - } finally { - // 恢复原先的 UNPACK_ALIGNMENT - try { - GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, prevUnpack); - } catch (Exception ignored) { - } - unbind(); - } - } - - /** - * 从ByteBuffer缓存像素数据 - */ - private void cachePixelDataFromBuffer(ByteBuffer buffer) { - try { - int originalPosition = buffer.position(); - pixelDataCache = new byte[buffer.remaining()]; - buffer.get(pixelDataCache); - buffer.position(originalPosition); - } catch (Exception e) { - System.err.println("Failed to cache pixel data from buffer: " + e.getMessage()); - pixelDataCache = null; - } - } - - /** - * 上传整数数组数据到纹理 - */ - public void uploadData(int[] pixelData) { - if (pixelData == null) { - throw new IllegalArgumentException("Pixel data cannot be null"); - } - - if (pixelData.length < width * height) { - throw new IllegalArgumentException("Pixel data array too small for texture dimensions"); - } - - // 将int数组转换为ByteBuffer - ByteBuffer buffer = MemoryUtil.memAlloc(pixelData.length * 4); - buffer.asIntBuffer().put(pixelData); - buffer.position(0); - - try { - uploadData(buffer); - } finally { - MemoryUtil.memFree(buffer); - } - } - - /** - * 生成mipmaps - */ - public void generateMipmaps() { - if (disposed) { - throw new IllegalStateException("Cannot generate mipmaps for disposed texture: " + name); - } - - if (!isPowerOfTwo(width) || !isPowerOfTwo(height)) { - System.err.println("Warning: Cannot generate mipmaps for non-power-of-two texture: " + name); - return; - } - - bind(0); - - // 重新创建纹理为可变纹理 - GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, format.getGLInternalFormat(), - width, height, 0, format.getGLFormat(), type.getGLType(), - (ByteBuffer) null); - textureCreated = true; - - // 生成mipmaps - GL30.glGenerateMipmap(GL11.GL_TEXTURE_2D); - - // 检查OpenGL错误 - checkGLError("glGenerateMipmap"); - - mipmapsEnabled = true; - - // 更新过滤器以使用mipmaps - if (minFilter == TextureFilter.LINEAR) { - setMinFilter(TextureFilter.LINEAR_MIPMAP_LINEAR); - } else if (minFilter == TextureFilter.NEAREST) { - setMinFilter(TextureFilter.NEAREST_MIPMAP_NEAREST); - } - - unbind(); - } - - // ==================== 纹理参数设置 ==================== - - /** - * 从当前纹理裁剪出一个子纹理并返回新的 Texture。 - * 仅支持 UNSIGNED_BYTE 类型的纹理(常见的 8-bit per component 图像)。 - * - * @param x 裁剪区域左上角 X(像素) - * @param y 裁剪区域左上角 Y(像素) - * @param w 裁剪区域宽度(像素) - * @param h 裁剪区域高度(像素) - * @param newName 新纹理名称 - * @return 新创建的子纹理 - */ - public Texture crop(int x, int y, int w, int h, String newName) { - if (disposed) { - throw new IllegalStateException("Cannot crop disposed texture: " + name); - } - if (x < 0 || y < 0 || w <= 0 || h <= 0 || x + w > width || y + h > height) { - throw new IllegalArgumentException("Crop rectangle out of bounds"); - } - if (type != TextureType.UNSIGNED_BYTE) { - throw new UnsupportedOperationException("Crop currently only supported for UNSIGNED_BYTE textures"); - } - - // 确保有像素缓存(若没有则尝试从 GPU 提取) - ensurePixelDataCached(); - if (!hasPixelData()) { - throw new RuntimeException("No pixel data available for cropping texture: " + name); - } - - int comps = format.getComponents(); - int rowSrcBytes = width * comps; - int rowDstBytes = w * comps; - byte[] cropped = new byte[w * h * comps]; - - for (int row = 0; row < h; row++) { - int srcPos = ((y + row) * width + x) * comps; - int dstPos = row * rowDstBytes; - System.arraycopy(this.pixelDataCache, srcPos, cropped, dstPos, rowDstBytes); - } - - // 将裁剪数据上传到新纹理 - ByteBuffer buffer = MemoryUtil.memAlloc(cropped.length); - buffer.put(cropped); - buffer.flip(); - - try { - Texture newTex = new Texture(newName, w, h, this.format, buffer); - // 复制参数 - newTex.setMinFilter(this.minFilter); - newTex.setMagFilter(this.magFilter); - newTex.setWrapS(this.wrapS); - newTex.setWrapT(this.wrapT); - if (this.mipmapsEnabled && newTex.isPowerOfTwo(w) && newTex.isPowerOfTwo(h)) { - newTex.generateMipmaps(); - } - // 缓存像素数据以便后续使用 - newTex.ensurePixelDataCached(); - return newTex; - } finally { - MemoryUtil.memFree(buffer); - } - } - - /** - * 设置最小化过滤器 - */ - public void setMinFilter(TextureFilter filter) { - if (this.minFilter != filter) { - this.minFilter = filter; - applyTextureParameters(); - } - } - - /** - * 设置最大化过滤器 - */ - public void setMagFilter(TextureFilter filter) { - if (this.magFilter != filter) { - this.magFilter = filter; - applyTextureParameters(); - } - } - - /** - * 设置S轴包装模式 - */ - public void setWrapS(TextureWrap wrap) { - if (this.wrapS != wrap) { - this.wrapS = wrap; - applyTextureParameters(); - } - } - - /** - * 设置T轴包装模式 - */ - public void setWrapT(TextureWrap wrap) { - if (this.wrapT != wrap) { - this.wrapT = wrap; - applyTextureParameters(); - } - } - - /** - * 设置双向包装模式 - */ - public void setWrap(TextureWrap wrap) { - setWrapS(wrap); - setWrapT(wrap); - } - - /** - * 应用纹理参数到GPU - */ - public void applyTextureParameters() { - if (disposed) return; - - bind(0); - - // 设置纹理参数 - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, minFilter.getGLFilter()); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, magFilter.getGLFilter()); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, wrapS.getGLWrap()); - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, wrapT.getGLWrap()); - - // 检查OpenGL错误 - checkGLError("glTexParameteri"); - - unbind(); - } - - // ==================== 绑定管理 ==================== - - /** - * 绑定纹理到指定纹理单元 - */ - public void bind(int textureUnit) { - if (disposed) { - throw new IllegalStateException("Cannot bind disposed texture: " + name); - } - - if (textureUnit < 0 || textureUnit >= 32) { // 合理的纹理单元范围 - throw new IllegalArgumentException("Invalid texture unit: " + textureUnit); - } - - RenderSystem.activeTexture(GL13.GL_TEXTURE0 + textureUnit); - RenderSystem.bindTexture(textureId); - RenderSystem.checkGLError("Texture.bind"); - } - - /** - * 绑定纹理到默认纹理单元(0) - */ - public void bind() { - bind(0); - } - - /** - * 解绑纹理 - */ - public void unbind() { - RenderSystem.bindTexture(0); - if (previousActiveTexture != -1) { - try { - RenderSystem.activeTexture(previousActiveTexture); - } catch (Exception e) { - System.err.println("Warning: failed to restore previous active texture unit: " + e.getMessage()); - } finally { - previousActiveTexture = -1; - } - } - } - - - // ==================== 资源管理 ==================== - - /** - * 释放纹理资源 - */ - public void dispose() { - if (!disposed) { - try { - RenderSystem.deleteTextures(textureId); - } catch (Exception e) { - System.err.println("Error disposing texture: " + e.getMessage()); - } - - pixelDataCache = null; - - disposed = true; - TEXTURE_CACHE.values().removeIf(texture -> texture.textureId == this.textureId); - } - } - - /** - * 检查纹理是否已释放 - */ - public boolean isDisposed() { - return disposed; - } - - // ==================== 静态工厂方法 ==================== - - /** - * 创建纯色纹理 - */ - public static Texture createSolidColor(String name, int width, int height, int rgbaColor) { - int[] pixels = new int[width * height]; - java.util.Arrays.fill(pixels, rgbaColor); - Texture texture = new Texture(name, width, height, TextureFormat.RGBA, pixels); - texture.ensurePixelDataCached(); - return texture; - } - - /** - * 创建棋盘格纹理(用于调试) - */ - public static Texture createCheckerboard(String name, int width, int height, int tileSize, - int color1, int color2) { - int[] pixels = new int[width * height]; - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - boolean isColor1 = ((x / tileSize) + (y / tileSize)) % 2 == 0; - pixels[y * width + x] = isColor1 ? color1 : color2; - } - } - Texture texture = new Texture(name, width, height, TextureFormat.RGBA, pixels); - texture.ensurePixelDataCached(); - return texture; - } - - /** - * 从缓存获取纹理,如果不存在则创建 - */ - public static Texture getOrCreate(String name, int width, int height, TextureFormat format) { - return TEXTURE_CACHE.computeIfAbsent(name, k -> - new Texture(name, width, height, format)); - } - - /** - * 从文件系统加载图像数据 - */ - public static ImageData loadImageFromFile(String filePath) { - if (filePath == null || filePath.trim().isEmpty()) { - throw new IllegalArgumentException("File path cannot be null or empty"); - } - - File file = new File(filePath); - if (!file.exists()) { - throw new RuntimeException("Texture file not found: " + filePath); - } - - IntBuffer width = MemoryUtil.memAllocInt(1); - IntBuffer height = MemoryUtil.memAllocInt(1); - IntBuffer components = MemoryUtil.memAllocInt(1); - - try { - // 使用 STB 加载图像 - ByteBuffer imageData = STBImage.stbi_load(filePath, width, height, components, 0); - - if (imageData == null) { - String error = STBImage.stbi_failure_reason(); - throw new RuntimeException("Failed to load image: " + filePath + " - " + error); - } - - // 确定纹理格式 - TextureFormat format = getTextureFormat(components.get(0)); - - return new ImageData( - imageData, - width.get(0), - height.get(0), - format, - filePath - ); - - } finally { - MemoryUtil.memFree(width); - MemoryUtil.memFree(height); - MemoryUtil.memFree(components); - } - } - - /** - * 从文件创建纹理 - */ - public static Texture createFromFile(String name, String filePath) { - return createFromFile(name, filePath, TextureFilter.LINEAR, TextureFilter.LINEAR); - } - - public static Texture createFromFile(String name, String filePath, TextureFilter minFilter, TextureFilter magFilter) { - ImageData imageData = loadImageFromFile(filePath); - - try { - // 创建纹理 - Texture texture = new Texture( - name, - imageData.width, - imageData.height, - imageData.format - ); - - // 上传数据(这会自动缓存) - texture.uploadData(imageData.data); - - // 设置纹理参数 - texture.setMinFilter(minFilter); - texture.setMagFilter(magFilter); - - // 如果是2的幂次方尺寸,生成mipmaps - if (texture.isPowerOfTwo(imageData.width) && texture.isPowerOfTwo(imageData.height)) { - texture.generateMipmaps(); - } - - return texture; - - } finally { - // 释放STB图像数据 - STBImage.stbi_image_free(imageData.data); - } - } - - /** - * 从纹理提取图像数据 - */ - public ByteBuffer extractTextureData() { - if (disposed) { - throw new IllegalStateException("Cannot extract data from disposed texture"); - } - - bind(0); - - try { - // 计算数据大小 - int dataSize = width * height * format.getComponents(); - ByteBuffer pixelData = MemoryUtil.memAlloc(dataSize); - - // 从GPU读取纹理数据 - RenderSystem.getTexImage(GL11.GL_TEXTURE_2D, 0, format.getGLFormat(), type.getGLType(), pixelData); - - // 检查OpenGL错误 - RenderSystem.checkGLError("Texture.extractTextureData"); - - pixelData.flip(); - return pixelData; - - } finally { - unbind(); - } - } - - /** - * 将纹理保存到文件(支持PNG、JPG等格式) - */ - public boolean saveToFile(String filePath) { - return saveToFile(filePath, "png"); - } - - public boolean saveToFile(String filePath, String format) { - if (disposed) { - throw new IllegalStateException("Cannot save disposed texture"); - } - - ByteBuffer pixelData = extractTextureData(); - - try { - // 根据格式保存图像 - boolean success = false; - int components = this.format.getComponents(); // 使用纹理的格式组件数量 - int stride = width * components; // 每行的字节数 - - switch (format.toLowerCase()) { - case "png": - success = STBImageWrite.stbi_write_png( - filePath, width, height, components, pixelData, stride - ); - break; - - case "jpg": - case "jpeg": - success = STBImageWrite.stbi_write_jpg( - filePath, width, height, components, pixelData, 90 // 质量 0-100 - ); - break; - - case "bmp": - success = STBImageWrite.stbi_write_bmp(filePath, width, height, components, pixelData); - break; - - case "tga": - success = STBImageWrite.stbi_write_tga(filePath, width, height, components, pixelData); - break; - - default: - throw new IllegalArgumentException("Unsupported image format: " + format); - } - - if (!success) { - System.err.println("Failed to save texture to: " + filePath); - return false; - } - - return true; - - } finally { - MemoryUtil.memFree(pixelData); - } - } - - /** - * 检查是否有可用的像素数据缓存 - */ - public boolean hasPixelData() { - return this.pixelDataCache != null && this.pixelDataCache.length > 0; - } - - /** - * 获取内部像素数据缓存 - */ - public byte[] getPixelData() { - if (pixelDataCache == null) { - // 如果没有缓存,从GPU提取并缓存 - cachePixelDataFromGPU(); - } - return pixelDataCache != null ? pixelDataCache.clone() : null; - } - - /** - * 设置像素数据缓存 - */ - public void setPixelData(byte[] pixelData) { - this.pixelDataCache = pixelData != null ? pixelData.clone() : null; - } - - /** - * 从GPU提取像素数据并缓存 - */ - private void cachePixelDataFromGPU() { - if (disposed) { - return; - } - - try { - ByteBuffer gpuData = extractTextureData(); - if (gpuData != null && gpuData.remaining() > 0) { - pixelDataCache = new byte[gpuData.remaining()]; - gpuData.get(pixelDataCache); - gpuData.rewind(); - MemoryUtil.memFree(gpuData); - } - } catch (Exception e) { - System.err.println("Failed to cache pixel data from GPU: " + e.getMessage()); - pixelDataCache = null; - } - } - - /** - * 清除像素数据缓存 - */ - public void clearPixelDataCache() { - this.pixelDataCache = null; - } - - /** - * 确保像素数据缓存可用(如果不存在则从GPU提取) - */ - public void ensurePixelDataCached() { - if (!hasPixelData()) { - cachePixelDataFromGPU(); - } - } - - /** - * 获取像素数据缓存大小(字节) - */ - public int getPixelDataCacheSize() { - return pixelDataCache != null ? pixelDataCache.length : 0; - } - - /** - * 创建纹理的深拷贝(包括图像数据) - */ - public Texture copy() { - return copy(this.name + "_copy"); - } - - public Texture copy(String newName) { - if (disposed) { - throw new IllegalStateException("Cannot copy disposed texture"); - } - - // 确保缓存数据可用 - ensurePixelDataCached(); - - ByteBuffer pixelData = null; - try { - if (hasPixelData()) { - // 使用缓存数据创建新纹理 - pixelData = MemoryUtil.memAlloc(pixelDataCache.length); - pixelData.put(pixelDataCache); - pixelData.flip(); - } else { - // 回退到GPU提取 - pixelData = extractTextureData(); - } - - // 创建新纹理 - Texture copy = new Texture(newName, width, height, format, pixelData); - - // 复制纹理参数 - copy.setMinFilter(this.minFilter); - copy.setMagFilter(this.magFilter); - copy.setWrapS(this.wrapS); - copy.setWrapT(this.wrapT); - - // 复制像素数据缓存 - if (hasPixelData()) { - copy.setPixelData(this.pixelDataCache); - } - - if (this.mipmapsEnabled) { - copy.generateMipmaps(); - } - - return copy; - - } finally { - if (pixelData != null) { - MemoryUtil.memFree(pixelData); - } - } - } - -// ==================== 辅助方法和内部类 ==================== - - /** - * 根据组件数量确定纹理格式 - */ - public static TextureFormat getTextureFormat(int components) { - switch (components) { - case 1: - return TextureFormat.RED; - case 2: - return TextureFormat.RG; - case 3: - return TextureFormat.RGB; - case 4: - return TextureFormat.RGBA; - default: - throw new IllegalArgumentException("Unsupported number of components: " + components); - } - } - - /** - * 图像数据容器类 - */ - public static class ImageData { - public final ByteBuffer data; - public final int width; - public final int height; - public final TextureFormat format; - public final String sourcePath; - - public ImageData(ByteBuffer data, int width, int height, TextureFormat format, String sourcePath) { - this.data = data; - this.width = width; - this.height = height; - this.format = format; - this.sourcePath = sourcePath; - } - - /** - * 创建图像数据的拷贝 - */ - public ImageData copy() { - ByteBuffer copyBuffer = MemoryUtil.memAlloc(data.capacity()); - copyBuffer.put(data); - copyBuffer.flip(); - data.rewind(); - - return new ImageData(copyBuffer, width, height, format, sourcePath); - } - - /** - * 释放图像数据内存 - */ - public void free() { - if (data != null) { - STBImage.stbi_image_free(data); - } - } - } - - /** - * 支持的图像格式检查 - */ - public static boolean isSupportedImageFormat(String filePath) { - if (filePath == null) return false; - - String lowerPath = filePath.toLowerCase(); - return lowerPath.endsWith(".png") || - lowerPath.endsWith(".jpg") || - lowerPath.endsWith(".jpeg") || - lowerPath.endsWith(".bmp") || - lowerPath.endsWith(".tga") || - lowerPath.endsWith(".psd") || - lowerPath.endsWith(".gif") || - lowerPath.endsWith(".hdr") || - lowerPath.endsWith(".pic"); - } - - /** - * 获取图像文件信息(不加载完整图像) - */ - public static ImageInfo getImageInfo(String filePath) { - if (!isSupportedImageFormat(filePath)) { - throw new IllegalArgumentException("Unsupported image format: " + filePath); - } - - IntBuffer width = MemoryUtil.memAllocInt(1); - IntBuffer height = MemoryUtil.memAllocInt(1); - IntBuffer components = MemoryUtil.memAllocInt(1); - - try { - boolean success = STBImage.stbi_info(filePath, width, height, components); - - if (!success) { - String error = STBImage.stbi_failure_reason(); - throw new RuntimeException("Failed to get image info: " + filePath + " - " + error); - } - - return new ImageInfo( - width.get(0), - height.get(0), - components.get(0), - filePath - ); - - } finally { - MemoryUtil.memFree(width); - MemoryUtil.memFree(height); - MemoryUtil.memFree(components); - } - } - - /** - * 图像信息类 - */ - public static class ImageInfo { - public final int width; - public final int height; - public final int components; - public final String filePath; - - public ImageInfo(int width, int height, int components, String filePath) { - this.width = width; - this.height = height; - this.components = components; - this.filePath = filePath; - } - - @Override - public String toString() { - return String.format("ImageInfo{size=%dx%d, components=%d, path='%s'}", - width, height, components, filePath); - } - } - -// ==================== 新的静态工厂方法 ==================== - - public static Texture createFromBufferedImage(String name, BufferedImage img) { - return createFromBufferedImage(name, img, TextureFilter.LINEAR, TextureFilter.LINEAR); - } - - public static Texture createFromBufferedImage(String name, BufferedImage img, TextureFilter minFilter, TextureFilter magFilter) { - if (img == null) throw new IllegalArgumentException("BufferedImage cannot be null"); - - final int width = img.getWidth(); - final int height = img.getHeight(); - final int len = width * height; - - // 获取或转换为 TYPE_INT_ARGB 的 int[] 像素数据以提高性能 - final int[] pixels; - BufferedImage working = img; - if (img.getType() != BufferedImage.TYPE_INT_ARGB) { - BufferedImage conv = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = conv.createGraphics(); - try { - g.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, java.awt.RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); - g.drawImage(img, 0, 0, null); - } finally { - g.dispose(); - } - working = conv; - } - pixels = ((java.awt.image.DataBufferInt) working.getRaster().getDataBuffer()).getData(); - - // 检测是否为预乘(alpha premultiplied) - boolean isPremultiplied = working.isAlphaPremultiplied(); - - // 分配本地 ByteBuffer 并填充为 非预乘 RGBA 顺序(R,G,B,A) - ByteBuffer buffer = MemoryUtil.memAlloc(len * 4); - try { - for (int i = 0; i < len; i++) { - int p = pixels[i]; - int a = (p >> 24) & 0xFF; - int r = (p >> 16) & 0xFF; - int g = (p >> 8) & 0xFF; - int b = (p) & 0xFF; - - // 如果是预乘 alpha,则反预乘(避免颜色被 alpha 缩小导致看起来发白或透明) - if (isPremultiplied && a != 0) { - // 反预乘:原色 = premultipliedColor * 255 / alpha - float invA = 255.0f / (float) a; - r = Math.min(255, Math.round(r * invA)); - g = Math.min(255, Math.round(g * invA)); - b = Math.min(255, Math.round(b * invA)); - } - - buffer.put((byte) (r & 0xFF)); - buffer.put((byte) (g & 0xFF)); - buffer.put((byte) (b & 0xFF)); - buffer.put((byte) (a & 0xFF)); - } - buffer.flip(); - - // 创建纹理并上传(uploadData 内已处理 GL_UNPACK_ALIGNMENT) - Texture texture = new Texture(name, width, height, TextureFormat.RGBA, buffer); - texture.setMinFilter(minFilter); - texture.setMagFilter(magFilter); - - // 若为 POT 尺寸且需要,则生成 mipmaps - if (texture.isPowerOfTwo(width) && texture.isPowerOfTwo(height)) { - texture.generateMipmaps(); - } - - // 缓存像素数据(可选,确保后续 crop/copy 等可用) - texture.ensurePixelDataCached(); - - return texture; - } finally { - MemoryUtil.memFree(buffer); - } - } - - - /** - * 从字节数组创建纹理 - */ - public static Texture createFromBytes(String name, byte[] imageData, TextureFilter minFilter, TextureFilter magFilter) { - ByteBuffer buffer = MemoryUtil.memAlloc(imageData.length); - buffer.put(imageData); - buffer.flip(); - - // 使用STB从内存加载图像 - IntBuffer width = MemoryUtil.memAllocInt(1); - IntBuffer height = MemoryUtil.memAllocInt(1); - IntBuffer components = MemoryUtil.memAllocInt(1); - - try { - ByteBuffer pixelData = STBImage.stbi_load_from_memory(buffer, width, height, components, 0); - - if (pixelData == null) { - String error = STBImage.stbi_failure_reason(); - throw new RuntimeException("Failed to load image from bytes: " + error); - } - - TextureFormat format = getTextureFormat(components.get(0)); - Texture texture = new Texture(name, width.get(0), height.get(0), format, pixelData); - - texture.setMinFilter(minFilter); - texture.setMagFilter(magFilter); - - STBImage.stbi_image_free(pixelData); - return texture; - - } finally { - MemoryUtil.memFree(buffer); - MemoryUtil.memFree(width); - MemoryUtil.memFree(height); - MemoryUtil.memFree(components); - } - } - - // ==================== 工具方法 ==================== - - /** - * 获取纹理内存占用估算(字节) - */ - public long getEstimatedMemoryUsage() { - int bytesPerPixel; - switch (type) { - case UNSIGNED_BYTE: - case BYTE: - bytesPerPixel = format.getComponents(); - break; - case UNSIGNED_SHORT: - case SHORT: - bytesPerPixel = format.getComponents() * 2; - break; - case UNSIGNED_INT: - case INT: - case FLOAT: - bytesPerPixel = format.getComponents() * 4; - break; - default: - bytesPerPixel = 4; - } - - long baseMemory = (long) width * height * bytesPerPixel; - - // 如果启用了mipmaps,加上mipmaps的内存 - if (mipmapsEnabled) { - return baseMemory * 4L / 3L; // mipmaps大约增加1/3内存 - } - - return baseMemory; - } - - /** - * 检查尺寸是否为2的幂 - */ - private boolean isPowerOfTwo(int value) { - return value > 0 && (value & (value - 1)) == 0; - } - - /** - * 计算mipmap级别数量 - */ - private int calculateMipmapLevels() { - return (int) Math.floor(Math.log(Math.max(width, height)) / Math.log(2)) + 1; - } - - /** - * 生成纹理ID - */ - private int generateTextureId() { - try { - IntBuffer textures = MemoryUtil.memAllocInt(1); - GL11.glGenTextures(textures); - int textureId = textures.get(0); - MemoryUtil.memFree(textures); - - if (textureId == 0) { - throw new RuntimeException("Failed to generate texture ID"); - } - - return textureId; - } catch (Exception e) { - throw new RuntimeException("Failed to generate texture: " + e.getMessage(), e); - } - } - - /** - * 检查OpenGL错误 - */ - private void checkGLError(String operation) { - int error = GL11.glGetError(); - if (error != GL11.GL_NO_ERROR) { - String errorName = getGLErrorString(error); - System.err.println("OpenGL error during " + operation + ": " + errorName); - // 不再抛出异常,而是记录错误 - } - } - - /** - * 获取 OpenGL 错误字符串 - */ - private String getGLErrorString(int error) { - switch (error) { - case GL11.GL_INVALID_ENUM: - return "GL_INVALID_ENUM"; - case GL11.GL_INVALID_VALUE: - return "GL_INVALID_VALUE"; - case GL11.GL_INVALID_OPERATION: - return "GL_INVALID_OPERATION"; - case GL11.GL_OUT_OF_MEMORY: - return "GL_OUT_OF_MEMORY"; - case GL11.GL_STACK_OVERFLOW: - return "GL_STACK_OVERFLOW"; - case GL11.GL_STACK_UNDERFLOW: - return "GL_STACK_UNDERFLOW"; - default: - return "Unknown Error (0x" + Integer.toHexString(error) + ")"; - } - } - - // ==================== Getter方法 ==================== - - public int getTextureId() { - return textureId; - } - - public String getName() { - return name; - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public TextureFormat getFormat() { - return format; - } - - public TextureType getType() { - return type; - } - - public TextureFilter getMinFilter() { - return minFilter; - } - - public TextureFilter getMagFilter() { - return magFilter; - } - - public TextureWrap getWrapS() { - return wrapS; - } - - public TextureWrap getWrapT() { - return wrapT; - } - - public boolean isMipmapsEnabled() { - return mipmapsEnabled; - } - - public long getCreationTime() { - return creationTime; - } - - // ==================== Object方法 ==================== - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Texture texture = (Texture) o; - return textureId == texture.textureId && - width == texture.width && - height == texture.height && - disposed == texture.disposed && - Objects.equals(name, texture.name) && - format == texture.format && - type == texture.type; - } - - @Override - public int hashCode() { - return Objects.hash(textureId, name, width, height, format, type, disposed); - } - - @Override - public String toString() { - return "Texture{" + - "id=" + textureId + - ", name='" + name + '\'' + - ", size=" + width + "x" + height + - ", format=" + format + - ", type=" + type + - ", memory=" + getEstimatedMemoryUsage() + " bytes" + - ", disposed=" + disposed + - '}'; - } - - // ==================== 静态清理方法 ==================== - - /** - * 清理所有缓存的纹理 - */ - public static void cleanupAll() { - TEXTURE_CACHE.values().forEach(Texture::dispose); - TEXTURE_CACHE.clear(); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java deleted file mode 100644 index 404ee2a..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java +++ /dev/null @@ -1,224 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import org.joml.Vector2f; - -import java.io.Serial; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * 封装一个2D顶点,包含位置、UV坐标和原始位置。 - * - * @author tzdwindows 7 - */ -public class Vertex implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - - private List controlledTriangles = new ArrayList<>(); - public Vector2f position; // 当前顶点位置 (x, y) - public Vector2f uv; // UV坐标 (u, v) - public Vector2f originalPosition; // 原始顶点位置 (用于变形) - private VertexTag tag; - private boolean selected; - private String name; - private boolean isDelete; - public int index; - - /** - * 构造函数 - * - * @param x 顶点 x 坐标 - * @param y 顶点 y 坐标 - * @param u 顶点 u 坐标 - * @param v 顶点 v 坐标 - */ - public Vertex(float x, float y, float u, float v) { - this.position = new Vector2f(x, y); - this.uv = new Vector2f(u, v); - this.originalPosition = new Vector2f(x, y); // 初始时,原始位置=当前位置 - this.tag = VertexTag.DEFAULT; - } - - /** - * 构造函数 - * - * @param position 顶点位置 - * @param uv UV坐标 - */ - public Vertex(Vector2f position, Vector2f uv) { - this.position = new Vector2f(position); - this.uv = new Vector2f(uv); - this.originalPosition = new Vector2f(position); - this.tag = VertexTag.DEFAULT; - } - - public Vertex(Vector2f position, Vector2f uv, VertexTag tag) { - this.position = new Vector2f(position); - this.uv = new Vector2f(uv); - this.originalPosition = new Vector2f(position); - this.tag = tag; - } - - /** - * 构造函数(用于复制) - * - * @param position 顶点位置 - * @param uv UV坐标 - * @param originalPosition 原始位置 - */ - public Vertex(Vector2f position, Vector2f uv, Vector2f originalPosition) { - this.position = new Vector2f(position); - this.uv = new Vector2f(uv); - this.originalPosition = new Vector2f(originalPosition); - this.tag = VertexTag.DEFAULT; - } - - /** - * 便捷构造函数,用于仅通过位置创建顶点 - * UV坐标将默认为 (0, 0)。 - * - * @param x 顶点 x 坐标 - * @param y 顶点 y 坐标 - */ - public Vertex(float x, float y) { - this.position = new Vector2f(x, y); - this.uv = new Vector2f(0, 0); // 为UV坐标提供一个默认值 - this.originalPosition = new Vector2f(x, y); - this.tag = VertexTag.DEFAULT; - } - - public VertexTag getTag() { - return tag; - } - - public void setTag(VertexTag tag) { - this.tag = tag; - } - - public void setIndex(int index) { - this.index = index; - } - - public void delete(){ - isDelete = true; - } - - public int getIndex() { - return index; - } - - /** - * 重置为原始位置 - */ - public void resetToOriginal() { - this.position.set(this.originalPosition); - } - - /** - * 保存当前位置为新的原始位置 - */ - public void saveAsOriginal() { - this.originalPosition.set(this.position); - } - - /** - * 设置顶点是否被选中(在渲染中被调用) - * @param selected 是否被选中 - */ - public void setSelected(boolean selected) { - this.selected = selected; - } - - /** - * 获取顶点是否被选中 - * @return 是否被选中 - */ - public boolean isSelected() { - return selected; - } - - public boolean isDelete() { - return isDelete; - } - - /** - * 创建此顶点的深拷贝 - */ - public Vertex copy() { - Vertex copy = new Vertex(this.position, this.uv, this.originalPosition); - copy.setTag(this.tag); - copy.setSelected(this.selected); - copy.setName(this.name); - copy.setIndex(this.index); - if (this.controlledTriangles != null) { - copy.setControlledTriangles(new ArrayList<>(this.controlledTriangles)); - } else { - copy.setControlledTriangles(new ArrayList<>()); - } - if (this.isDelete) { - copy.delete(); - } - return copy; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Vertex vertex = (Vertex) o; - return selected == vertex.selected && - isDelete == vertex.isDelete && - Objects.equals(position, vertex.position) && - Objects.equals(uv, vertex.uv) && - Objects.equals(originalPosition, vertex.originalPosition) && - tag == vertex.tag && - Objects.equals(name, vertex.name); - } - - public boolean _equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Vertex vertex = (Vertex) o; - return isDelete == vertex.isDelete && - Objects.equals(position, vertex.position) && - tag == vertex.tag && - Objects.equals(name, vertex.name); - } - - @Override - public int hashCode() { - return Objects.hash(position, uv, originalPosition, tag, selected, name, isDelete); - } - - @Override - public String toString() { - return "Vertex{" + - "name='" + name + '\'' + - ", pos=" + position + - ", uv=" + uv + - ", orig=" + originalPosition + - ", tag=" + tag + - ", selected=" + selected + - ", isDelete=" + isDelete + - '}'; - } - - public void setName(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public List getControlledTriangles() { - return controlledTriangles; - } - - public void setControlledTriangles(List controlledTriangles) { - this.controlledTriangles = (controlledTriangles != null) ? controlledTriangles : new ArrayList<>(); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexList.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexList.java deleted file mode 100644 index dee9c1a..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexList.java +++ /dev/null @@ -1,294 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import org.jetbrains.annotations.NotNull; - -import java.util.*; - -/** - * 一个自包含的几何数据单元,用于管理顶点(vertices) - * 以及定义它们之间拓扑结构(三角形)的索引(indices)。 - * - * @author Gemini - */ -public class VertexList implements Iterable { - - public final List vertices; - private String name; - private int[] indices; - - /** - * 默认构造函数。 - */ - public VertexList() { - this.vertices = new ArrayList<>(); - this.name = "KongFuZi"; - this.indices = new int[0]; // 初始化为空数组 - } - - /** - * 构造函数 - * - * @param name 此列表的名称 - */ - public VertexList(String name) { - this.vertices = new ArrayList<>(); - this.name = Objects.requireNonNull(name, "Name cannot be null"); - this.indices = new int[0]; // 初始化为空数组 - } - - /** - * 构造函数 - * - * @param name 此列表的名称 - * @param initialVertices 用于初始化列表的顶点集合 - * @param initialIndices 用于初始化列表的索引集合 - */ - public VertexList(String name, Collection initialVertices, int[] initialIndices) { - this(name); - if (initialVertices != null) { - this.vertices.addAll(initialVertices); - } - setIndices(initialIndices); // 使用setter来安全地设置初始索引 - } - - // --- 列表管理 (已更新以支持索引) --- - - /** - * 向列表末尾添加一个顶点。 - * 注意:这不会自动更新索引。您需要手动调用 setIndices() 来使用这个新顶点。 - * - * @param vertex 要添加的顶点 - */ - public void add(Vertex vertex) { - if (vertex != null) { - vertex.setIndex(this.vertices.size()); - this.vertices.add(vertex); - } - } - - /** - * 移除列表中的指定顶点,并安全地移除所有引用它的三角形,同时重映射所有后续索引。 - * - * @param vertex 要移除的顶点 - * @return 如果成功移除则为 true - */ - public boolean remove(Object vertex) { - int index = this.vertices.indexOf(vertex); - if (index != -1) { - remove(index); // 委托给基于索引的移除方法 - return true; - } - return false; - } - - /** - * [已重构] 移除指定索引处的顶点,并安全地移除所有引用它的三角形,同时重映射所有后续索引。 - * - * @param index 要移除的索引 - * @return 被移除的顶点 - */ - public Vertex remove(int index) { - if (index < 0 || index >= this.vertices.size()) { - throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + this.vertices.size()); - } - - // 核心逻辑:在移除顶点前,必须重构索引数组 - List newIndicesList = new ArrayList<>(); - for (int i = 0; i < this.indices.length; i += 3) { - int i1 = this.indices[i]; - int i2 = this.indices[i + 1]; - int i3 = this.indices[i + 2]; - - // 如果三角形包含被删除的顶点,则整个三角形都应该被丢弃 - if (i1 == index || i2 == index || i3 == index) { - continue; // 跳过这个三角形 - } - - // 调整索引编号:任何大于被删除索引的索引都需要减一 - if (i1 > index) i1--; - if (i2 > index) i2--; - if (i3 > index) i3--; - - newIndicesList.add(i1); - newIndicesList.add(i2); - newIndicesList.add(i3); - } - - // 用重构后的新索引数组替换旧的 - this.indices = newIndicesList.stream().mapToInt(Integer::intValue).toArray(); - - // 最后,安全地从列表中移除顶点 - return this.vertices.remove(index); - } - - /** - * 获取指定索引处的顶点。 - */ - public Vertex get(int index) { - return this.vertices.get(index); - } - - /** - * 返回列表中的顶点数量。 - */ - public int size() { - return this.vertices.size(); - } - - /** - * 检查列表是否为空。 - */ - public boolean isEmpty() { - return this.vertices.isEmpty(); - } - - /** - * [已重构] 清空列表中的所有顶点和索引。 - */ - public void clear() { - this.vertices.clear(); - this.indices = new int[0]; - } - - /** - * 返回内部顶点列表的副本。 - * - * @return 顶点的列表副本 - */ - public List getVertices() { - return new ArrayList<>(vertices); - } - - /** - * 根据标签过滤并返回顶点列表。 - * - * @return 过滤后的顶点列表 - */ - public List getVertices(VertexTag tag) { - return vertices.stream() - .filter(vertex -> vertex.getTag() == tag) - .toList(); - } - - // --- Getter 和 Setter (已更新以支持索引) --- - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = Objects.requireNonNull(name, "Name cannot be null"); - } - - /** - * [新增] 获取索引数组的副本。 - * - * @return 索引数组的克隆 - */ - public int[] getIndices() { - return indices; - } - - /** - * [新增] 获取仅由具有指定标签(Tag)的顶点组成的三角形索引。 - * - *

此方法会执行以下操作: - *

    - *
  1. 筛选出所有具有指定 {@code tag} 的顶点。
  2. - *
  3. 遍历原始索引数组中的所有三角形。
  4. - *
  5. 如果一个三角形的【全部三个顶点】都符合指定的 {@code tag},则保留该三角形。
  6. - *
  7. 将这些保留下来的三角形的索引,重映射到仅包含筛选后顶点的新索引空间中。
  8. - *
- * - * @param tag 要筛选的顶点标签 - * @return 一个新的索引数组,其中的索引值对应于通过 {@code getVertices(tag)} 获取的顶点列表。 - */ - public int[] getIndices(VertexTag tag) { - if (tag == null) { - return new int[0]; - } - - // 1. 快速找到所有带标签的顶点的原始索引,存入一个Set以便快速查找 - Set taggedOriginalIndices = new HashSet<>(); - for (int i = 0; i < this.vertices.size(); i++) { - if (this.vertices.get(i).getTag() == tag) { - taggedOriginalIndices.add(i); - } - } - - // 如果没有任何顶点带此标签,则不可能有相关三角形 - if (taggedOriginalIndices.isEmpty()) { - return new int[0]; - } - - // 2. 遍历所有三角形,只要有一个顶点的索引在Set中,就保留该三角形 - List newIndices = new ArrayList<>(); - for (int i = 0; i < this.indices.length; i += 3) { - int i1 = this.indices[i]; - int i2 = this.indices[i + 1]; - int i3 = this.indices[i + 2]; - - // 核心逻辑修改:从 && (AND) 改为 || (OR) - if (taggedOriginalIndices.contains(i1) || - taggedOriginalIndices.contains(i2) || - taggedOriginalIndices.contains(i3)) { - - // 添加原始索引,因为它们将用于原始的、完整的顶点列表 - newIndices.add(i1); - newIndices.add(i2); - newIndices.add(i3); - } - } - - return newIndices.stream().mapToInt(Integer::intValue).toArray(); - } - - /** - * [新增] 设置索引数组。 - * - * @param indices 新的索引数组 - */ - public void setIndices(int[] indices) { - this.indices = (indices != null) ? indices.clone() : new int[0]; - } - - - // --- 迭代器 --- - - @Override - public @NotNull Iterator iterator() { - return this.vertices.iterator(); - } - - // --- Object 方法 --- - - @Override - public String toString() { - return "VertexList{" + - "name='" + name + '\'' + - ", vertexCount=" + vertices.size() + - ", indexCount=" + indices.length + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - VertexList that = (VertexList) o; - return Objects.equals(vertices, that.vertices) && - Objects.equals(name, that.name) && - Arrays.equals(indices, that.indices); - } - - @Override - public int hashCode() { - int result = Objects.hash(vertices, name); - result = 31 * result + Arrays.hashCode(indices); - return result; - } - - public void set(int index, Vertex vertex) { - this.vertices.set(index, vertex); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexTag.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexTag.java deleted file mode 100644 index 1877101..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexTag.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -/** - * 定义 VertexList 的标签类型。 - */ -public enum VertexTag { - /** - * 用于变形的顶点 - */ - DEFORMATION, - - /** - * 默认的顶点 - */ - DEFAULT, - - /** - * 其他类型的顶点 - */ - OTHER -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/manager/RanderToolsManager.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/manager/RanderToolsManager.java deleted file mode 100644 index 01ec103..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/manager/RanderToolsManager.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util.manager; - -import com.chuangzhou.vivid2D.render.awt.tools.Tool; -import com.chuangzhou.vivid2D.render.model.util.tools.RanderTools; -import org.joml.Matrix3f; -import org.slf4j.Logger; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 渲染工具管理器 - * 负责管理Tool和RanderTools之间的绑定关系 - */ -public class RanderToolsManager { - private static Logger logger = org.slf4j.LoggerFactory.getLogger(RanderToolsManager.class); - - // 存储Tool和RanderTools的绑定关系 - private final Map toolRanderToolsMap; - - // 单例实例 - private static volatile RanderToolsManager instance; - - /** - * 私有构造函数 - */ - private RanderToolsManager() { - this.toolRanderToolsMap = new ConcurrentHashMap<>(); - } - - /** - * 获取单例实例 - * @return RanderToolsManager单例 - */ - public static RanderToolsManager getInstance() { - if (instance == null) { - synchronized (RanderToolsManager.class) { - if (instance == null) { - instance = new RanderToolsManager(); - } - } - } - return instance; - } - - // ================== 核心方法 ================== - - /** - * 绑定Tool和RanderTools(自动初始化) - * @param tool Tool对象 - * @param randerTools RanderTools对象 - * @return 是否绑定成功 - */ - public boolean bindToolWithRanderTools(Tool tool, RanderTools randerTools) { - if (tool == null || randerTools == null) { - return false; - } - toolRanderToolsMap.put(tool, randerTools); - tool.setAssociatedRanderTools(randerTools); - randerTools.init(); - return true; - } - - - /** - * 一键渲染所有绑定的工具 - * @param modelMatrix 模型矩阵 - * @param renderContext 渲染上下文 - * @return 成功渲染的工具数量 - */ - public int renderAllTools(Matrix3f modelMatrix, Object renderContext) { - int successCount = 0; - for (RanderTools randerTools : toolRanderToolsMap.values()) { - try { - if (randerTools.render(modelMatrix, renderContext)) { - successCount++; - } - } catch (Exception e) { - e.printStackTrace(); - logger.info("渲染工具执行失败: {} - {}", randerTools.getClass().getSimpleName(), e.getMessage()); - } - } - return successCount; - } - - // ================== 查询方法 ================== - - /** - * 获取Tool对应的RanderTools - * @param tool Tool对象 - * @return 对应的RanderTools,如果没有绑定返回null - */ - public RanderTools getRanderToolsForTool(Tool tool) { - return toolRanderToolsMap.get(tool); - } - - /** - * 检查Tool是否已绑定RanderTools - * @param tool Tool对象 - * @return 是否已绑定 - */ - public boolean isToolBound(Tool tool) { - return toolRanderToolsMap.containsKey(tool); - } - - /** - * 获取绑定数量 - * @return 当前绑定数量 - */ - public int getBindingCount() { - return toolRanderToolsMap.size(); - } - - // ================== 清理方法 ================== - - /** - * 清理所有绑定 - */ - public void clearAllBindings() { - // 清理Tool中的关联引用 - for (Tool tool : toolRanderToolsMap.keySet()) { - tool.setAssociatedRanderTools(null); - } - toolRanderToolsMap.clear(); - } - - @Override - public String toString() { - return String.format("RanderToolsManager[bindings=%d]", getBindingCount()); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/RanderTools.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/RanderTools.java deleted file mode 100644 index 73218c8..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/RanderTools.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util.tools; - -import org.joml.Matrix3f; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * 抽象渲染工具类 - * 提供基础的算法启用标识管理和渲染功能 - */ -public abstract class RanderTools { - private static final Logger logger = LoggerFactory.getLogger(RanderTools.class); - - /** - * 算法启用标识 - * true表示算法启用,false表示算法禁用 - */ - protected Map algorithmEnabled = new LinkedHashMap<>(); - - /** - * 默认构造函数 - */ - public RanderTools() {} - - public void init(){ - init(algorithmEnabled); - } - - /** - * 初始化算法启用标识 - * @param algorithmEnabled 算法启用标识 - */ - public abstract void init(Map algorithmEnabled); - - /** - * 抽象渲染方法 - * 子类必须实现具体地渲染逻辑 - * - * @param renderContext 渲染上下文对象 - * @return 渲染结果,通常为boolean表示渲染成功与否 - */ - public abstract boolean render(Matrix3f modelMatrix,Object renderContext); - - /** - * 获取算法启用标识 - * @param algorithmName 算法名称 - * @return 算法启用标识 - */ - public boolean isAlgorithmEnabled(String algorithmName) { - return algorithmEnabled.getOrDefault(algorithmName, false); - } - - /** - * 获取算法启用标识 - * @return 算法启用标识 - */ - public Map getAlgorithmEnabled() { - return new LinkedHashMap<>(algorithmEnabled); - } - - /** - * 设置算法启用标识 - * @param algorithmName 算法名称 - * @param enabled 启用标识 - */ - public void setAlgorithmEnabled(String algorithmName, boolean enabled) { - // 如果算法名称不存在,自动添加 - if (!algorithmEnabled.containsKey(algorithmName)) { - logger.warn("算法名称不存在,自动添加:{}", algorithmName); - } - algorithmEnabled.put(algorithmName, enabled); - } - - /** - * 添加新的算法标识 - * @param algorithmName 算法名称 - * @param enabled 初始启用状态 - */ - public void addAlgorithm(String algorithmName, boolean enabled) { - algorithmEnabled.put(algorithmName, enabled); - logger.debug("添加算法:{},初始状态:{}", algorithmName, enabled); - } - - /** - * 移除算法标识 - * @param algorithmName 算法名称 - */ - public void removeAlgorithm(String algorithmName) { - if (algorithmEnabled.remove(algorithmName) != null) { - logger.debug("移除算法:{}", algorithmName); - } - } - - /** - * 启用所有算法 - */ - public void enableAllAlgorithms() { - for (String algorithmName : algorithmEnabled.keySet()) { - algorithmEnabled.put(algorithmName, true); - } - } - - /** - * 禁用所有算法 - */ - public void disableAllAlgorithms() { - for (String algorithmName : algorithmEnabled.keySet()) { - algorithmEnabled.put(algorithmName, false); - } - } - - /** - * 检查算法是否存在 - * @param algorithmName 算法名称 - * @return 是否存在 - */ - public boolean hasAlgorithm(String algorithmName) { - return algorithmEnabled.containsKey(algorithmName); - } - - /** - * 获取工具状态信息 - * @return 包含工具状态信息的字符串 - */ - public String getStatus() { - return String.format("RanderTools[algorithmEnabled=%s]", algorithmEnabled); - } - - /** - * 获取详细状态信息 - * @return 详细状态字符串 - */ - public String getDetailedStatus() { - StringBuilder sb = new StringBuilder(); - sb.append("RanderTools Status:\n"); - sb.append(" Algorithms: ").append(algorithmEnabled.size()).append("\n"); - - for (Map.Entry entry : algorithmEnabled.entrySet()) { - sb.append(" - ").append(entry.getKey()).append(": ").append(entry.getValue()).append("\n"); - } - - return sb.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/VertexDeformationRander.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/VertexDeformationRander.java deleted file mode 100644 index ef720f1..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/VertexDeformationRander.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util.tools; - -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.Vertex; -import com.chuangzhou.vivid2D.render.model.util.VertexTag; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; -import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; -import org.joml.Matrix3f; -import org.joml.Vector2f; -import org.joml.Vector4f; -import org.lwjgl.opengl.GL11; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - -/** - * [已修改] 顶点变形渲染工具 (支持外部控制笼) - * 该类现在渲染: - * 1. 底层被变形网格的【三角剖分线框】。 - * 2. 位于上层的【外部控制笼】(控制点和连线)。 - * 这样可以直观地展示控制笼如何影响网格变形。 - */ -public class VertexDeformationRander extends RanderTools { - - private static final float DEFORMATION_VERTEX_SIZE = 6.0f; - - @Override - public void init(Map algorithmEnabled) { - algorithmEnabled.put("showDeformationVertices", false); - algorithmEnabled.put("showDeformationVertexInfluence", true); - } - - @Override - public boolean render(Matrix3f modelMatrix, Object renderContext) { - if (renderContext instanceof Mesh2D mesh2D) { - if (mesh2D.getStates("showDeformationVertices")) { - RenderSystem.pushState(); - try { - mesh2D.setSolidShader(modelMatrix); - drawControlCageAndMesh(mesh2D); - } finally { - RenderSystem.popState(); - } - return true; - } - } - return false; - } - - /** - * [已修改] 此方法现在会先渲染底层网格的线框,然后再渲染上层的控制笼。 - * @param mesh2D 要渲染的网格 (作为被变形的对象) - */ - private void drawControlCageAndMesh(Mesh2D mesh2D) { - List controlCage = mesh2D.getDeformationControlVertices(); - Tesselator t = Tesselator.getInstance(); - BufferBuilder bb = t.getBuilder(); - RenderSystem.pushState(); - try { - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - GL11.glDisable(GL11.GL_DEPTH_TEST); - drawMeshWireframe(bb, mesh2D); - if (controlCage != null && !controlCage.isEmpty()) { - List orderedCage = getOrderedVertices(controlCage); - drawConnectionLines(bb, orderedCage); - for (Vertex vertex : orderedCage) { - if (vertex == null){ - continue; - } - drawStyledVertex(bb, vertex.position.x, vertex.position.y, vertex.isSelected()); - } - } - } finally { - RenderSystem.popState(); - } - } - - /** - * 绘制网格的线框,以显示其三角剖分结构。 - * 这就是“Vertex 的控制区域”的可视化。 - * @param bb BufferBuilder 实例 - * @param mesh 目标网格 - */ - private void drawMeshWireframe(BufferBuilder bb, Mesh2D mesh) { - List vertices = mesh.getActiveVertexList().getVertices(); - int[] indices = mesh.getActiveVertexList().getIndices(VertexTag.DEFORMATION); - if (vertices == null || vertices.isEmpty() || indices == null || indices.length == 0) { - return; - } - Vector4f wireframeColor = new Vector4f(0.5f, 0.5f, 0.5f, 0.4f); - GL11.glLineWidth(1.0f); - for (int i = 0; i < indices.length; i += 3) { - int i1 = indices[i]; - int i2 = indices[i + 1]; - int i3 = indices[i + 2]; - Vertex v1 = vertices.get(i1); - Vertex v2 = vertices.get(i2); - Vertex v3 = vertices.get(i3); - bb.begin(GL11.GL_LINE_LOOP, 3); - bb.setColor(wireframeColor); - bb.vertex(v1.position.x, v1.position.y, 0f, 0f); - bb.vertex(v2.position.x, v2.position.y, 0f, 0f); - bb.vertex(v3.position.x, v3.position.y, 0f, 0f); - bb.endImmediate(); - } - } - - /** - * 绘制连接控制顶点的虚线。 - */ - private void drawConnectionLines(BufferBuilder bb, List verts) { - if (verts == null || verts.size() < 2) return; - Vector4f lineColor = new Vector4f(0.1f, 0.1f, 0.1f, 0.85f); - - for (int i = 0; i < verts.size(); i++) { - Vector2f a = verts.get(i).position; - Vector2f b = verts.get((i + 1) % verts.size()).position; // 使用 % 来处理首尾相连 - drawDashedLine(bb, a.x, a.y, b.x, b.y, 4, 4, lineColor); - } - } - - /** - * 绘制自定义样式的顶点,并根据 'isSelected' 状态改变颜色。 - */ - private void drawStyledVertex(BufferBuilder bb, float cx, float cy, boolean isSelected) { - float halfSize = VertexDeformationRander.DEFORMATION_VERTEX_SIZE / 2.0f; - Vector4f fillColor = isSelected ? new Vector4f(1.0f, 0.3f, 0.3f, 1.0f) : new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); - - bb.begin(GL11.GL_QUADS, 4); - bb.setColor(fillColor); - bb.vertex(cx - halfSize, cy - halfSize, 0f, 0f); - bb.vertex(cx + halfSize, cy - halfSize, 0f, 0f); - bb.vertex(cx + halfSize, cy + halfSize, 0f, 0f); - bb.vertex(cx - halfSize, cy + halfSize, 0f, 0f); - bb.endImmediate(); - - GL11.glLineWidth(1.5f); - bb.begin(GL11.GL_LINE_LOOP, 4); - bb.setColor(new Vector4f(0.0f, 0.0f, 0.0f, 1.0f)); - bb.vertex(cx - halfSize, cy - halfSize, 0f, 0f); - bb.vertex(cx + halfSize, cy - halfSize, 0f, 0f); - bb.vertex(cx + halfSize, cy + halfSize, 0f, 0f); - bb.vertex(cx - halfSize, cy + halfSize, 0f, 0f); - bb.endImmediate(); - GL11.glLineWidth(1.0f); - } - - /** - * 辅助方法,获取按极角排序后的顶点列表 (Vertex 对象)。 - */ - private List getOrderedVertices(List verts) { - if (verts == null || verts.size() <= 1) return new ArrayList<>(verts); - - Vector2f center = new Vector2f(0f, 0f); - for (Vertex v : verts) { center.add(v.position); } - center.div(verts.size()); - - List sortedVerts = new ArrayList<>(verts); - sortedVerts.sort(Comparator.comparingDouble(v -> Math.atan2(v.position.y - center.y, v.position.x - center.x))); - return sortedVerts; - } - - /** - * 绘制虚线的底层实现。 - */ - private void drawDashedLine(BufferBuilder bb, float x1, float y1, float x2, float y2, float segmentLen, float gapLen, Vector4f color) { - float dx = x2 - x1, dy = y2 - y1; - float total = (float)Math.sqrt(dx*dx + dy*dy); - if (total < 1e-4f) return; - float nx = dx / total, ny = dy / total; - float pos = 0f; - GL11.glLineWidth(1.5f); // 让虚线稍微粗一点 - while (pos < total) { - float segStart = pos; - float segEnd = Math.min(total, pos + segmentLen); - if (segEnd > segStart) { - float sx = x1 + nx * segStart, sy = y1 + ny * segStart; - float ex = x1 + nx * segEnd, ey = y1 + ny * segEnd; - bb.begin(GL11.GL_LINES, 2); - bb.setColor(color); - bb.vertex(sx, sy, 0f, 0f); bb.vertex(ex, ey, 0f, 0f); - bb.endImmediate(); - } - pos += segmentLen + gapLen; - } - GL11.glLineWidth(1.0f); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/Camera.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/Camera.java deleted file mode 100644 index 341827e..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/Camera.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems; - -import org.joml.Vector2f; - -/** - * 摄像机类 - * - * @author tzdwindows 7 - */ -public class Camera { - private final Vector2f position = new Vector2f(0.0f, 0.0f); - private float zoom = 1.0f; - private float zPosition = 0.0f; - private boolean enabled = true; - - public Camera() { - } - - public void setPosition(float x, float y) { - position.set(x, y); - } - - public void setPosition(Vector2f pos) { - position.set(pos); - } - - public Vector2f getPosition() { - return new Vector2f(position); - } - - public void setZoom(float zoom) { - this.zoom = Math.max(0.1f, Math.min(10.0f, zoom)); - } - - public float getZoom() { - return zoom; - } - - public void setZPosition(float z) { - this.zPosition = z; - } - - public float getZPosition() { - return zPosition; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - /** - * 获取摄像机是否启用 - */ - public boolean isEnabled() { - return enabled; - } - - public void move(float dx, float dy) { - position.add(dx, dy); - } - - public void zoom(float factor) { - zoom *= factor; - zoom = Math.max(0.1f, Math.min(10.0f, zoom)); - } - - public void reset() { - position.set(0.0f, 0.0f); - zoom = 1.0f; - zPosition = 0.0f; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/Matrix3fUtils.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/Matrix3fUtils.java deleted file mode 100644 index e37ec2a..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/Matrix3fUtils.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems; - -import org.joml.Matrix3f; -import org.joml.Vector2f; - -/** - * @author tzdwindows 7 - */ -public class Matrix3fUtils { - public static Vector2f transformPoint(Matrix3f matrix, Vector2f point, Vector2f dest) { - float x = matrix.m00() * point.x + matrix.m01() * point.y + matrix.m02(); - float y = matrix.m10() * point.x + matrix.m11() * point.y + matrix.m12(); - return dest.set(x, y); - } - - public static Vector2f transformPoint(Matrix3f matrix, Vector2f point) { - return transformPoint(matrix, point, new Vector2f()); - } - - public static Vector2f transformPointInverse(Matrix3f matrix, Vector2f point, Vector2f dest) { - Matrix3f inverse = new Matrix3f(matrix).invert(); - return transformPoint(inverse, point, dest); - } - - public static Vector2f transformPointInverse(Matrix3f matrix, Vector2f point) { - return transformPointInverse(matrix, point, new Vector2f()); - } - - /** - * 变换向量(不考虑平移,只考虑旋转和缩放) - */ - public static Vector2f transformVector(Matrix3f matrix, Vector2f vector, Vector2f dest) { - float x = matrix.m00() * vector.x + matrix.m01() * vector.y; - float y = matrix.m10() * vector.x + matrix.m11() * vector.y; - return dest.set(x, y); - } - - public static Vector2f transformVector(Matrix3f matrix, Vector2f vector) { - return transformVector(matrix, vector, new Vector2f()); - } - - /** - * 逆变换向量(不考虑平移,只考虑旋转和缩放的逆) - */ - public static Vector2f transformVectorInverse(Matrix3f matrix, Vector2f vector, Vector2f dest) { - // 计算2x2子矩阵的行列式 - float det = matrix.m00() * matrix.m11() - matrix.m01() * matrix.m10(); - - if (Math.abs(det) < 1e-6f) { - return dest.set(vector); - } - - float invDet = 1.0f / det; - - // 计算2x2子矩阵的逆 - float m00 = matrix.m11() * invDet; - float m01 = -matrix.m01() * invDet; - float m10 = -matrix.m10() * invDet; - float m11 = matrix.m00() * invDet; - - float x = vector.x * m00 + vector.y * m01; - float y = vector.x * m10 + vector.y * m11; - - return dest.set(x, y); - } - - public static Vector2f transformVectorInverse(Matrix3f matrix, Vector2f vector) { - return transformVectorInverse(matrix, vector, new Vector2f()); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java deleted file mode 100644 index 8383a43..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/RenderSystem.java +++ /dev/null @@ -1,1785 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems; - -import org.lwjgl.opengl.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.function.IntSupplier; -import java.util.function.Supplier; - -/** - * vivid2D 渲染系统 - * - *

提供线程安全的 OpenGL 调用抽象,支持渲染命令队列

- * - * @author tzdwindows 7 - * @version 1.0 - * @since 2025-10-16 - */ -public final class RenderSystem { - private static final Logger logger = LoggerFactory.getLogger(RenderSystem.class); - - private RenderSystem() { /* no instances */ } - - // ================== 线程管理 ================== - private static Thread gameThread; - private static Thread renderThread; - private static boolean isInInit; - - // ================== 渲染命令队列 ================== - private static final Queue renderQueue = new ConcurrentLinkedQueue<>(); - private static boolean isReplayingQueue; - - // ================== 状态管理 ================== - private static int viewportWidth = 800; - private static int viewportHeight = 600; - private static final float[] clearColor = new float[]{0.0f, 0.0f, 0.0f, 1.0f}; - public static final int GL_MULTISAMPLE = GL13.GL_MULTISAMPLE; - - // 纹理过滤模式常量 - public static final int FILTER_NEAREST = GL11.GL_NEAREST; - public static final int FILTER_LINEAR = GL11.GL_LINEAR; - public static final int FILTER_NEAREST_MIPMAP_NEAREST = GL11.GL_NEAREST_MIPMAP_NEAREST; - public static final int FILTER_LINEAR_MIPMAP_NEAREST = GL11.GL_LINEAR_MIPMAP_NEAREST; - public static final int FILTER_NEAREST_MIPMAP_LINEAR = GL11.GL_NEAREST_MIPMAP_LINEAR; - public static final int FILTER_LINEAR_MIPMAP_LINEAR = GL11.GL_LINEAR_MIPMAP_LINEAR; - - // 纹理环绕模式常量 - public static final int WRAP_REPEAT = GL11.GL_REPEAT; - public static final int WRAP_CLAMP_TO_EDGE = GL12.GL_CLAMP_TO_EDGE; - public static final int WRAP_CLAMP_TO_BORDER = GL13.GL_CLAMP_TO_BORDER; - public static final int WRAP_MIRRORED_REPEAT = GL14.GL_MIRRORED_REPEAT; - - public static final int GL_ARRAY_BUFFER = GL15.GL_ARRAY_BUFFER; - public static final int GL_ELEMENT_ARRAY_BUFFER = GL15.GL_ELEMENT_ARRAY_BUFFER; - public static final int GL_STATIC_DRAW = GL15.GL_STATIC_DRAW; - public static final int GL_DYNAMIC_DRAW = GL15.GL_DYNAMIC_DRAW; - public static final int GL_STREAM_DRAW = GL15.GL_STREAM_DRAW; - public static final int GL_FLOAT = GL11.GL_FLOAT; - - // ================== 绘制模式常量 ================== - public static final int DRAW_POINTS = GL11.GL_POINTS; - public static final int DRAW_LINES = GL11.GL_LINES; - public static final int DRAW_LINE_LOOP = GL11.GL_LINE_LOOP; - public static final int DRAW_LINE_STRIP = GL11.GL_LINE_STRIP; - public static final int DRAW_TRIANGLES = GL11.GL_TRIANGLES; - public static final int DRAW_TRIANGLE_STRIP = GL11.GL_TRIANGLE_STRIP; - public static final int DRAW_TRIANGLE_FAN = GL11.GL_TRIANGLE_FAN; - public static final int DRAW_QUADS = GL11.GL_QUADS; - public static final int GL_LINE_LOOP = GL11.GL_LINE_LOOP; - public static final int GL_LINE_STRIP = GL11.GL_LINE_STRIP; - public static final int GL_TRIANGLE_FAN = GL11.GL_TRIANGLE_FAN; - public static final int GL_QUADS = GL11.GL_QUADS; - public static final int GL_TRIANGLES = GL11.GL_TRIANGLES; - - // ================== 索引类型常量 ================== - public static final int GL_UNSIGNED_BYTE = GL11.GL_UNSIGNED_BYTE; - public static final int GL_UNSIGNED_SHORT = GL11.GL_UNSIGNED_SHORT; - public static final int GL_UNSIGNED_INT = GL11.GL_UNSIGNED_INT; - public static final int GL_TEXTURE0 = GL13.GL_TEXTURE0; - - public static final int GL_PACK_ALIGNMENT = GL11.GL_PACK_ALIGNMENT; - public static final int GL_UNPACK_ALIGNMENT = GL11.GL_UNPACK_ALIGNMENT; - - public static final int GL_COLOR_BUFFER_BIT = GL11.GL_COLOR_BUFFER_BIT; - public static final int GL_DEPTH_BUFFER_BIT = GL11.GL_DEPTH_BUFFER_BIT; - public static final int GL_STENCIL_BUFFER_BIT = GL11.GL_STENCIL_BUFFER_BIT; - - public static final int GL_RGBA = GL11.GL_RGBA; - public static final int GL_RGB = GL11.GL_RGB; - public static final int GL_BGRA = GL12.GL_BGRA; - public static final int GL_BGR = GL12.GL_BGR; - public static final int GL_RED = GL11.GL_RED; - public static final int GL_RG = GL30.GL_RG; - - public static final int GL_BYTE = GL11.GL_BYTE; - public static final int GL_SHORT = GL11.GL_SHORT; - public static final int GL_INT = GL11.GL_INT; - - public static final int GL_TRUE = org.lwjgl.opengl.GL11.GL_TRUE; - public static final int GL_COMPILE_STATUS = org.lwjgl.opengl.GL20.GL_COMPILE_STATUS; - public static final int GL_LINK_STATUS = org.lwjgl.opengl.GL20.GL_LINK_STATUS; - public static final int GL_VALIDATE_STATUS = org.lwjgl.opengl.GL20.GL_VALIDATE_STATUS; - private static final java.util.Deque stateStack = new java.util.ArrayDeque<>(); - - /** - * 渲染状态快照类 - */ - private static class RenderState { - private int currentProgram; - private boolean blendEnabled; - private boolean depthTestEnabled; - private int blendSrcFactor; - private int blendDstFactor; - private int activeTexture; - private int boundTexture; - private float[] clearColor; - private int[] viewport; - - // 默认构造:尝试安全读取 GL 状态;如果失败则使用默认值(不抛异常) - public RenderState() { - setDefaults(); - // 使用 MemoryStack 能更安全地分配临时缓冲并自动释放 - try (org.lwjgl.system.MemoryStack stack = org.lwjgl.system.MemoryStack.stackPush()) { - - // current program - try { - int prog = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM); - if (prog >= 0) this.currentProgram = prog; - } catch (Throwable ex) { - logger.debug("Could not read GL_CURRENT_PROGRAM: {}", ex.getMessage()); - } - - // blend / depth flags - try { - this.blendEnabled = GL11.glIsEnabled(GL11.GL_BLEND); - } catch (Throwable ex) { - logger.debug("Could not read GL_BLEND enabled: {}", ex.getMessage()); - } - try { - this.depthTestEnabled = GL11.glIsEnabled(GL11.GL_DEPTH_TEST); - } catch (Throwable ex) { - logger.debug("Could not read GL_DEPTH_TEST enabled: {}", ex.getMessage()); - } - - // blend func (src/dst) — 分开询问并保护 - try { - java.nio.IntBuffer buf = stack.mallocInt(1); - GL11.glGetIntegerv(GL14.GL_BLEND_SRC_ALPHA, buf); // 尝试安全常量 - int src = buf.get(0); - if (isValidBlendFunc(src)) this.blendSrcFactor = src; - } catch (Throwable ex) { - // 退回到原有常量查询(兼容旧驱动),但都包裹在 try/catch - try { - java.nio.IntBuffer buf2 = stack.mallocInt(1); - GL11.glGetIntegerv(GL11.GL_BLEND_SRC, buf2); - int s = buf2.get(0); - if (isValidBlendFunc(s)) this.blendSrcFactor = s; - } catch (Throwable ex2) { - logger.debug("Could not read blend src: {}, {}", ex.getMessage(), ex2.getMessage()); - } - } - - try { - java.nio.IntBuffer buf = stack.mallocInt(1); - GL11.glGetIntegerv(GL11.GL_BLEND_DST, buf); - int dst = buf.get(0); - if (isValidBlendFunc(dst)) this.blendDstFactor = dst; - } catch (Throwable ex) { - logger.debug("Could not read blend dst: {}", ex.getMessage()); - } - - // active texture & bound texture —— 使用安全范围检查 - try { - java.nio.IntBuffer buf = stack.mallocInt(1); - GL11.glGetIntegerv(GL13.GL_ACTIVE_TEXTURE, buf); - int at = buf.get(0); - if (at >= GL13.GL_TEXTURE0 && at <= GL13.GL_TEXTURE31) { - this.activeTexture = at; - } else { - // 保持默认 - } - - GL11.glGetIntegerv(GL11.GL_TEXTURE_BINDING_2D, buf); - int bt = buf.get(0); - if (bt >= 0) this.boundTexture = bt; - } catch (Throwable ex) { - logger.debug("Could not read texture state: {}", ex.getMessage()); - } - - // clear color - try { - java.nio.FloatBuffer fbuf = stack.mallocFloat(4); - GL11.glGetFloatv(GL11.GL_COLOR_CLEAR_VALUE, fbuf); - this.clearColor = new float[]{fbuf.get(0), fbuf.get(1), fbuf.get(2), fbuf.get(3)}; - } catch (Throwable ex) { - logger.debug("Could not read clear color: {}", ex.getMessage()); - } - - // viewport - try { - java.nio.IntBuffer vbuf = stack.mallocInt(4); - GL11.glGetIntegerv(GL11.GL_VIEWPORT, vbuf); - int vx = vbuf.get(0), vy = vbuf.get(1), vw = vbuf.get(2), vh = vbuf.get(3); - // 做基本合法性检查:宽高 > 0 - if (vw > 0 && vh > 0) { - this.viewport = new int[]{vx, vy, vw, vh}; - } - } catch (Throwable ex) { - logger.debug("Could not read viewport: {}", ex.getMessage()); - } - } // MemoryStack 自动释放 - - // 再次清理任何遗留 GL error,确保 pushState 后渲染不被污染 - while (GL11.glGetError() != GL11.GL_NO_ERROR) { /* clear */ } - } - - // 新增:fallback 构造器,仅创建默认值(在捕获异常时使用) - public RenderState(boolean fallbackDefaults) { - setDefaults(); - } - - private void setDefaults() { - this.currentProgram = 0; - this.blendEnabled = false; - this.depthTestEnabled = false; - this.blendSrcFactor = GL11.GL_SRC_ALPHA; - this.blendDstFactor = GL11.GL_ONE_MINUS_SRC_ALPHA; - this.activeTexture = GL13.GL_TEXTURE0; - this.boundTexture = 0; - this.clearColor = new float[]{0.0f, 0.0f, 0.0f, 1.0f}; - // 尝试从系统获取视口默认:若没有则使用 stored viewportWidth/height(确保为正) - int vw = Math.max(1, viewportWidth); - int vh = Math.max(1, viewportHeight); - this.viewport = new int[]{0, 0, vw, vh}; - } - - public void restore() { - try { - // 恢复视口(做范围与合法性校验) - if (viewport != null && viewport.length == 4) { - int w = Math.max(1, Math.min(viewport[2], 65535)); - int h = Math.max(1, Math.min(viewport[3], 65535)); - GL11.glViewport(viewport[0], viewport[1], w, h); - } - - // 恢复清除颜色 - if (clearColor != null && clearColor.length == 4) { - GL11.glClearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); - } - - // 恢复着色器程序 - try { - if (GL20.glIsProgram(currentProgram)) { - GL20.glUseProgram(currentProgram); - } else { - GL20.glUseProgram(0); - } - } catch (Throwable ex) { - GL20.glUseProgram(0); - } - - // 恢复纹理状态 - 检查 activeTexture 合法范围 - if (activeTexture >= GL13.GL_TEXTURE0 && activeTexture <= GL13.GL_TEXTURE31) { - GL13.glActiveTexture(activeTexture); - } else { - GL13.glActiveTexture(GL13.GL_TEXTURE0); - } - GL11.glBindTexture(GL11.GL_TEXTURE_2D, Math.max(0, boundTexture)); - - // 恢复混合 - if (blendEnabled) GL11.glEnable(GL11.GL_BLEND); else GL11.glDisable(GL11.GL_BLEND); - - if (isValidBlendFunc(blendSrcFactor) && isValidBlendFunc(blendDstFactor)) { - GL11.glBlendFunc(blendSrcFactor, blendDstFactor); - } else { - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - } - - // 恢复深度测试 - if (depthTestEnabled) GL11.glEnable(GL11.GL_DEPTH_TEST); else GL11.glDisable(GL11.GL_DEPTH_TEST); - - } catch (Throwable e) { - logger.error("Error during state restoration: {}", e.getMessage()); - } finally { - // 清理可能的 GL error - while (GL11.glGetError() != GL11.GL_NO_ERROR) { /* clear */ } - } - } - - private boolean isValidBlendFunc(int func) { - switch (func) { - case GL11.GL_ZERO: - case GL11.GL_ONE: - case GL11.GL_SRC_COLOR: - case GL11.GL_ONE_MINUS_SRC_COLOR: - case GL11.GL_DST_COLOR: - case GL11.GL_ONE_MINUS_DST_COLOR: - case GL11.GL_SRC_ALPHA: - case GL11.GL_ONE_MINUS_SRC_ALPHA: - case GL11.GL_DST_ALPHA: - case GL11.GL_ONE_MINUS_DST_ALPHA: - case GL14.GL_SRC_ALPHA_SATURATE: - return true; - default: - return false; - } - } - - @Override - public String toString() { - return "RenderState{" + - "currentProgram=" + currentProgram + - ", blendEnabled=" + blendEnabled + - ", depthTestEnabled=" + depthTestEnabled + - ", blendSrcFactor=" + blendSrcFactor + - ", blendDstFactor=" + blendDstFactor + - ", activeTexture=" + activeTexture + - ", boundTexture=" + boundTexture + - ", clearColor=" + java.util.Arrays.toString(clearColor) + - ", viewport=" + java.util.Arrays.toString(viewport) + - '}'; - } - } - // ================== 初始化方法 ================== - - public static void initRenderThread() { - if (renderThread == null) { - renderThread = Thread.currentThread(); - logger.info("Render thread initialized: {}", Thread.currentThread().getName()); - } else if (renderThread != Thread.currentThread()) { - throw new IllegalStateException("Render thread already initialized by another thread"); - } - } - - public static void beginInitialization() { - isInInit = true; - } - - public static void finishInitialization() { - isInInit = false; - if (!renderQueue.isEmpty()) { - replayQueue(); - } - } - - // ================== 线程断言 ================== - - public static boolean isOnRenderThread() { - return Thread.currentThread() == renderThread; - } - - public static boolean isInInitPhase() { - return isInInit; - } - - public static void assertOnRenderThread() { - if (!isOnRenderThread()) { - throw new IllegalStateException("RenderSystem called from wrong thread: " + - Thread.currentThread().getName() + ", expected: " + - (renderThread != null ? renderThread.getName() : "render thread")); - } - } - - public static void assertOnRenderThreadOrInit() { - if (!isInInit && !isOnRenderThread()) { - throw new IllegalStateException("RenderSystem called from wrong thread"); - } - } - - // ================== 渲染命令队列 ================== - - public static void recordRenderCall(Runnable renderCall) { - renderQueue.add(renderCall); - } - - public static void replayQueue() { - isReplayingQueue = true; - while (!renderQueue.isEmpty()) { - Runnable call = renderQueue.poll(); - if (call != null) { - call.run(); - } - } - isReplayingQueue = false; - } - - // ================== OpenGL 状态管理封装 ================== - - public static void enable(int capability) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _enable(capability)); - } else { - _enable(capability); - } - } - - /** - * 保存当前渲染状态到栈中 - */ - public static void pushState() { - if (!isOnRenderThread()) { - recordRenderCall(() -> _pushState()); - } else { - _pushState(); - } - } - - private static void _pushState() { - assertOnRenderThread(); - checkGLError("started pushState"); - try { - // 尝试构造 RenderState(内部会尽量安全地查询 GL) - stateStack.push(new RenderState()); - } catch (Exception e) { - // 严格容错:如果构造失败,记录并 push 一个默认状态,保证栈平衡与渲染继续 - logger.warn("Failed to push full render state, pushing fallback default state: {}", e.getMessage()); - stateStack.push(new RenderState(true)); // 调用下面新增的 fallback 构造器 - } - - // 检查并记录任何产生的 GL 错误(非致命) - checkGLError("end pushState"); - } - - /** - * 从栈中恢复之前的渲染状态 - */ - public static void popState() { - if (!isOnRenderThread()) { - recordRenderCall(() -> _popState()); - } else { - _popState(); - } - } - - private static void _popState() { - assertOnRenderThread(); - if (!stateStack.isEmpty()) { - RenderState state = stateStack.pop(); - state.restore(); - checkGLError("popState"); - } else { - logger.warn("popState called with empty state stack"); - } - } - - /** - * 获取当前状态栈大小 - */ - public static int getStateStackSize() { - return stateStack.size(); - } - - private static void _enable(int capability) { - assertOnRenderThread(); - GL11.glEnable(capability); - } - - public static void disable(int capability) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _disable(capability)); - } else { - _disable(capability); - } - } - - private static void _disable(int capability) { - assertOnRenderThread(); - GL11.glDisable(capability); - } - - public static void clearColor(float r, float g, float b, float a) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _clearColor(r, g, b, a)); - } else { - _clearColor(r, g, b, a); - } - } - - public static void setClearColor(float r, float g, float b, float a) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _setClearColor(r, g, b, a)); - } else { - _setClearColor(r, g, b, a); - } - } - - private static void _setClearColor(float r, float g, float b, float a) { - assertOnRenderThread(); - GL11.glClearColor(r, g, b, a); - // 可选:更新内部状态记录 - clearColor[0] = r; - clearColor[1] = g; - clearColor[2] = b; - clearColor[3] = a; - } - - private static void _clearColor(float r, float g, float b, float a) { - assertOnRenderThread(); - GL11.glClearColor(r, g, b, a); - clearColor[0] = r; - clearColor[1] = g; - clearColor[2] = b; - clearColor[3] = a; - } - - public static void clear(int mask) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _clear(mask)); - } else { - _clear(mask); - } - } - - private static void _clear(int mask) { - assertOnRenderThread(); - GL11.glClear(mask); - } - - public static void viewport(int x, int y, int width, int height) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _viewport(x, y, width, height)); - } else { - _viewport(x, y, width, height); - } - } - - private static void _viewport(int x, int y, int width, int height) { - assertOnRenderThread(); - GL11.glViewport(x, y, width, height); - viewportWidth = width; - viewportHeight = height; - } - - // ================== VAO 操作 ================== - - public static int glGenVertexArrays() { - if (!isOnRenderThread()) { - throw new IllegalStateException("VAO generation must be on render thread"); - } - return GL30.glGenVertexArrays(); - } - - public static void glDeleteVertexArrays(int vao) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _glDeleteVertexArrays(vao)); - } else { - _glDeleteVertexArrays(vao); - } - } - - private static void _glDeleteVertexArrays(int vao) { - assertOnRenderThread(); - GL30.glDeleteVertexArrays(vao); - } - - // ================== Uniform 设置 ================== - - public static void uniform1i(int location, int value) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _uniform1i(location, value)); - } else { - _uniform1i(location, value); - } - } - - private static void _uniform1i(int location, int value) { - assertOnRenderThread(); - GL20.glUniform1i(location, value); - } - - public static void uniform1f(int location, float value) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _uniform1f(location, value)); - } else { - _uniform1f(location, value); - } - } - - private static void _uniform1f(int location, float value) { - assertOnRenderThread(); - GL20.glUniform1f(location, value); - } - - public static void uniform2f(int location, float x, float y) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _uniform2f(location, x, y)); - } else { - _uniform2f(location, x, y); - } - } - - private static void _uniform2f(int location, float x, float y) { - assertOnRenderThread(); - GL20.glUniform2f(location, x, y); - } - - public static void uniform3f(int location, float x, float y, float z) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _uniform3f(location, x, y, z)); - } else { - _uniform3f(location, x, y, z); - } - } - - private static void _uniform3f(int location, float x, float y, float z) { - assertOnRenderThread(); - GL20.glUniform3f(location, x, y, z); - } - - public static void uniform4f(int location, float x, float y, float z, float w) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _uniform4f(location, x, y, z, w)); - } else { - _uniform4f(location, x, y, z, w); - } - } - - private static void _uniform4f(int location, float x, float y, float z, float w) { - assertOnRenderThread(); - GL20.glUniform4f(location, x, y, z, w); - } - - public static void uniformMatrix3(int location, boolean transpose, java.nio.FloatBuffer matrix) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _uniformMatrix3(location, transpose, matrix)); - } else { - _uniformMatrix3(location, transpose, matrix); - } - } - - private static void _uniformMatrix3(int location, boolean transpose, java.nio.FloatBuffer matrix) { - assertOnRenderThread(); - GL20.glUniformMatrix3fv(location, transpose, matrix); - } - - public static void uniformMatrix4(int location, boolean transpose, java.nio.FloatBuffer matrix) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _uniformMatrix4(location, transpose, matrix)); - } else { - _uniformMatrix4(location, transpose, matrix); - } - } - - private static void _uniformMatrix4(int location, boolean transpose, java.nio.FloatBuffer matrix) { - assertOnRenderThread(); - GL20.glUniformMatrix4fv(location, transpose, matrix); - } - - // 便捷方法:Vector2f - public static void uniform2f(int location, org.joml.Vector2f vec) { - uniform2f(location, vec.x, vec.y); - } - - // 便捷方法:Vector3f - public static void uniform3f(int location, org.joml.Vector3f vec) { - uniform3f(location, vec.x, vec.y, vec.z); - } - - // 便捷方法:Vector4f - public static void uniform4f(int location, org.joml.Vector4f vec) { - uniform4f(location, vec.x, vec.y, vec.z, vec.w); - } - - // 便捷方法:Matrix3f - public static void uniformMatrix3(int location, org.joml.Matrix3f matrix) { - java.nio.FloatBuffer fb = org.lwjgl.system.MemoryUtil.memAllocFloat(9); - try { - matrix.get(fb); - uniformMatrix3(location, false, fb); - } finally { - org.lwjgl.system.MemoryUtil.memFree(fb); - } - } - - // 便捷方法:Matrix4f - public static void uniformMatrix4(int location, org.joml.Matrix4f matrix) { - java.nio.FloatBuffer fb = org.lwjgl.system.MemoryUtil.memAllocFloat(16); - try { - matrix.get(fb); - uniformMatrix4(location, false, fb); - } finally { - org.lwjgl.system.MemoryUtil.memFree(fb); - } - } - - // Uniform 位置查询 - public static int getUniformLocation(int program, String name) { - if (!isOnRenderThread()) { - throw new IllegalStateException("Uniform location queries must be on render thread"); - } - return GL20.glGetUniformLocation(program, name); - } - - // ================== 绘制命令 ================== - - public static void lineWidth(float width) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _lineWidth(width)); - } else { - _lineWidth(width); - } - } - - private static void _lineWidth(float width) { - assertOnRenderThread(); - GL11.glLineWidth(width); - } - - public static void drawElements(int mode, int count, int type, long indices) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _drawElements(mode, count, type, indices)); - } else { - _drawElements(mode, count, type, indices); - } - } - - private static void _drawElements(int mode, int count, int type, long indices) { - assertOnRenderThread(); - GL11.glDrawElements(mode, count, type, indices); - } - - public static void drawArrays(int mode, int first, int count) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _drawArrays(mode, first, count)); - } else { - _drawArrays(mode, first, count); - } - } - - private static void _drawArrays(int mode, int first, int count) { - assertOnRenderThread(); - GL11.glDrawArrays(mode, first, count); - } - - // VAO 绑定支持 - public static void glBindVertexArray(Supplier vaoSupplier) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _glBindVertexArray(vaoSupplier.get())); - } else { - _glBindVertexArray(vaoSupplier.get()); - } - } - - private static void _glBindVertexArray(int vao) { - assertOnRenderThread(); - GL30.glBindVertexArray(vao); - } - - // ================== 混合模式 ================== - - public static void enableBlend() { - if (!isOnRenderThread()) { - recordRenderCall(RenderSystem::_enableBlend); - } else { - _enableBlend(); - } - } - - private static void _enableBlend() { - assertOnRenderThread(); - GL11.glEnable(GL11.GL_BLEND); - } - - public static void disableBlend() { - if (!isOnRenderThread()) { - recordRenderCall(RenderSystem::_disableBlend); - } else { - _disableBlend(); - } - } - - private static void _disableBlend() { - assertOnRenderThread(); - GL11.glDisable(GL11.GL_BLEND); - } - - public static void blendFunc(int sfactor, int dfactor) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _blendFunc(sfactor, dfactor)); - } else { - _blendFunc(sfactor, dfactor); - } - } - - private static void _blendFunc(int sfactor, int dfactor) { - assertOnRenderThread(); - GL11.glBlendFunc(sfactor, dfactor); - } - - public static void defaultBlendFunc() { - blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - } - - // ================== 着色器程序管理 ================== - - public static int createProgram() { - if (!isOnRenderThread()) { - throw new IllegalStateException("Program creation must be on render thread"); - } - return GL20.glCreateProgram(); - } - - /** - * 获取当前激活的着色器程序ID - */ - public static int getCurrentProgram() { - if (!isOnRenderThread()) { - throw new IllegalStateException("getCurrentProgram must be called on render thread"); - } - return GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM); - } - - public static void attachShader(int program, int shader) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _attachShader(program, shader)); - } else { - _attachShader(program, shader); - } - } - - private static void _attachShader(int program, int shader) { - assertOnRenderThread(); - GL20.glAttachShader(program, shader); - } - - public static void linkProgram(int program) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _linkProgram(program)); - } else { - _linkProgram(program); - } - } - - private static void _linkProgram(int program) { - assertOnRenderThread(); - GL20.glLinkProgram(program); - } - - public static int getProgram(int program, int pname) { - if (!isOnRenderThread()) { - throw new IllegalStateException("Program queries must be on render thread"); - } - return GL20.glGetProgrami(program, pname); - } - - public static String getProgramInfoLog(int program) { - if (!isOnRenderThread()) { - throw new IllegalStateException("Program queries must be on render thread"); - } - return GL20.glGetProgramInfoLog(program); - } - - public static void detachShader(int program, int shader) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _detachShader(program, shader)); - } else { - _detachShader(program, shader); - } - } - - private static void _detachShader(int program, int shader) { - assertOnRenderThread(); - GL20.glDetachShader(program, shader); - } - - public static void deleteProgram(int program) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _deleteProgram(program)); - } else { - _deleteProgram(program); - } - } - - private static void _deleteProgram(int program) { - assertOnRenderThread(); - if (GL20.glIsProgram(program)) { - GL20.glDeleteProgram(program); - } - } - - public static ByteBuffer loadFont(String fontFileName) throws IOException { - List possiblePaths = getFontPaths(fontFileName); - - for (Path fontPath : possiblePaths) { - if (Files.exists(fontPath)) { - try (FileChannel fc = FileChannel.open(fontPath, StandardOpenOption.READ)) { - ByteBuffer buffer = ByteBuffer.allocateDirect((int) fc.size()); - while (buffer.hasRemaining()) { - fc.read(buffer); - } - buffer.flip(); - return buffer; - } - } - } - - logger.warn("无法加载字体: {}", fontFileName); - return null; - } - - private static List getFontPaths(String fontFileName) { - String os = System.getProperty("os.name").toLowerCase(); - List paths = new ArrayList<>(); - - if (os.contains("win")) { - paths.add(Path.of("C:/Windows/Fonts/" + fontFileName)); - for (char drive = 'C'; drive <= 'E'; drive++) { - paths.add(Path.of(drive + ":/Windows/Fonts/" + fontFileName)); - } - } else if (os.contains("mac")) { - paths.add(Path.of(System.getProperty("user.home"), "Library", "Fonts", fontFileName)); - paths.add(Path.of("/Library", "Fonts", fontFileName)); - paths.add(Path.of("/System", "Library", "Fonts", fontFileName)); - } else if (os.contains("nix") || os.contains("nux") || os.contains("aix")) { - paths.add(Path.of("/usr/share/fonts", fontFileName)); - paths.add(Path.of("/usr/local/share/fonts", fontFileName)); - paths.add(Path.of(System.getProperty("user.home"), ".local/share/fonts", fontFileName)); - paths.add(Path.of(System.getProperty("user.home"), ".fonts", fontFileName)); - paths.add(Path.of("/usr/X11R6/lib/X11/fonts", fontFileName)); - } else { - paths.add(Path.of(System.getProperty("user.home"), "fonts", fontFileName)); - } - - return paths; - } - - public static int linkProgram(int vertexShader, int fragmentShader) { - assertOnRenderThread(); - - int program = createProgram(); - attachShader(program, vertexShader); - attachShader(program, fragmentShader); - linkProgram(program); - - int status = getProgram(program, GL20.GL_LINK_STATUS); - if (status == GL11.GL_FALSE) { - String log = getProgramInfoLog(program); - deleteProgram(program); - throw new RuntimeException("Program link failed: " + log); - } - - // 着色器可以在链接后删除 - detachShader(program, vertexShader); - detachShader(program, fragmentShader); - deleteShader(vertexShader); - deleteShader(fragmentShader); - - return program; - } - - // 支持几何着色器的链接方法 - public static int linkProgram(int vertexShader, int geometryShader, int fragmentShader) { - assertOnRenderThread(); - - int program = createProgram(); - attachShader(program, vertexShader); - if (geometryShader != 0) { - attachShader(program, geometryShader); - } - attachShader(program, fragmentShader); - linkProgram(program); - - int status = getProgram(program, GL20.GL_LINK_STATUS); - if (status == GL11.GL_FALSE) { - String log = getProgramInfoLog(program); - deleteProgram(program); - throw new RuntimeException("Program link failed: " + log); - } - - // 清理着色器 - detachShader(program, vertexShader); - if (geometryShader != 0) { - detachShader(program, geometryShader); - } - detachShader(program, fragmentShader); - deleteShader(vertexShader); - if (geometryShader != 0) { - deleteShader(geometryShader); - } - deleteShader(fragmentShader); - - return program; - } - - // ================== 着色器管理 ================== - - public static int createShader(int type) { - if (!isOnRenderThread()) { - throw new IllegalStateException("Shader creation must be on render thread"); - } - return GL20.glCreateShader(type); - } - - public static void shaderSource(int shader, String source) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _shaderSource(shader, source)); - } else { - _shaderSource(shader, source); - } - } - - private static void _shaderSource(int shader, String source) { - assertOnRenderThread(); - GL20.glShaderSource(shader, source); - } - - public static void compileShader(int shader) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _compileShader(shader)); - } else { - _compileShader(shader); - } - } - - private static void _compileShader(int shader) { - assertOnRenderThread(); - GL20.glCompileShader(shader); - } - - public static int getShaderi(int shader, int pname) { - if (!isOnRenderThread()) { - throw new IllegalStateException("Shader queries must be on render thread"); - } - return GL20.glGetShaderi(shader, pname); - } - - public static String getShaderInfoLog(int shader) { - if (!isOnRenderThread()) { - throw new IllegalStateException("Shader queries must be on render thread"); - } - return GL20.glGetShaderInfoLog(shader); - } - - public static void deleteShader(int shader) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _deleteShader(shader)); - } else { - _deleteShader(shader); - } - } - - private static void _deleteShader(int shader) { - assertOnRenderThread(); - GL20.glDeleteShader(shader); - } - - public static int compileShader(int type, String source) { - assertOnRenderThread(); - - int shader = createShader(type); - shaderSource(shader, source); - compileShader(shader); - - int status = getShaderi(shader, GL20.GL_COMPILE_STATUS); - if (status == GL11.GL_FALSE) { - String log = getShaderInfoLog(shader); - deleteShader(shader); - throw new RuntimeException("Shader compilation failed: " + log); - } - return shader; - } - - // ================== 深度测试 ================== - - public static void depthFunc(int func) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _depthFunc(func)); - } else { - _depthFunc(func); - } - } - - private static void _depthFunc(int func) { - assertOnRenderThread(); - GL11.glDepthFunc(func); - } - - public static void depthMask(boolean flag) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _depthMask(flag)); - } else { - _depthMask(flag); - } - } - - private static void _depthMask(boolean flag) { - assertOnRenderThread(); - GL11.glDepthMask(flag); - } - - public static void clearDepth(double depth) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _clearDepth(depth)); - } else { - _clearDepth(depth); - } - } - - private static void _clearDepth(double depth) { - assertOnRenderThread(); - GL11.glClearDepth(depth); - } - - public static void enableDepthTest() { - if (!isOnRenderThread()) { - recordRenderCall(() -> _enableDepthTest()); - } else { - _enableDepthTest(); - } - } - - private static void _enableDepthTest() { - assertOnRenderThread(); - GL11.glEnable(GL11.GL_DEPTH_TEST); - } - - public static void disableDepthTest() { - if (!isOnRenderThread()) { - recordRenderCall(() -> _disableDepthTest()); - } else { - _disableDepthTest(); - } - } - - private static void _disableDepthTest() { - assertOnRenderThread(); - GL11.glDisable(GL11.GL_DEPTH_TEST); - } - - // ================== 纹理管理 ================== - - public static void bindTexture(int texture) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _bindTexture(texture)); - } else { - _bindTexture(texture); - } - } - - public static void getTexImage(int target, int level, int format, int type, java.nio.ByteBuffer pixels) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _getTexImage(target, level, format, type, pixels)); - } else { - _getTexImage(target, level, format, type, pixels); - } - } - - private static void _getTexImage(int target, int level, int format, int type, java.nio.ByteBuffer pixels) { - assertOnRenderThread(); - GL11.glGetTexImage(target, level, format, type, pixels); - } - - private static void _bindTexture(int texture) { - assertOnRenderThread(); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture); - } - - public static void activeTexture(int texture) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _activeTexture(texture)); - } else { - _activeTexture(texture); - } - } - - private static void _activeTexture(int texture) { - assertOnRenderThread(); - GL13.glActiveTexture(texture); - } - - public static int genTextures() { - if (!isOnRenderThread()) { - throw new IllegalStateException("Texture generation must be on render thread"); - } - return GL11.glGenTextures(); - } - - public static void deleteTextures(int texture) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _deleteTextures(texture)); - } else { - _deleteTextures(texture); - } - } - - private static void _deleteTextures(int texture) { - assertOnRenderThread(); - GL11.glDeleteTextures(texture); - } - - public static void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, java.nio.ByteBuffer pixels) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _texImage2D(target, level, internalFormat, width, height, border, format, type, pixels)); - } else { - _texImage2D(target, level, internalFormat, width, height, border, format, type, pixels); - } - } - - private static void _texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, java.nio.ByteBuffer pixels) { - assertOnRenderThread(); - GL11.glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixels); - } - - public static void texParameteri(int target, int pname, int param) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _texParameteri(target, pname, param)); - } else { - _texParameteri(target, pname, param); - } - } - - private static void _texParameteri(int target, int pname, int param) { - assertOnRenderThread(); - GL11.glTexParameteri(target, pname, param); - } - - public static void setTextureMinFilter(int filter) { - texParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filter); - } - - public static void setTextureMagFilter(int filter) { - texParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, filter); - } - - public static void setTextureWrapS(int wrap) { - texParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, wrap); - } - - public static void setTextureWrapT(int wrap) { - texParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, wrap); - } - - // 创建默认白色纹理的便捷方法 - public static int createDefaultTexture() { - assertOnRenderThread(); - - int textureId = genTextures(); - if (textureId == 0) { - logger.error("Failed to generate texture ID"); - return 0; - } - - bindTexture(textureId); - - try { - // 创建 1x1 白色纹理 - 使用更兼容的格式 - java.nio.ByteBuffer buffer = org.lwjgl.system.MemoryUtil.memAlloc(4); - try { - // 填充 RGBA 数据:白色不透明 - buffer.put((byte) 0xFF) // R - .put((byte) 0xFF) // G - .put((byte) 0xFF) // B - .put((byte) 0xFF) // A - .flip(); - - // 使用更兼容的纹理格式组合 - // 注意:有些系统可能不支持 GL_RGBA8,使用 GL_RGBA 作为内部格式 - texImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, - 1, 1, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer); - - checkGLError("texImage2D in createDefaultTexture"); - - } finally { - org.lwjgl.system.MemoryUtil.memFree(buffer); - } - - // 设置纹理参数 - setTextureMinFilter(GL11.GL_NEAREST); - setTextureMagFilter(GL11.GL_NEAREST); - setTextureWrapS(GL12.GL_CLAMP_TO_EDGE); - setTextureWrapT(GL12.GL_CLAMP_TO_EDGE); - - checkGLError("texture parameters in createDefaultTexture"); - - } catch (Exception e) { - logger.error("Error creating default texture: {}", e.getMessage()); - // 清理失败的纹理 - deleteTextures(textureId); - return 0; - } finally { - bindTexture(0); // 解绑 - } - - return textureId; - } - - // ================== 纹理管理扩展 ================== - - /** - * 设置着色器纹理 - * - * @param textureUnit 纹理单元 (0, 1, 2, ...) - * @param texture 纹理对象 - */ - public static void setShaderTexture(int textureUnit, com.chuangzhou.vivid2D.render.model.util.Texture texture) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _setShaderTexture(textureUnit, texture)); - } else { - _setShaderTexture(textureUnit, texture); - } - } - - private static void _setShaderTexture(int textureUnit, com.chuangzhou.vivid2D.render.model.util.Texture texture) { - assertOnRenderThread(); - - if (texture == null) { - logger.warn("setShaderTexture: texture is null for unit {}", textureUnit); - return; - } - - if (texture.isDisposed()) { - logger.warn("setShaderTexture: texture is disposed for unit {}", textureUnit); - return; - } - - try { - // 激活纹理单元 - activeTexture(GL13.GL_TEXTURE0 + textureUnit); - - // 绑定纹理 - bindTexture(texture.getTextureId()); - - // 检查错误 - checkGLError("setShaderTexture"); - - } catch (Exception e) { - logger.error("setShaderTexture failed for unit {}: {}", textureUnit, e.getMessage()); - } - } - - /** - * 设置着色器纹理 - 接受纹理ID的版本 - * - * @param textureUnit 纹理单元 - * @param textureId 纹理ID - */ - public static void setShaderTexture(int textureUnit, int textureId) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _setShaderTexture(textureUnit, textureId)); - } else { - _setShaderTexture(textureUnit, textureId); - } - } - - private static void _setShaderTexture(int textureUnit, int textureId) { - assertOnRenderThread(); - - if (textureId == 0) { - logger.warn("setShaderTexture: textureId is 0 for unit {}", textureUnit); - return; - } - - try { - // 激活纹理单元 - activeTexture(GL13.GL_TEXTURE0 + textureUnit); - - // 绑定纹理 - bindTexture(textureId); - - // 检查错误 - checkGLError("setShaderTexture"); - - } catch (Exception e) { - logger.error("setShaderTexture failed for unit {}: {}", textureUnit, e.getMessage()); - } - } - - // ================== 着色器程序 ================== - - public static void useProgram(int program) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _useProgram(program)); - } else { - _useProgram(program); - } - } - - private static void _useProgram(int program) { - assertOnRenderThread(); - GL20.glUseProgram(program); - } - - // ================== 缓冲区操作 ================== - - public static int glGenBuffers() { - if (!isOnRenderThread()) { - throw new IllegalStateException("Buffer generation must be on render thread"); - } - return GL15.glGenBuffers(); - } - - public static void glBindBuffer(int target, IntSupplier buffer) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _glBindBuffer(target, buffer.getAsInt())); - } else { - _glBindBuffer(target, buffer.getAsInt()); - } - } - - private static void _glBindBuffer(int target, int buffer) { - assertOnRenderThread(); - GL15.glBindBuffer(target, buffer); - } - - public static void glBufferData(int target, java.nio.IntBuffer data, int usage) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _glBufferData(target, data, usage)); - } else { - _glBufferData(target, data, usage); - } - } - - private static void _glBufferData(int target, java.nio.IntBuffer data, int usage) { - assertOnRenderThread(); - GL15.glBufferData(target, data, usage); - } - - public static void glBufferData(int target, java.nio.FloatBuffer data, int usage) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _glBufferData(target, data, usage)); - } else { - _glBufferData(target, data, usage); - } - } - - private static void _glBufferData(int target, java.nio.FloatBuffer data, int usage) { - assertOnRenderThread(); - GL15.glBufferData(target, data, usage); - } - - public static void glDeleteBuffers(int buffer) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _glDeleteBuffers(buffer)); - } else { - _glDeleteBuffers(buffer); - } - } - - private static void _glDeleteBuffers(int buffer) { - assertOnRenderThread(); - GL15.glDeleteBuffers(buffer); - } - -// ================== 顶点属性 ================== - - public static void enableVertexAttribArray(int index) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _enableVertexAttribArray(index)); - } else { - _enableVertexAttribArray(index); - } - } - - private static void _enableVertexAttribArray(int index) { - assertOnRenderThread(); - GL20.glEnableVertexAttribArray(index); - } - - public static void disableVertexAttribArray(int index) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _disableVertexAttribArray(index)); - } else { - _disableVertexAttribArray(index); - } - } - - private static void _disableVertexAttribArray(int index) { - assertOnRenderThread(); - GL20.glDisableVertexAttribArray(index); - } - - public static void vertexAttribPointer(int index, int size, int type, boolean normalized, int stride, long pointer) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _vertexAttribPointer(index, size, type, normalized, stride, pointer)); - } else { - _vertexAttribPointer(index, size, type, normalized, stride, pointer); - } - } - - private static void _vertexAttribPointer(int index, int size, int type, boolean normalized, int stride, long pointer) { - assertOnRenderThread(); - GL20.glVertexAttribPointer(index, size, type, normalized, stride, pointer); - } - - // ================== 工具方法 ================== - - public static void readPixels(int x, int y, int width, int height, int format, int type, java.nio.ByteBuffer pixels) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _readPixels(x, y, width, height, format, type, pixels)); - } else { - _readPixels(x, y, width, height, format, type, pixels); - } - } - - public static void readPixels(int x, int y, int width, int height, int format, int type, int pixels) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _readPixels(x, y, width, height, format, type, pixels)); - } else { - _readPixels(x, y, width, height, format, type, pixels); - } - } - - private static void _readPixels(int x, int y, int width, int height, int format, int type, java.nio.ByteBuffer pixels) { - assertOnRenderThread(); - GL11.glReadPixels(x, y, width, height, format, type, pixels); - } - - private static void _readPixels(int x, int y, int width, int height, int format, int type, int pixels) { - assertOnRenderThread(); - GL11.glReadPixels(x, y, width, height, format, type, pixels); - } - - /** - * 检查特定扩展是否支持 - */ - public static boolean isExtensionSupported(String extension) { - assertOnRenderThread(); - - String extensionsString = GL11.glGetString(GL11.GL_EXTENSIONS); - if (extensionsString == null) { - return false; - } - - String[] extensions = extensionsString.split("\\s+"); - for (String ext : extensions) { - if (ext.equals(extension)) { - return true; - } - } - - return false; - } - - public static void pixelStore(int pname, int param) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _pixelStore(pname, param)); - } else { - _pixelStore(pname, param); - } - } - - private static void _pixelStore(int pname, int param) { - assertOnRenderThread(); - GL11.glPixelStorei(pname, param); - } - - - /** - * 获取 OpenGL 厂商信息 - */ - public static String getVendor() { - if (!isOnRenderThread()) { - throw new IllegalStateException("OpenGL info queries must be on render thread"); - } - return GL11.glGetString(GL11.GL_VENDOR); - } - - /** - * 获取 OpenGL 渲染器信息 - */ - public static String getRenderer() { - if (!isOnRenderThread()) { - throw new IllegalStateException("OpenGL info queries must be on render thread"); - } - return GL11.glGetString(GL11.GL_RENDERER); - } - - /** - * 获取 OpenGL 版本信息 - */ - public static String getOpenGLVersion() { - if (!isOnRenderThread()) { - throw new IllegalStateException("OpenGL info queries must be on render thread"); - } - return GL11.glGetString(GL11.GL_VERSION); - } - - /** - * 获取 GLSL 版本信息 - */ - public static String getGLSLVersion() { - if (!isOnRenderThread()) { - throw new IllegalStateException("OpenGL info queries must be on render thread"); - } - return GL20.glGetString(GL20.GL_SHADING_LANGUAGE_VERSION); - } - - /** - * 记录详细的 OpenGL 信息 - */ - public static void logDetailedGLInfo() { - assertOnRenderThread(); - - Logger logger = LoggerFactory.getLogger(RenderSystem.class); - - logger.info("=== OpenGL System Information ==="); - logger.info("Vendor: {}", getVendor()); - logger.info("Renderer: {}", getRenderer()); - logger.info("OpenGL Version: {}", getOpenGLVersion()); - logger.info("GLSL Version: {}", getGLSLVersion()); - - // 添加扩展数量信息 - int numExtensions = GL11.glGetInteger(GL30.GL_NUM_EXTENSIONS); - logger.info("Number of Extensions: {}", numExtensions); - - // 添加重要特性支持检查 - logger.info("Max Texture Size: {}x{}", - GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE), - GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE)); - logger.info("Max Vertex Attributes: {}", - GL11.glGetInteger(GL20.GL_MAX_VERTEX_ATTRIBS)); - logger.info("Max Texture Units: {}", - GL11.glGetInteger(GL20.GL_MAX_TEXTURE_IMAGE_UNITS)); - } - - public static void setupDefaultState() { - beginInitialization(); - - // 设置默认 OpenGL 状态 - clearColor(0.0f, 0.0f, 0.0f, 1.0f); - viewport(0, 0, viewportWidth, viewportHeight); - enableBlend(); - defaultBlendFunc(); - disableDepthTest(); - - finishInitialization(); - } - - public static void checkGLError(String operation) { - if (!isOnRenderThread()) { - recordRenderCall(() -> _checkGLError(operation)); - } else { - _checkGLError(operation); - } - } - - private static void _checkGLError(String operation) { - assertOnRenderThread(); - int error = GL11.glGetError(); - if (error != GL11.GL_NO_ERROR) { - Exception stackTraceException = new Exception("OpenGL error stack trace"); - StackTraceElement[] stackTrace = stackTraceException.getStackTrace(); - - StringBuilder stackTraceBuilder = new StringBuilder(); - stackTraceBuilder.append("Call stack:\n"); - - for (int i = 2; i < Math.min(stackTrace.length, 10); i++) { - StackTraceElement element = stackTrace[i]; - stackTraceBuilder.append(" at ") - .append(element.getClassName()) - .append(".") - .append(element.getMethodName()) - .append("(") - .append(element.getFileName()) - .append(":") - .append(element.getLineNumber()) - .append(")\n"); - } - - logger.error("OpenGL error during {}: {}\n{}", - operation, getGLErrorString(error), stackTraceBuilder); - } - } - - private static String getGLErrorString(int error) { - switch (error) { - case GL11.GL_INVALID_ENUM: - return "GL_INVALID_ENUM"; - case GL11.GL_INVALID_VALUE: - return "GL_INVALID_VALUE"; - case GL11.GL_INVALID_OPERATION: - return "GL_INVALID_OPERATION"; - case GL11.GL_OUT_OF_MEMORY: - return "GL_OUT_OF_MEMORY"; - default: - return "Unknown (0x" + Integer.toHexString(error) + ")"; - } - } - - // ================== 获取状态 ================== - - public static int getViewportWidth() { - return viewportWidth; - } - - public static int getViewportHeight() { - return viewportHeight; - } - - public static float[] getClearColor() { - return clearColor.clone(); - } - - public static int getQueueSize() { - return renderQueue.size(); - } - - - /** - * 顶点格式模式枚举 - */ - public enum VertexFormat { - POINTS(DRAW_POINTS), - LINES(DRAW_LINES), - LINE_LOOP(DRAW_LINE_LOOP), - LINE_STRIP(DRAW_LINE_STRIP), - TRIANGLES(DRAW_TRIANGLES), - TRIANGLE_STRIP(DRAW_TRIANGLE_STRIP), - TRIANGLE_FAN(DRAW_TRIANGLE_FAN), - QUADS(DRAW_QUADS); - - private final int glMode; - - VertexFormat(int glMode) { - this.glMode = glMode; - } - - public int asGLMode() { - return glMode; - } - - public static VertexFormat fromGLMode(int glMode) { - for (VertexFormat format : values()) { - if (format.glMode == glMode) { - return format; - } - } - return TRIANGLES; // 默认回退 - } - } - - /** - * 索引类型枚举 - */ - public enum IndexType { - BYTE(GL_UNSIGNED_BYTE, 1), - SHORT(GL_UNSIGNED_SHORT, 2), - INT(GL_UNSIGNED_INT, 4); - - private final int glType; - private final int bytes; - - IndexType(int glType, int bytes) { - this.glType = glType; - this.bytes = bytes; - } - - public int asGLType() { - return glType; - } - - public int bytes() { - return bytes; - } - - public static IndexType fromGLType(int glType) { - for (IndexType type : values()) { - if (type.glType == glType) { - return type; - } - } - return SHORT; // 默认回退 - } - } - - /** - * 模式类(用于向后兼容) - */ - public static class Mode { - public static VertexFormat fromGLMode(int glMode) { - return VertexFormat.fromGLMode(glMode); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferBuilder.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferBuilder.java deleted file mode 100644 index c2fb1f5..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferBuilder.java +++ /dev/null @@ -1,284 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.buffer; - -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import org.joml.Vector4f; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL20; -import org.lwjgl.system.MemoryUtil; - -import java.nio.FloatBuffer; - -/** - * 简化版 BufferBuilder,用于按顶点流构建并一次性绘制几何体。 - * 每个顶点格式: float x, float y, float u, float v (共4个 float) - *

- * 用法: - * BufferBuilder bb = new BufferBuilder(); - * bb.begin(GL11.GL_LINE_LOOP, 16); - * bb.vertex(x,y,u,v); - * ... - * bb.end(); // 立即绘制并 cleanup - *

- * 设计原则:简单、可靠、方便把临时多顶点数据提交到 GPU。 - * - * @author tzdwindows - * @version 1.2 - * @since 2025-10-16 - */ -public class BufferBuilder { - private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(BufferBuilder.class); - private static final int COMPONENTS_PER_VERTEX = 4; // x,y,u,v - private float[] array; - private int size; // float 数量 - private int vertexCount; - private int mode; // GL mode - private final RenderState renderState = new RenderState(); - private boolean stateSaved = false; - - // 内置状态,用于构建完成的缓冲区 - public static class BuiltBuffer { - private final int vao; - private final int vbo; - private final int vertexCount; - private final int mode; - private final RenderState renderState; - - public BuiltBuffer(int vao, int vbo, int vertexCount, int mode, RenderState renderState) { - this.vao = vao; - this.vbo = vbo; - this.vertexCount = vertexCount; - this.mode = mode; - this.renderState = renderState; - } - - public RenderState renderState() { - return renderState; - } - - public int vao() { - return vao; - } - - public int vbo() { - return vbo; - } - - public int vertexCount() { - return vertexCount; - } - - public int mode() { - return mode; - } - } - - /** - * 渲染状态容器类 - */ - public static class RenderState { - public int textureId = 0; - public int textureUnit = 0; - public int shaderProgram = 0; - public Vector4f color = new Vector4f(1, 1, 1, 1); - public float opacity = 1.0f; - public int blendMode = 0; - public boolean depthTest = false; - public boolean blending = true; - - // 保存当前状态 - public void saveCurrentState() { - this.textureId = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); - this.shaderProgram = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM); - } - - // 应用保存的状态 - public void applyState() { - if (textureId != 0) { - RenderSystem.bindTexture(textureId); - } - if (shaderProgram != 0) { - RenderSystem.useProgram(shaderProgram); - } - } - - // 复制状态 - public RenderState copy() { - RenderState copy = new RenderState(); - copy.textureId = this.textureId; - copy.textureUnit = this.textureUnit; - copy.shaderProgram = this.shaderProgram; - copy.color = new Vector4f(this.color); - copy.opacity = this.opacity; - copy.blendMode = this.blendMode; - copy.depthTest = this.depthTest; - copy.blending = this.blending; - return copy; - } - } - - - public BufferBuilder() { - this(256); // 默认容量:256 floats -> 64 顶点 - } - - public BufferBuilder(int initialFloatCapacity) { - this.array = new float[Math.max(16, initialFloatCapacity)]; - this.size = 0; - this.vertexCount = 0; - this.mode = RenderSystem.DRAW_TRIANGLES; - } - - private void ensureCapacity(int additionalFloats) { - int need = size + additionalFloats; - if (need > array.length) { - int newCap = array.length; - while (newCap < need) newCap <<= 1; - float[] na = new float[newCap]; - System.arraycopy(array, 0, na, 0, size); - array = na; - } - } - - /** - * 开始构建,传入要绘制的 GL 模式(例如 GL11.GL_LINE_LOOP) - * estimatedVertexCount 可传 0 表示不估计 - */ - public void begin(int glMode, int estimatedVertexCount) { - this.mode = glMode; - this.size = 0; - this.vertexCount = 0; - - // 保存当前渲染状态 - if (!stateSaved) { - renderState.saveCurrentState(); - stateSaved = true; - } - - if (estimatedVertexCount > 0) { - ensureCapacity(estimatedVertexCount * COMPONENTS_PER_VERTEX); - } - } - - /** - * 设置纹理状态 - */ - public void setTexture(int textureId, int textureUnit) { - this.renderState.textureId = textureId; - this.renderState.textureUnit = textureUnit; - } - - /** - * 设置着色器程序 - */ - public void setShader(int programId) { - this.renderState.shaderProgram = programId; - } - - /** - * 设置颜色 - */ - public void setColor(Vector4f color) { - this.renderState.color = new Vector4f(color); - } - - /** - * 设置透明度 - */ - public void setOpacity(float opacity) { - this.renderState.opacity = opacity; - } - - /** - * 添加顶点:x,y,u,v - */ - public void vertex(float x, float y, float u, float v) { - ensureCapacity(COMPONENTS_PER_VERTEX); - array[size++] = x; - array[size++] = y; - array[size++] = u; - array[size++] = v; - vertexCount++; - } - - /** - * 结束构建并返回 BuiltBuffer(不立即绘制) - */ - public BuiltBuffer end() { - if (vertexCount == 0) return null; - - RenderSystem.assertOnRenderThread(); - - // 上传缓冲区数据 - FloatBuffer fb = MemoryUtil.memAllocFloat(size); - fb.put(array, 0, size).flip(); - - // 创建 GPU 资源 - int vao = RenderSystem.glGenVertexArrays(); - int vbo = RenderSystem.glGenBuffers(); - - // 使用 RenderSystem 要求的 Supplier/IntSupplier 风格来绑定(修复类型不匹配编译错误) - RenderSystem.glBindVertexArray(() -> vao); - RenderSystem.glBindBuffer(RenderSystem.GL_ARRAY_BUFFER, () -> vbo); - RenderSystem.glBufferData(RenderSystem.GL_ARRAY_BUFFER, fb, RenderSystem.GL_DYNAMIC_DRAW); - - // 设置顶点属性 - RenderSystem.enableVertexAttribArray(0); - RenderSystem.vertexAttribPointer(0, 2, RenderSystem.GL_FLOAT, false, - COMPONENTS_PER_VERTEX * Float.BYTES, 0); - - RenderSystem.enableVertexAttribArray(1); - RenderSystem.vertexAttribPointer(1, 2, RenderSystem.GL_FLOAT, false, - COMPONENTS_PER_VERTEX * Float.BYTES, 2 * Float.BYTES); - - MemoryUtil.memFree(fb); - - // 解绑 VBO/VAO(同样使用 Supplier/IntSupplier) - RenderSystem.glBindBuffer(RenderSystem.GL_ARRAY_BUFFER, () -> 0); - RenderSystem.glBindVertexArray(() -> 0); - - // 创建状态副本 - RenderState stateCopy = renderState.copy(); - - // 重置状态保存标志 - stateSaved = false; - - return new BuiltBuffer(vao, vbo, vertexCount, mode, stateCopy); - } - - /** - * 立即上传并绘制(传统方式,向后兼容) - */ - public void endImmediate() { - BuiltBuffer buffer = end(); - if (buffer != null) { - BufferUploader.drawWithShader(buffer); - } - } - - /** - * 清理构建器创建的所有 GPU 资源 - * 应该在确定不再需要这些资源时调用 - */ - public static void dispose(BuiltBuffer buffer) { - if (buffer != null) { - RenderSystem.glDeleteVertexArrays(buffer.vao()); - RenderSystem.glDeleteBuffers(buffer.vbo()); - } - } - - /** - * 清除构建器状态以便重用 - */ - public void clear() { - this.size = 0; - this.vertexCount = 0; - } - - public int getVertexCount() { - return vertexCount; - } - - public boolean isEmpty() { - return vertexCount == 0; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferUploader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferUploader.java deleted file mode 100644 index 0df235e..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/BufferUploader.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.buffer; - -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL20; - -/** - * 缓冲区上传器 - * - * @author tzdwindows 7 - * @version 1.1 - 添加颜色支持 - */ -public class BufferUploader { - - public static void drawWithShader(BufferBuilder.BuiltBuffer buffer) { - if (buffer == null) return; - - RenderSystem.assertOnRenderThread(); - - // 保存当前状态 - BufferBuilder.RenderState currentState = new BufferBuilder.RenderState(); - currentState.saveCurrentState(); - - try { - // 应用缓冲区指定的渲染状态 - BufferBuilder.RenderState bufferState = buffer.renderState(); - applyRenderState(bufferState); - - // 绑定 VAO 和绘制 - if (buffer.vao() != 0) { - RenderSystem.glBindVertexArray(() -> buffer.vao()); - } - - if (buffer.vertexCount() > 0) { - RenderSystem.drawArrays(buffer.mode(), 0, buffer.vertexCount()); - } - - if (buffer.vao() != 0) { - RenderSystem.glBindVertexArray(() -> 0); - } - - } finally { - // 恢复之前的状态 - restoreRenderState(currentState); - } - } - - /** - * 应用指定的渲染状态 - */ - private static void applyRenderState(BufferBuilder.RenderState state) { - // 应用纹理 - if (state.textureId != 0) { - RenderSystem.activeTexture(RenderSystem.GL_TEXTURE0 + state.textureUnit); - RenderSystem.bindTexture(state.textureId); - } - - // 应用着色器 - int currentProgram = 0; - if (state.shaderProgram != 0) { - currentProgram = state.shaderProgram; - RenderSystem.useProgram(state.shaderProgram); - } else { - currentProgram = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM); - } - if (currentProgram != 0) { - int colorLoc = RenderSystem.getUniformLocation(currentProgram, "uColor"); - if (colorLoc == -1) { - } else { - RenderSystem.uniform4f(colorLoc, - state.color.x, state.color.y, state.color.z, state.color.w); - } - } else { - System.err.println("DEBUG: No shader program available for color setting"); - } - - // 应用混合模式 - if (state.blending) { - RenderSystem.enableBlend(); - RenderSystem.defaultBlendFunc(); - } else { - RenderSystem.disableBlend(); - } - - // 应用深度测试 - if (state.depthTest) { - RenderSystem.enableDepthTest(); - } else { - RenderSystem.disableDepthTest(); - } - } - - /** - * 恢复之前的渲染状态 - */ - private static void restoreRenderState(BufferBuilder.RenderState previousState) { - previousState.applyState(); - } - - public static void draw(BufferBuilder.BuiltBuffer buffer) { - drawWithShader(buffer); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/Tesselator.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/Tesselator.java deleted file mode 100644 index f0df796..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/buffer/Tesselator.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.buffer; - -import com.chuangzhou.vivid2D.render.systems.RenderSystem; - -/** - * 管理缓存 - * - * @author tzdwindows - * @version 1.0 - */ -public class Tesselator { - private static final int DEFAULT_BUFFER_SIZE = 2097152; - private static final Tesselator INSTANCE = new Tesselator(); - - private final BufferBuilder builder; - - public static Tesselator getInstance() { - RenderSystem.assertOnRenderThreadOrInit(); - return INSTANCE; - } - - public Tesselator(int bufferSize) { - this.builder = new BufferBuilder(bufferSize); - } - - public Tesselator() { - this(DEFAULT_BUFFER_SIZE); - } - - public void end() { - RenderSystem.assertOnRenderThread(); - BufferUploader.drawWithShader(this.builder.end()); - } - - public BufferBuilder getBuilder() { - return this.builder; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/CompleteShader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/CompleteShader.java deleted file mode 100644 index 42cd59c..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/CompleteShader.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.sources; - -/** - * 完整着色器接口 - * 一个完整的着色器程序需要顶点着色器和片段着色器 - * - * @author tzdwindows 7 - */ -public interface CompleteShader { - Shader getVertexShader(); - - Shader getFragmentShader(); - - String getShaderName(); - - boolean isDefaultShader(); - - default void setDefaultUniforms(ShaderProgram program) { - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/Shader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/Shader.java deleted file mode 100644 index 3e2e3a0..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/Shader.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.sources; - -/** - * 着色器接口 - * - * @author tzdwindows 7 - */ -public interface Shader { - String getShaderCode(); - - String getShaderName(); -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderManagement.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderManagement.java deleted file mode 100644 index aef5604..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderManagement.java +++ /dev/null @@ -1,286 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.sources; - -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import com.chuangzhou.vivid2D.render.systems.sources.def.Shader2D; -import com.chuangzhou.vivid2D.render.systems.sources.def.SolidColorShader; -import com.chuangzhou.vivid2D.render.systems.sources.def.TextShader; -import org.joml.Vector3f; -import org.joml.Vector4f; -import org.lwjgl.opengl.GL20; -import org.lwjgl.system.MemoryStack; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.FloatBuffer; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.lwjgl.system.MemoryStack.stackPush; - -/** - * 着色器管理器 - 负责着色器的编译、链接和管理 - * - * @author tzdwindows 7 - * @version 1.0 - * @since 2025-10-16 - */ -public class ShaderManagement { - private static final Logger logger = LoggerFactory.getLogger(ShaderManagement.class); - - /** - * 着色器程序缓存映射,按名称存储已编译的着色器程序 - */ - public static final Map shaderMap = new HashMap<>(); - - /** - * 着色器列表,按顺序存储所有着色器源代码 - */ - public static final List shaderList = List.of( - new Shader2D(), - new SolidColorShader(), - new TextShader() - ); - - /** - * 默认着色器程序 - */ - private static ShaderProgram defaultProgram; - - /** - * 编译所有注册的着色器 - */ - public static void compileAllShaders() { - // 确保在渲染线程 - RenderSystem.assertOnRenderThread(); - - for (CompleteShader completeShader : shaderList) { - compileShaderProgram(completeShader); - } - - // 设置默认着色器 - if (defaultProgram == null && !shaderMap.isEmpty()) { - defaultProgram = shaderMap.values().iterator().next(); - } - } - - /** - * 编译单个完整的着色器程序 - */ - private static void compileShaderProgram(CompleteShader completeShader) { - String shaderName = completeShader.getShaderName(); - - try { - // 编译顶点着色器 - Shader vertexShader = completeShader.getVertexShader(); - int vsId = compileShader(GL20.GL_VERTEX_SHADER, vertexShader.getShaderCode(), - vertexShader.getShaderName()); - - // 编译片段着色器 - Shader fragmentShader = completeShader.getFragmentShader(); - int fsId = compileShader(GL20.GL_FRAGMENT_SHADER, fragmentShader.getShaderCode(), - fragmentShader.getShaderName()); - - // 链接程序 - int programId = linkProgram(vsId, fsId, shaderName); - - // 创建着色器程序对象 - ShaderProgram shaderProgram = new ShaderProgram(programId); - shaderMap.put(shaderName, shaderProgram); - - // 如果是默认着色器,设置为默认程序 - if (completeShader.isDefaultShader()) { - defaultProgram = shaderProgram; - setupDefaultUniforms(shaderProgram); - } - - // 清理单独的着色器对象 - RenderSystem.deleteShader(vsId); - RenderSystem.deleteShader(fsId); - - logger.info("成功编译着色器: {}", shaderName); - - } catch (Exception e) { - logger.error("编译着色器失败: {}", shaderName); - e.printStackTrace(); - throw new RuntimeException("Shader compilation failed: " + shaderName, e); - } - } - - /** - * 设置默认着色器的uniform值 - */ - private static void setupDefaultUniforms(ShaderProgram program) { - program.use(); - - // 设置纹理单元 - setUniformInt(program, "uTexture", 0); - setUniformFloat(program, "uOpacity", 1.0f); - setUniformVec4(program, "uColor", new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); - setUniformInt(program, "uBlendMode", 0); - setUniformInt(program, "uDebugMode", 0); - setUniformInt(program, "uLightCount", 0); - - program.stop(); - - RenderSystem.checkGLError("setupDefaultUniforms"); - } - - /** - * 编译着色器 - */ - private static int compileShader(int type, String source, String shaderName) { - int shaderId = RenderSystem.createShader(type); - RenderSystem.shaderSource(shaderId, source); - RenderSystem.compileShader(shaderId); - - // 检查编译状态 - if (RenderSystem.getShaderi(shaderId, RenderSystem.GL_COMPILE_STATUS) != RenderSystem.GL_TRUE) { - String log = RenderSystem.getShaderInfoLog(shaderId); - RenderSystem.deleteShader(shaderId); - throw new RuntimeException("着色器编译失败 [" + shaderName + "]:\n" + log); - } - - return shaderId; - } - - /** - * 链接着色器程序 - */ - private static int linkProgram(int vertexShaderId, int fragmentShaderId, String programName) { - int programId = RenderSystem.createProgram(); - RenderSystem.attachShader(programId, vertexShaderId); - RenderSystem.attachShader(programId, fragmentShaderId); - RenderSystem.linkProgram(programId); - - // 检查链接状态 - if (RenderSystem.getProgram(programId, RenderSystem.GL_LINK_STATUS) != RenderSystem.GL_TRUE) { - String log = RenderSystem.getProgramInfoLog(programId); - RenderSystem.deleteProgram(programId); - throw new RuntimeException("着色器程序链接失败 [" + programName + "]:\n" + log); - } - - // 验证程序(使用自定义验证方法) - validateProgram(programId, programName); - - return programId; - } - - /** - * 自定义程序验证方法 - */ - private static void validateProgram(int programId, String programName) { - int validateStatus = RenderSystem.getProgram(programId, RenderSystem.GL_VALIDATE_STATUS); - if (validateStatus != RenderSystem.GL_TRUE) { - String log = RenderSystem.getProgramInfoLog(programId); - logger.warn("着色器程序验证警告 [{}]: {}", programName, log); - } - } - - /** - * 获取默认着色器程序 - */ - public static ShaderProgram getDefaultProgram() { - return defaultProgram; - } - - /** - * 按名称获取着色器程序 - */ - public static ShaderProgram getShaderProgram(String name) { - return shaderMap.get(name); - } - - public static List getShaderList() { - return shaderList; - } - - /** - * 清理所有着色器资源 - */ - public static void cleanup() { - RenderSystem.assertOnRenderThread(); - - for (ShaderProgram program : shaderMap.values()) { - program.delete(); - } - shaderMap.clear(); - defaultProgram = null; - } - - // Uniform设置方法 - public static void setUniformInt(ShaderProgram program, String name, int value) { - program.use(); - int location = program.getUniformLocation(name); - if (location != -1) { - RenderSystem.uniform1i(location, value); - } - } - - public static void setUniformFloat(ShaderProgram program, String name, float value) { - program.use(); - int location = program.getUniformLocation(name); - if (location != -1) { - RenderSystem.uniform1f(location, value); - } - } - - public static void setUniformVec2(ShaderProgram program, String name, float x, float y) { - program.use(); - int location = program.getUniformLocation(name); - if (location != -1) { - RenderSystem.uniform2f(location, x, y); - } - } - - public static void setUniformVec2(ShaderProgram program, String name, org.joml.Vector2f vec) { - setUniformVec2(program, name, vec.x, vec.y); - } - - public static void setUniformVec3(ShaderProgram program, String name, float x, float y, float z) { - program.use(); - int location = program.getUniformLocation(name); - if (location != -1) { - RenderSystem.uniform3f(location, x, y, z); - } - } - - public static void setUniformVec3(ShaderProgram program, String name, Vector3f vec) { - setUniformVec3(program, name, vec.x, vec.y, vec.z); - } - - public static void setUniformVec4(ShaderProgram program, String name, float[] values) { - if (values.length != 4) { - throw new IllegalArgumentException("Vec4 uniform requires 4 values"); - } - program.use(); - int location = program.getUniformLocation(name); - if (location != -1) { - try (MemoryStack stack = stackPush()) { - FloatBuffer buffer = stack.mallocFloat(4); - buffer.put(values).flip(); - RenderSystem.uniform4f(location, values[0], values[1], values[2], values[3]); - } - } - } - - public static void setUniformVec4(ShaderProgram program, String name, Vector4f vec) { - setUniformVec4(program, name, new float[]{vec.x, vec.y, vec.z, vec.w}); - } - - public static void setUniformMat3(ShaderProgram program, String name, FloatBuffer matrix) { - program.use(); - int location = program.getUniformLocation(name); - if (location != -1) { - RenderSystem.uniformMatrix3(location, false, matrix); - } - } - - public static void setUniformMat3(ShaderProgram program, String name, org.joml.Matrix3f matrix) { - program.use(); - int location = program.getUniformLocation(name); - if (location != -1) { - RenderSystem.uniformMatrix3(location, matrix); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderProgram.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderProgram.java deleted file mode 100644 index 411c7dc..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/ShaderProgram.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.sources; - -import org.joml.Matrix3f; -import org.lwjgl.opengl.GL20; - -import java.util.HashMap; -import java.util.Map; - -import static org.lwjgl.opengl.GL20.*; - -/** - * @author tzdwindows 7 - */ -public class ShaderProgram { - public final int programId; - public final Map uniformCache = new HashMap<>(); - - public ShaderProgram(int programId) { - this.programId = programId; - } - - public void use() { - GL20.glUseProgram(programId); - } - - public void stop() { - GL20.glUseProgram(0); - } - - public int getUniformLocation(String name) { - return uniformCache.computeIfAbsent(name, k -> { - return glGetUniformLocation(programId, k); - }); - } - - // 添加 uniform 设置方法 - public void setUniform1i(String name, int value) { - int location = getUniformLocation(name); - if (location != -1) { - glUniform1i(location, value); - } - } - - public void setUniform1f(String name, float value) { - int location = getUniformLocation(name); - if (location != -1) { - glUniform1f(location, value); - } - } - - public void setUniform2f(String name, float x, float y) { - int location = getUniformLocation(name); - if (location != -1) { - glUniform2f(location, x, y); - } - } - - public void setUniform3f(String name, float x, float y, float z) { - int location = getUniformLocation(name); - if (location != -1) { - glUniform3f(location, x, y, z); - } - } - - public void setUniform4f(String name, float x, float y, float z, float w) { - int location = getUniformLocation(name); - if (location != -1) { - glUniform4f(location, x, y, z, w); - } - } - - public void setUniformMatrix3(String name, Matrix3f matrix) { - int location = getUniformLocation(name); - if (location != -1) { - float[] matrixArray = new float[9]; - matrix.get(matrixArray); - glUniformMatrix3fv(location, false, matrixArray); - } - } - - // 重载方法,直接使用 location - public void setUniform1i(int location, int value) { - if (location != -1) { - glUniform1i(location, value); - } - } - - public void setUniform1f(int location, float value) { - if (location != -1) { - glUniform1f(location, value); - } - } - - public void setUniform4f(int location, float x, float y, float z, float w) { - if (location != -1) { - glUniform4f(location, x, y, z, w); - } - } - - public void setUniformMatrix3(int location, Matrix3f matrix) { - if (location != -1) { - float[] matrixArray = new float[9]; - matrix.get(matrixArray); - glUniformMatrix3fv(location, false, matrixArray); - } - } - - public void delete() { - if (GL20.glIsProgram(programId)) { - GL20.glDeleteProgram(programId); - } - } - - public int getProgramId() { - return programId; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/FragmentShaders.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/FragmentShaders.java deleted file mode 100644 index 96efc15..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/FragmentShaders.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.sources.def; - -import com.chuangzhou.vivid2D.render.systems.sources.Shader; - -/** - * @author tzdwindows 7 - */ -public class FragmentShaders implements Shader { - public static final String FRAGMENT_SHADER_SRC = - """ - #version 330 core - in vec2 vTexCoord; - in vec2 vWorldPos; - out vec4 FragColor; - - uniform sampler2D uTexture; - uniform vec4 uColor; - uniform float uOpacity; - uniform int uBlendMode; - uniform int uDebugMode; - - #define MAX_LIGHTS 8 - uniform vec2 uLightsPos[MAX_LIGHTS]; - uniform vec3 uLightsColor[MAX_LIGHTS]; - uniform float uLightsIntensity[MAX_LIGHTS]; - uniform int uLightsIsAmbient[MAX_LIGHTS]; - uniform int uLightCount; - - // 常用衰减系数(可在 shader 内微调) - const float ATT_CONST = 1.0; - const float ATT_LINEAR = 0.09; - const float ATT_QUAD = 0.032; - - void main() { - // 先采样纹理 - vec4 tex = texture(uTexture, vTexCoord); - float alpha = tex.a * uOpacity; - if (alpha <= 0.001) discard; - - // 如果没有光源,跳过光照计算(性能更好并且保持原始贴图色) - if (uLightCount == 0) { - vec3 base = tex.rgb * uColor.rgb; - // 简单的色调映射(防止数值过大) - base = clamp(base, 0.0, 1.0); - FragColor = vec4(base, alpha); - return; - } - - // 基础颜色(纹理 * 部件颜色) - vec3 baseColor = tex.rgb * uColor.rgb; - - // 全局环境光基线(可以适度提高以避免全黑) - vec3 ambient = vec3(0.06); // 小环境光补偿 - vec3 lighting = vec3(0.0); - vec3 specularAccum = vec3(0.0); - - // 累积环境光(来自被标记为环境光的光源) - for (int i = 0; i < uLightCount; ++i) { - if (uLightsIsAmbient[i] == 1) { - lighting += uLightsColor[i] * uLightsIntensity[i]; - } - } - // 加上基线环境光 - lighting += ambient; - - // 对每个非环境光计算基于距离的衰减与简单高光 - for (int i = 0; i < uLightCount; ++i) { - if (uLightsIsAmbient[i] == 1) continue; - - vec2 toLight = uLightsPos[i] - vWorldPos; - float dist = length(toLight); - // 标准物理式衰减 - float attenuation = ATT_CONST / (ATT_CONST + ATT_LINEAR * dist + ATT_QUAD * dist * dist); - - // 强度受光源强度和衰减影响 - float radiance = uLightsIntensity[i] * attenuation; - - // 漫反射:在纯2D情景下,法线与视线近似固定(Z向), - // 所以漫反射对所有片元是恒定的。我们用一个基于距离的柔和因子来模拟明暗变化。 - float diffuseFactor = clamp(1.0 - (dist * 0.0015), 0.0, 1.0); // 通过调节常数控制半径感觉 - vec3 diff = uLightsColor[i] * radiance * diffuseFactor; - lighting += diff; - - // 简单高光(基于视向与反射的大致模拟,产生亮点) - vec3 lightDir3 = normalize(vec3(toLight, 0.0)); - vec3 viewDir = vec3(0.0, 0.0, 1.0); - vec3 normal = vec3(0.0, 0.0, 1.0); - vec3 reflectDir = reflect(-lightDir3, normal); - float specFactor = pow(max(dot(viewDir, reflectDir), 0.0), 16.0); // 16 为高光粗糙度,可调 - float specIntensity = 0.2; // 高光强度系数 - specularAccum += uLightsColor[i] * radiance * specFactor * specIntensity; - } - - // 限制光照的最大值以避免过曝(可根据场景调整) - vec3 totalLighting = min(lighting + specularAccum, vec3(2.0)); - - // 将光照应用到基础颜色 - vec3 finalColor = baseColor * totalLighting; - - // 支持简单混合模式(保留原有行为) - if (uBlendMode == 1) finalColor = tex.rgb + uColor.rgb; - else if (uBlendMode == 2) finalColor = tex.rgb * uColor.rgb; - else if (uBlendMode == 3) finalColor = 1.0 - (1.0 - tex.rgb) * (1.0 - uColor.rgb); - - finalColor = clamp(finalColor, 0.0, 1.0); - FragColor = vec4(finalColor, alpha); - } - """; - - @Override - public String getShaderCode() { - return FRAGMENT_SHADER_SRC; - } - - @Override - public String getShaderName() { - return "Fragment shaders"; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/Shader2D.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/Shader2D.java deleted file mode 100644 index 56c6513..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/Shader2D.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.sources.def; - -import com.chuangzhou.vivid2D.render.systems.sources.CompleteShader; -import com.chuangzhou.vivid2D.render.systems.sources.Shader; - -/** - * 默认着色器实现 - * - * @author tzdwindows 7 - * @version 1.0 - * @since 2025-10-17 - */ -public class Shader2D implements CompleteShader { - @Override - public Shader getVertexShader() { - return new VertexShaders(); - } - - @Override - public Shader getFragmentShader() { - return new FragmentShaders(); - } - - @Override - public String getShaderName() { - return "Vivid2d Shader"; - } - - @Override - public boolean isDefaultShader() { - return true; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorFragmentShader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorFragmentShader.java deleted file mode 100644 index 4b6fc3b..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorFragmentShader.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.sources.def; - -import com.chuangzhou.vivid2D.render.systems.sources.Shader; - -/** - * 纯色着色器的片段着色器 - * 只使用颜色,忽略纹理 - * - * @author tzdwindows 7 - */ -public class SolidColorFragmentShader implements Shader { - public static final String FRAGMENT_SHADER_SRC = - """ - #version 330 core - out vec4 FragColor; - - uniform vec4 uColor; - uniform float uOpacity; - - void main() { - // 直接使用颜色,忽略纹理 - vec4 finalColor = uColor; - finalColor.a *= uOpacity; - - // 如果透明度太低则丢弃片段 - if (finalColor.a <= 0.001) discard; - - FragColor = finalColor; - } - """; - - @Override - public String getShaderCode() { - return FRAGMENT_SHADER_SRC; - } - - @Override - public String getShaderName() { - return "Solid Color Fragment Shader"; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorShader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorShader.java deleted file mode 100644 index ad300de..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorShader.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.sources.def; - -import com.chuangzhou.vivid2D.render.systems.sources.CompleteShader; -import com.chuangzhou.vivid2D.render.systems.sources.Shader; -import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram; - -/** - * 纯色着色器程序 - * 专门用于绘制纯色几何体,如选中框、调试图形等 - * - * @author tzdwindows 7 - */ -public class SolidColorShader implements CompleteShader { - private final SolidColorVertexShader vertexShader; - private final SolidColorFragmentShader fragmentShader; - - public SolidColorShader() { - this.vertexShader = new SolidColorVertexShader(); - this.fragmentShader = new SolidColorFragmentShader(); - } - - @Override - public Shader getVertexShader() { - return vertexShader; - } - - @Override - public Shader getFragmentShader() { - return fragmentShader; - } - - @Override - public String getShaderName() { - return "Solid Color Shader"; - } - - @Override - public boolean isDefaultShader() { - return false; // 这不是默认着色器,是专门用途的着色器 - } - - @Override - public void setDefaultUniforms(ShaderProgram program) { - // 设置默认的uniform值 - if (program != null) { - // 设置默认颜色为白色 - int colorLoc = program.getUniformLocation("uColor"); - if (colorLoc != -1) { - program.setUniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); - } - - // 设置默认不透明度 - int opacityLoc = program.getUniformLocation("uOpacity"); - if (opacityLoc != -1) { - program.setUniform1f(opacityLoc, 1.0f); - } - } - } - - /** - * 设置着色器颜色 - */ - public void setColor(ShaderProgram program, float r, float g, float b, float a) { - if (program != null) { - int colorLoc = program.getUniformLocation("uColor"); - if (colorLoc != -1) { - program.setUniform4f(colorLoc, r, g, b, a); - } - } - } - - /** - * 设置着色器不透明度 - */ - public void setOpacity(ShaderProgram program, float opacity) { - if (program != null) { - int opacityLoc = program.getUniformLocation("uOpacity"); - if (opacityLoc != -1) { - program.setUniform1f(opacityLoc, opacity); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorVertexShader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorVertexShader.java deleted file mode 100644 index 014666b..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/SolidColorVertexShader.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.sources.def; - -import com.chuangzhou.vivid2D.render.systems.sources.Shader; - -/** - * 纯色着色器的顶点着色器 - * - * @author tzdwindows 7 - */ -public class SolidColorVertexShader implements Shader { - public static final String VERTEX_SHADER_SRC = - """ - #version 330 core - layout(location = 0) in vec2 aPosition; - layout(location = 1) in vec2 aTexCoord; - - uniform mat3 uModelMatrix; - uniform mat3 uViewMatrix; - uniform mat3 uProjectionMatrix; - - void main() { - // 使用 3x3 矩阵链计算屏幕位置 - vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0); - gl_Position = vec4(p.xy, 0.0, 1.0); - } - """; - - @Override - public String getShaderCode() { - return VERTEX_SHADER_SRC; - } - - @Override - public String getShaderName() { - return "Solid Color Vertex Shader"; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/TextShader.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/TextShader.java deleted file mode 100644 index 6ea07e8..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/TextShader.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.sources.def; - -import com.chuangzhou.vivid2D.render.systems.sources.CompleteShader; -import com.chuangzhou.vivid2D.render.systems.sources.Shader; -import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram; -import org.joml.Vector4f; - -/** - * 文本着色器 (已修正并与渲染引擎兼容) - * - * @author tzdwindows 7 - */ -public class TextShader implements CompleteShader { - - private final VertexShader vertexShader = new VertexShader(); - private final FragmentShader fragmentShader = new FragmentShader(); - - @Override - public Shader getVertexShader() { - return vertexShader; - } - - @Override - public Shader getFragmentShader() { - return fragmentShader; - } - - @Override - public String getShaderName() { - return "TextShader"; - } - - @Override - public boolean isDefaultShader() { - return false; - } - - @Override - public void setDefaultUniforms(ShaderProgram program) { - program.setUniform4f("uColor", 1.0f, 1.0f, 1.0f, 1.0f); - program.setUniform1i("uTexture", 0); - } - - // --- Vertex Shader (已适配 mat3 和 uCameraZ) --- - private static class VertexShader implements Shader { - @Override - public String getShaderCode() { - return """ - #version 330 core - layout(location = 0) in vec2 aPosition; - layout(location = 1) in vec2 aTexCoord; - - uniform mat3 uModelMatrix; - uniform mat3 uViewMatrix; - uniform mat3 uProjectionMatrix; - - uniform float uCameraZ; - - out vec2 vTexCoord; - - void main() { - vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0); - - // 输出为 gl_Position (vec4) - gl_Position = vec4(p.xy, 0.0, 1.0); - vTexCoord = aTexCoord; - } - """; - } - - @Override - public String getShaderName() { - return "TextVertexShader"; - } - } - - private static class FragmentShader implements Shader { - @Override - public String getShaderCode() { - return """ - #version 330 core - in vec2 vTexCoord; - out vec4 FragColor; - - uniform sampler2D uTexture; - uniform vec4 uColor; - - void main() { - float alpha = texture(uTexture, vTexCoord).r; - FragColor = vec4(uColor.rgb, uColor.a * alpha); - if (FragColor.a < 0.01) { - discard; - } - } - """; - } - - @Override - public String getShaderName() { - return "TextFragmentShader"; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/VertexShaders.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/VertexShaders.java deleted file mode 100644 index 8b9f7ce..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/sources/def/VertexShaders.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.chuangzhou.vivid2D.render.systems.sources.def; - -import com.chuangzhou.vivid2D.render.systems.sources.Shader; - -/** - * 顶点着色器 - * - * @author tzdwindows 7 - * @version 1.0 - * @since 2025-10-17 - */ -public class VertexShaders implements Shader { - public static final String VERTEX_SHADER_SRC = - """ - #version 330 core - layout(location = 0) in vec2 aPosition; - layout(location = 1) in vec2 aTexCoord; - out vec2 vTexCoord; - out vec2 vWorldPos; - - uniform mat3 uModelMatrix; - uniform mat3 uViewMatrix; - uniform mat3 uProjectionMatrix; - - void main() { - // 使用 3x3 矩阵链计算屏幕位置(假设矩阵是二维仿射) - vec3 p = uProjectionMatrix * uViewMatrix * uModelMatrix * vec3(aPosition, 1.0); - gl_Position = vec4(p.xy, 0.0, 1.0); - vTexCoord = aTexCoord; - // 输出 world-space 位置供 fragment shader 使用(仅 xy) - vWorldPos = (uModelMatrix * vec3(aPosition, 1.0)).xy; - } - """; - - @Override - public String getShaderCode() { - return VERTEX_SHADER_SRC; - } - - @Override - public String getShaderName() { - return "Vertex Shaders"; - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/AI2Test.java b/src/main/java/com/chuangzhou/vivid2D/test/AI2Test.java deleted file mode 100644 index 6097ca9..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/test/AI2Test.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.chuangzhou.vivid2D.test; - -import com.chuangzhou.vivid2D.ai.anime_face_segmentation.AnimeModelWrapper; - -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; -import java.util.Set; - -/** - * 用来分析人物的脸部信息头发、眼睛、嘴巴、脸部、皮肤、衣服 - */ -public class AI2Test { - public static void main(String[] args) throws Exception { - System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); - System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); - - // 使用 AnimeModelWrapper 而不是 VividModelWrapper - AnimeModelWrapper wrapper = AnimeModelWrapper.load(Paths.get("C:\\Users\\Administrator\\Desktop\\model\\Anime-Face-Segmentation\\Anime-Face-Segmentation.pt")); - - // 使用 Anime-Face-Segmentation 的 7 个标签 - Set animeLabels = Set.of( - "background", - "hair", // 头发 - "eye", // 眼睛 - "mouth", // 嘴巴 - "face", // 脸部 - "skin", // 皮肤 - "clothes" // 衣服 - ); - - wrapper.segmentAndSave( - Paths.get("C:\\Users\\Administrator\\Desktop\\b_215609167a3a20ac2075487bd532bbff.jpg").toFile(), - animeLabels, - Paths.get("C:\\models\\out") - ); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/AI3Test.java b/src/main/java/com/chuangzhou/vivid2D/test/AI3Test.java deleted file mode 100644 index 42f3f28..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/test/AI3Test.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.chuangzhou.vivid2D.test; - -import com.chuangzhou.vivid2D.ai.anime_segmentation.Anime2VividModelWrapper; - -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; -import java.util.Set; - -/** - * 这个ai模型负责分离人物与背景 - */ -public class AI3Test { - public static void main(String[] args) throws Exception { - System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); - System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); - Anime2VividModelWrapper wrapper = Anime2VividModelWrapper.load(Paths.get("C:\\Users\\Administrator\\Desktop\\model\\anime-segmentation-main\\anime-segmentation.pt")); - - Set faceLabels = Set.of("foreground"); - - wrapper.segmentAndSave( - Paths.get("C:\\Users\\Administrator\\Desktop\\b_e15c587fab8a7291740d44e4ce57599f.jpg").toFile(), - faceLabels, - Paths.get("C:\\models\\out") - ); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/AITest.java b/src/main/java/com/chuangzhou/vivid2D/test/AITest.java deleted file mode 100644 index b89ff83..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/test/AITest.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.chuangzhou.vivid2D.test; - -import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetVividModelWrapper; - -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; -import java.util.Set; - -/** - * 测试人脸解析模型 - */ -public class AITest { - public static void main(String[] args) throws Exception { - System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); - System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); - BiSeNetVividModelWrapper wrapper = BiSeNetVividModelWrapper.load(Paths.get("C:\\models\\bisenet_face_parsing.pt")); - - // 使用 BiSeNet 人脸解析模型的 18 个非背景标签 - Set faceLabels = Set.of( - "skin", "nose", "eye_left", "eye_right", "eyebrow_left", - "eyebrow_right", "ear_left", "ear_right", "mouth", "lip_upper", - "lip_lower", "hair", "hat", "earring", "necklace", "clothes", - "facial_hair", "neck" - ); - - wrapper.segmentAndSave( - Paths.get("C:\\Users\\Administrator\\Desktop\\b_f4881214f0d18b6cf848b6736f554821.png").toFile(), - faceLabels, - Paths.get("C:\\models\\out") - ); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java deleted file mode 100644 index 0743f44..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.chuangzhou.vivid2D.test; - -import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel; -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.awt.ParametersPanel; -import com.chuangzhou.vivid2D.render.awt.TransformPanel; -import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement; -import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.formdev.flatlaf.themes.FlatMacDarkLaf; -import com.formdev.flatlaf.themes.FlatMacLightLaf; -import org.jetbrains.annotations.NotNull; - -import javax.swing.*; -import java.awt.*; -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.util.List; - -/** - * 简单的测试示例:创建一个 Model2D,添加几层(部件), - * 然后在 JFrame 中展示 ModelLayerPanel(左侧)、ModelRenderPanel(中间渲染区) - * 和模型树(右侧)以便观察变化。 - */ -public class ModelLayerPanelTest { - public static void main(String[] args) { - SwingUtilities.invokeLater(() -> { - try { - UIManager.setLookAndFeel(new FlatMacDarkLaf()); - } catch (UnsupportedLookAndFeelException e) { - throw new RuntimeException(e); - } - System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); - System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); - JFrame frame = new JFrame("ModelLayerPanel 测试(含渲染面板和变换面板)"); - frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - frame.setLayout(new BorderLayout()); - ModelRenderPanel renderPanel = new ModelRenderPanel("C:\\Users\\Administrator\\Desktop\\testing.model", 640, 480); - ModelLayerPanel layerPanel = new ModelLayerPanel(renderPanel); - layerPanel.setPreferredSize(new Dimension(260, 600)); - frame.add(layerPanel, BorderLayout.WEST); - renderPanel.setPreferredSize(new Dimension(640, 480)); - frame.add(renderPanel, BorderLayout.CENTER); - TransformPanel transformPanel = new TransformPanel(renderPanel); - JTabbedPane rightTabbedPane = new JTabbedPane(); - renderPanel.getGlContextManager().waitForModel().thenAccept(model -> { - if (model == null) return; - JTree tree = new JTree(model.toTreeNode()); - JScrollPane treeScroll = new JScrollPane(tree); - treeScroll.setPreferredSize(new Dimension(280, 600)); - rightTabbedPane.addTab("模型结构", treeScroll); - JScrollPane transformScroll = new JScrollPane(transformPanel); - transformScroll.setPreferredSize(new Dimension(280, 600)); - rightTabbedPane.addTab("变换控制", transformScroll); - rightTabbedPane.setPreferredSize(new Dimension(300, 600)); - frame.add(rightTabbedPane, BorderLayout.EAST); - ParametersPanel parametersPanel = new ParametersPanel(renderPanel); - renderPanel.setParametersManagement(ParametersManagement.getInstance(parametersPanel)); - JScrollPane paramScroll = new JScrollPane(parametersPanel); - paramScroll.setPreferredSize(new Dimension(280, 600)); - rightTabbedPane.addTab("参数管理", paramScroll); - JPanel bottom = getBottom(renderPanel, transformPanel); - frame.add(bottom, BorderLayout.SOUTH); - renderPanel.addModelClickListener((mesh, modelX, modelY, screenX, screenY) -> { - if (mesh == null) return; - System.out.println("点击了模型:" + mesh.getName() + ",模型坐标:" + modelX + ", " + modelY + ",屏幕坐标:" + screenX + ", " + screenY); - List selectedPart = renderPanel.getSelectedParts(); - transformPanel.setSelectedParts(selectedPart); - rightTabbedPane.setSelectedIndex(1); - }); - frame.addWindowListener(new java.awt.event.WindowAdapter() { - @Override - public void windowClosed(java.awt.event.WindowEvent e) { - try { - renderPanel.getGlContextManager().dispose(); - } catch (Throwable ignored) { - } - model.saveToFile("C:\\Users\\Administrator\\Desktop\\testing.model"); - ParametersManagementData managementData = new ParametersManagementData(renderPanel.getParametersManagement()); - String managementFilePath = "C:\\Users\\Administrator\\Desktop\\testing.model" + ".data"; - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(managementFilePath))) { - oos.writeObject(managementData); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - System.exit(0); - } - }); - frame.setSize(1300, 700); - frame.setLocationRelativeTo(null); - frame.setVisible(true); - }); - }); - } - - private static @NotNull JPanel getBottom(ModelRenderPanel renderPanel, TransformPanel transformPanel) { - JPanel bottom = new JPanel(new FlowLayout(FlowLayout.LEFT)); - - JButton printOrderBtn = new JButton("打印部件顺序(控制台)"); - printOrderBtn.addActionListener(e -> { - System.out.println("当前模型部件顺序:"); - renderPanel.getGlContextManager().waitForModel().thenAccept(model -> { - if (model == null) return; - for (ModelPart p : model.getParts()) { - System.out.println(" - " + p.getName() + " (可见=" + p.isVisible() + ", 不透明度=" + p.getOpacity() + ")"); - } - }); - }); - bottom.add(printOrderBtn); - - // 添加选中部件更新按钮 - JButton updateSelectionBtn = new JButton("更新选中部件"); - updateSelectionBtn.addActionListener(e -> { - renderPanel.getGlContextManager().executeInGLContext(() -> { - List selectedPart = renderPanel.getSelectedParts(); - transformPanel.setSelectedParts(selectedPart); - }); - }); - bottom.add(updateSelectionBtn); - return bottom; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelLoadTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelLoadTest.java deleted file mode 100644 index f58ce27..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelLoadTest.java +++ /dev/null @@ -1,456 +0,0 @@ -package com.chuangzhou.vivid2D.test; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.model.Model2D; -import org.lwjgl.glfw.GLFWVidMode; -import org.lwjgl.opengl.GL; -import org.lwjgl.system.MemoryUtil; - -import java.io.File; -import java.lang.reflect.Method; -import java.nio.IntBuffer; -import java.util.List; - -import static org.lwjgl.glfw.GLFW.*; -import static org.lwjgl.opengl.GL11.*; - -/** - * ModelLoadTest - enhanced debug loader - *

- * - 加载模型后会自动检查 model 内部结构并打印(parts, meshes, textures) - * - 尝试把第一个 part 放到窗口中心以确保在可视范围内 - * - 每帧确保 ModelRender 的 viewport 与窗口大小一致 - *

- * 运行前请确保 MODEL_PATH 指向你保存的 model 文件 - */ -public class ModelLoadTest { - - private long window; - private Model2D model; - - // Window dimensions - private static int WINDOW_WIDTH = 800; - private static int WINDOW_HEIGHT = 600; - - // Debug - private boolean enableWireframe = false; - - // 要加载的 model 文件路径(请根据实际保存位置修改) - private static final String MODEL_PATH = "C:\\Users\\Administrator\\Desktop\\testing.model"; - - public static void main(String[] args) { - new ModelLoadTest().run(); - } - - public void run() { - try { - init(); - loop(); - } catch (Throwable t) { - t.printStackTrace(); - } finally { - cleanup(); - } - } - - private void init() throws Exception { - if (!glfwInit()) { - throw new IllegalStateException("Unable to initialize GLFW"); - } - - // Configure GLFW - glfwDefaultWindowHints(); - glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); - glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - - // Create window - window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Model Load Test - DEBUG", MemoryUtil.NULL, MemoryUtil.NULL); - if (window == MemoryUtil.NULL) { - throw new RuntimeException("Failed to create the GLFW window"); - } - - // Center window - GLFWVidMode vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor()); - if (vidMode != null) { - glfwSetWindowPos( - window, - (vidMode.width() - WINDOW_WIDTH) / 2, - (vidMode.height() - WINDOW_HEIGHT) / 2 - ); - } - - // Key callback - glfwSetKeyCallback(window, (w, key, scancode, action, mods) -> { - if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) { - glfwSetWindowShouldClose(window, true); - } - if (key == GLFW_KEY_SPACE && action == GLFW_RELEASE) { - enableWireframe = !enableWireframe; - System.out.println("Wireframe: " + (enableWireframe ? "ON" : "OFF")); - } - }); - - // Create OpenGL context - glfwMakeContextCurrent(window); - GL.createCapabilities(); - - // Show window - glfwShowWindow(window); - - // 初始化渲染系统 - ModelRender.initialize(); - - // 尝试加载 model - loadModelFromFile(MODEL_PATH); - - // 一些额外检查/尝试修复 - postLoadSanityChecks(); - } - - /** - * 尝试多种方式加载 Model2D:优先尝试静态方法 loadFromFile/fromFile/load, - * 其次尝试创建空实例并调用实例方法 loadFromFile(String) - */ - private void loadModelFromFile(String path) { - inspectSerializedFileStructure(path); - File f = new File(path); - if (!f.exists()) { - System.err.println("Model file not found: " + path); - model = null; - return; - } - - // 尝试静态工厂方法 - try { - Method m; - String[] names = {"loadFromFile", "fromFile", "load"}; - for (String name : names) { - try { - m = Model2D.class.getMethod(name, String.class); - } catch (NoSuchMethodException e) { - m = null; - } - if (m != null) { - Object res = m.invoke(null, path); - if (res instanceof Model2D) { - model = (Model2D) res; - System.out.println("Model loaded via static method: " + name); - return; - } - } - } - } catch (Throwable ignored) { - // 继续尝试实例方法 - } - - // 尝试实例方法 - try { - Model2D inst = null; - try { - inst = Model2D.class.getConstructor().newInstance(); - } catch (NoSuchMethodException nsme) { - try { - inst = Model2D.class.getConstructor(String.class).newInstance("loaded_model"); - } catch (NoSuchMethodException ex) { - inst = null; - } - } - if (inst != null) { - try { - Method im = Model2D.class.getMethod("loadFromFile", String.class); - im.invoke(inst, path); - model = inst; - System.out.println("Model loaded via instance method loadFromFile"); - return; - } catch (NoSuchMethodException ignored) { - } - } - } catch (Throwable t) { - // ignore - } - - // 最后尝试反序列化 ObjectInputStream - try { - java.io.ObjectInputStream ois = new java.io.ObjectInputStream(new java.io.FileInputStream(f)); - Object obj = ois.readObject(); - ois.close(); - if (obj instanceof Model2D) { - model = (Model2D) obj; - System.out.println("Model deserialized from file via ObjectInputStream"); - return; - } else { - System.err.println("Deserialized object is not Model2D: " + (obj != null ? obj.getClass() : "null")); - } - } catch (Throwable t) { - // ignore - } - - System.err.println("Failed to load model from file using known methods: " + path); - model = null; - } - - // ====== 追加方法:反序列化内容深度检测(帮助诊断保存文件里到底有什么) ====== - private void inspectSerializedFileStructure(String path) { - File f = new File(path); - if (!f.exists()) { - System.err.println("File not found: " + path); - return; - } - System.out.println("Inspecting serialized object structure in file: " + path); - try (java.io.ObjectInputStream ois = new java.io.ObjectInputStream(new java.io.FileInputStream(f))) { - Object obj = ois.readObject(); - printObjectStructure(obj, 0, new java.util.HashSet<>()); - } catch (Throwable t) { - System.err.println("Failed to read/inspect serialized file: " + t.getMessage()); - } - } - - private void printObjectStructure(Object obj, int indent, java.util.Set seen) { - if (obj == null) { - printIndent(indent); - System.out.println("null"); - return; - } - if (seen.contains(obj)) { - printIndent(indent); - System.out.println("<>"); - return; - } - seen.add(obj); - Class cls = obj.getClass(); - printIndent(indent); - System.out.println(cls.getName()); - // 如果是集合,列出元素类型/数目(不深入过深避免堆栈) - if (obj instanceof java.util.Collection col) { - printIndent(indent + 1); - System.out.println("size=" + col.size()); - int i = 0; - for (Object e : col) { - if (i++ > 20) { - printIndent(indent + 1); - System.out.println("... (truncated)"); - break; - } - printObjectStructure(e, indent + 1, seen); - } - return; - } - if (obj instanceof java.util.Map map) { - printIndent(indent + 1); - System.out.println("size=" + map.size()); - int i = 0; - for (Object k : map.keySet()) { - if (i++ > 20) { - printIndent(indent + 1); - System.out.println("... (truncated)"); - break; - } - printIndent(indent + 1); - System.out.println("Key:"); - printObjectStructure(k, indent + 2, seen); - printIndent(indent + 1); - System.out.println("Value:"); - printObjectStructure(map.get(k), indent + 2, seen); - } - return; - } - // 打印字段(反射),但避免进入 Java 内部类和基本类型 - java.lang.reflect.Field[] fields = cls.getDeclaredFields(); - for (java.lang.reflect.Field f : fields) { - f.setAccessible(true); - Object val = null; - try { - val = f.get(obj); - } catch (Throwable ignored) { - } - printIndent(indent + 1); - System.out.println(f.getName() + " : " + (val == null ? "null" : val.getClass().getName())); - // 对常见自定义类型深入一层 - if (val != null && !val.getClass().getName().startsWith("java.") && !val.getClass().isPrimitive()) { - if (val instanceof Number || val instanceof String) continue; - printObjectStructure(val, indent + 2, seen); - } - } - } - - private void printIndent(int n) { - for (int i = 0; i < n; i++) System.out.print(" "); - } - - - /** - * 加载后的一些检查与尝试性修复: - * - 打印 parts/meshes/textures 的信息 - * - 如果没有 parts/meshes,提醒并退出 - * - 如果有 part,尝试把第一个 part 放到窗口中心 - */ - private void postLoadSanityChecks() { - if (model == null) { - System.err.println("Model is null after loading. Nothing to render."); - return; - } - - System.out.println("Model loaded: " + model.getClass().getName()); - - try { - // 通过反射尝试读取 parts / meshes / textures 等 - Method getParts = tryGetMethod(Model2D.class, "getParts"); - Method getMeshes = tryGetMethod(Model2D.class, "getMeshes"); - Method getTextures = tryGetMethod(Model2D.class, "getTextures"); - - if (getParts != null) { - Object partsObj = getParts.invoke(model); - if (partsObj instanceof List parts) { - System.out.println("Parts count: " + parts.size()); - for (int i = 0; i < parts.size(); i++) { - Object p = parts.get(i); - System.out.println(" Part[" + i + "]: " + p.getClass().getName()); - // 尝试获取 name / position via reflection - try { - Method getName = tryGetMethod(p.getClass(), "getName"); - Method getPosition = tryGetMethod(p.getClass(), "getPosition"); - Object name = getName != null ? getName.invoke(p) : ""; - Object pos = getPosition != null ? getPosition.invoke(p) : null; - System.out.println(" name=" + name + ", pos=" + pos); - } catch (Throwable ignored) { - } - } - } - } - - if (getMeshes != null) { - Object meshesObj = getMeshes.invoke(model); - if (meshesObj instanceof List meshes) { - System.out.println("Meshes count: " + meshes.size()); - for (int i = 0; i < Math.min(meshes.size(), 10); i++) { - Object m = meshes.get(i); - System.out.println(" Mesh[" + i + "]: " + m.getClass().getName()); - // try to print vertex count / texture - try { - Method getVertexCount = tryGetMethod(m.getClass(), "getVertexCount"); - Method getTexture = tryGetMethod(m.getClass(), "getTexture"); - Object vc = getVertexCount != null ? getVertexCount.invoke(m) : null; - Object tex = getTexture != null ? getTexture.invoke(m) : null; - System.out.println(" vertexCount=" + vc + ", texture=" + tex); - } catch (Throwable ignored) { - } - } - } - } - - if (getTextures != null) { - Object texObj = getTextures.invoke(model); - if (texObj instanceof List texs) { - System.out.println("Textures count: " + texs.size()); - for (int i = 0; i < Math.min(texs.size(), 10); i++) { - Object t = texs.get(i); - System.out.println(" Texture[" + i + "]: " + t.getClass().getName()); - try { - Method getW = tryGetMethod(t.getClass(), "getWidth"); - Method getH = tryGetMethod(t.getClass(), "getHeight"); - Method getId = tryGetMethod(t.getClass(), "getTextureId"); - Object w = getW != null ? getW.invoke(t) : "?"; - Object h = getH != null ? getH.invoke(t) : "?"; - Object id = getId != null ? getId.invoke(t) : "?"; - System.out.println(" size=" + w + "x" + h + ", id=" + id); - } catch (Throwable ignored) { - } - } - } - } - - } catch (Throwable t) { - System.err.println("Failed to introspect model: " + t.getMessage()); - t.printStackTrace(); - } - } - - private Method tryGetMethod(Class cls, String name, Class... params) { - try { - return cls.getMethod(name, params); - } catch (NoSuchMethodException e) { - return null; - } - } - - private void loop() { - glClearColor(0.2f, 0.2f, 0.2f, 1.0f); - - float last = (float) glfwGetTime(); - - while (!glfwWindowShouldClose(window)) { - float now = (float) glfwGetTime(); - float delta = now - last; - last = now; - - // 每帧确保 viewport 与窗口大小一致(避免投影错位) - IntBuffer w = MemoryUtil.memAllocInt(1); - IntBuffer h = MemoryUtil.memAllocInt(1); - glfwGetFramebufferSize(window, w, h); - int ww = Math.max(1, w.get(0)); - int hh = Math.max(1, h.get(0)); - MemoryUtil.memFree(w); - MemoryUtil.memFree(h); - if (ww != WINDOW_WIDTH || hh != WINDOW_HEIGHT) { - WINDOW_WIDTH = ww; - WINDOW_HEIGHT = hh; - ModelRender.setViewport(WINDOW_WIDTH, WINDOW_HEIGHT); - System.out.println("Viewport updated to: " + WINDOW_WIDTH + "x" + WINDOW_HEIGHT); - } - - // render - render(delta); - - glfwSwapBuffers(window); - glfwPollEvents(); - } - } - - private void render(float deltaTime) { - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - if (enableWireframe) { - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); - } else { - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - } - - if (model == null) { - // Nothing to render, 提示并返回 - //(仅打印一次以免刷屏) - System.err.println("No model to render (model == null)."); - return; - } - - try { - ModelRender.render(deltaTime, model); - } catch (Throwable t) { - t.printStackTrace(); - } - - // 额外检查 glGetError - int err = glGetError(); - if (err != GL_NO_ERROR) { - System.err.println("OpenGL error after render: 0x" + Integer.toHexString(err)); - } - } - - private void cleanup() { - System.out.println("Cleaning up..."); - - try { - ModelRender.cleanup(); - } catch (Throwable ignored) { - } - - - if (window != MemoryUtil.NULL) { - glfwDestroyWindow(window); - } - glfwTerminate(); - System.out.println("Finished cleanup"); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java deleted file mode 100644 index 183ee91..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java +++ /dev/null @@ -1,238 +0,0 @@ -package com.chuangzhou.vivid2D.test; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.util.LightSource; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.Texture; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import org.joml.Vector2f; -import org.lwjgl.glfw.GLFW; -import org.lwjgl.glfw.GLFWErrorCallback; -import org.lwjgl.glfw.GLFWVidMode; -import org.lwjgl.opengl.GL; -import org.lwjgl.system.MemoryUtil; - -import java.awt.*; -import java.nio.ByteBuffer; -import java.util.Random; - -/** - * ModelRenderLightingTest - * 测试使用 Model2D + 光源进行简单光照渲染 - * - * @author tzdwindows 7 - */ -public class ModelRenderLightingTest { - - private static final int WINDOW_WIDTH = 800; - private static final int WINDOW_HEIGHT = 600; - private static final String WINDOW_TITLE = "Vivid2D ModelRender Lighting Test"; - - private long window; - private boolean running = true; - - private Model2D model; - private final Random random = new Random(); - - private float animationTime = 0f; - - public static void main(String[] args) { - new ModelRenderLightingTest().run(); - } - - public void run() { - try { - init(); - loop(); - } finally { - cleanup(); - } - } - - private void init() { - GLFWErrorCallback.createPrint(System.err).set(); - if (!GLFW.glfwInit()) throw new IllegalStateException("Unable to initialize GLFW"); - - GLFW.glfwDefaultWindowHints(); - GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); - GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE); - - window = GLFW.glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE, MemoryUtil.NULL, MemoryUtil.NULL); - if (window == MemoryUtil.NULL) throw new RuntimeException("Failed to create GLFW window"); - - GLFWVidMode vidMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor()); - GLFW.glfwSetWindowPos(window, (vidMode.width() - WINDOW_WIDTH) / 2, (vidMode.height() - WINDOW_HEIGHT) / 2); - - GLFW.glfwSetKeyCallback(window, (wnd, key, scancode, action, mods) -> { - if (key == GLFW.GLFW_KEY_ESCAPE && action == GLFW.GLFW_RELEASE) running = false; - }); - - GLFW.glfwSetWindowSizeCallback(window, (wnd, w, h) -> ModelRender.setViewport(w, h)); - - GLFW.glfwMakeContextCurrent(window); - GLFW.glfwSwapInterval(1); - GLFW.glfwShowWindow(window); - - GL.createCapabilities(); - - ModelRender.initialize(); - createModelWithLighting(); - - System.out.println("Lighting Test initialized"); - } - - private void createModelWithLighting() { - model = new Model2D("HumanoidLighting"); - - // 创建根部件 body - ModelPart body = model.createPart("body"); - body.setPosition(0, 0); - Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 80, 120); - bodyMesh.setTexture(createSolidTexture(64, 128, 0xFF4A6AFF)); - body.addMesh(bodyMesh); - - // head - ModelPart head = model.createPart("head"); - head.setPosition(0, -90); - Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 60, 60); - headMesh.setTexture(createHeadTexture()); - head.addMesh(headMesh); - - // arms - ModelPart leftArm = model.createPart("left_arm"); - leftArm.setPosition(-60, -20); - Mesh2D leftArmMesh = Mesh2D.createQuad("left_arm_mesh", 18, 90); - leftArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED)); - leftArm.addMesh(leftArmMesh); - - ModelPart rightArm = model.createPart("right_arm"); - rightArm.setPosition(60, -20); - Mesh2D rightArmMesh = Mesh2D.createQuad("right_arm_mesh", 18, 90); - rightArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED)); - rightArmMesh.setSelected(true); - rightArm.addMesh(rightArmMesh); - - // legs - ModelPart leftLeg = model.createPart("left_leg"); - leftLeg.setPosition(-20, 90); - Mesh2D leftLegMesh = Mesh2D.createQuad("left_leg_mesh", 20, 100); - leftLegMesh.setTexture(createSolidTexture(20, 100, 0xFF4169E1)); - leftLeg.addMesh(leftLegMesh); - - ModelPart rightLeg = model.createPart("right_leg"); - rightLeg.setPosition(20, 90); - Mesh2D rightLegMesh = Mesh2D.createQuad("right_leg_mesh", 20, 100); - rightLegMesh.setTexture(createSolidTexture(20, 100, 0xFF4169E1)); - rightLeg.addMesh(rightLegMesh); - - // 层级关系 - body.addChild(head); - body.addChild(leftArm); - body.addChild(rightArm); - body.addChild(leftLeg); - body.addChild(rightLeg); - - LightSource ambientLight = new LightSource( - Color.lightGray, - 0.3f - ); - ambientLight.setAmbient(true); - model.addLight(ambientLight); - - // 添加光源 - model.addLight(new LightSource(new Vector2f(-100, -100), Color.lightGray, 200f)); - model.addLight(new LightSource(new Vector2f(150, 150), new Color(1f, 1f, 1f), 200f)); - } - - private Texture createSolidTexture(int w, int h, int rgba) { - ByteBuffer buf = MemoryUtil.memAlloc(w * h * 4); - byte a = (byte) ((rgba >> 24) & 0xFF); - byte r = (byte) ((rgba >> 16) & 0xFF); - byte g = (byte) ((rgba >> 8) & 0xFF); - byte b = (byte) (rgba & 0xFF); - for (int i = 0; i < w * h; i++) buf.put(r).put(g).put(b).put(a); - buf.flip(); - Texture t = new Texture("solid_" + rgba, w, h, Texture.TextureFormat.RGBA, buf); - MemoryUtil.memFree(buf); - return t; - } - - private Texture createHeadTexture() { - int width = 64, height = 64; - int[] pixels = new int[width * height]; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - float dx = (x - width / 2f) / (width / 2f); - float dy = (y - height / 2f) / (height / 2f); - float dist = (float) Math.sqrt(dx * dx + dy * dy); - int alpha = dist > 1.0f ? 0 : 255; - int r = (int) (240 * (1f - dist * 0.25f)); - int g = (int) (200 * (1f - dist * 0.25f)); - int b = (int) (180 * (1f - dist * 0.25f)); - pixels[y * width + x] = (alpha << 24) | (r << 16) | (g << 8) | b; - } - } - return new Texture("head_tex", width, height, Texture.TextureFormat.RGBA, pixels); - } - - private void loop() { - long last = System.nanoTime(); - double nsPerUpdate = 1_000_000_000.0 / 60.0; - double accumulator = 0.0; - - while (running && !GLFW.glfwWindowShouldClose(window)) { - long now = System.nanoTime(); - accumulator += (now - last) / nsPerUpdate; - last = now; - - while (accumulator >= 1.0) { - update(1.0f / 60.0f); - accumulator -= 1.0; - } - - render(); - GLFW.glfwSwapBuffers(window); - GLFW.glfwPollEvents(); - } - } - - private void update(float dt) { - animationTime += dt; - float armSwing = (float) Math.sin(animationTime * 3f) * 0.7f; - float legSwing = (float) Math.sin(animationTime * 3f + Math.PI) * 0.6f; - float headRot = (float) Math.sin(animationTime * 1.4f) * 0.15f; - - ModelPart leftArm = model.getPart("left_arm"); - ModelPart rightArm = model.getPart("right_arm"); - ModelPart leftLeg = model.getPart("left_leg"); - ModelPart rightLeg = model.getPart("right_leg"); - ModelPart head = model.getPart("head"); - - if (leftArm != null) leftArm.setRotation(-0.8f * armSwing - 0.2f); - if (rightArm != null) rightArm.setRotation(0.8f * armSwing + 0.2f); - if (leftLeg != null) leftLeg.setRotation(0.6f * legSwing); - if (rightLeg != null) rightLeg.setRotation(-0.6f * legSwing); - if (head != null) head.setRotation(headRot); - - model.update(dt); - } - - private void render() { - RenderSystem.setClearColor(0.18f, 0.18f, 0.25f, 1.0f); - ModelRender.render(1f / 60f, model); - } - - private void cleanup() { - ModelRender.cleanup(); - Texture.cleanupAll(); - if (window != MemoryUtil.NULL) GLFW.glfwDestroyWindow(window); - GLFW.glfwTerminate(); - GLFW.glfwSetErrorCallback(null).free(); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java deleted file mode 100644 index 855e9e4..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java +++ /dev/null @@ -1,296 +0,0 @@ -package com.chuangzhou.vivid2D.test; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; -import com.chuangzhou.vivid2D.render.model.util.Texture; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import org.joml.Vector2f; -import org.lwjgl.glfw.GLFW; -import org.lwjgl.glfw.GLFWErrorCallback; -import org.lwjgl.glfw.GLFWVidMode; -import org.lwjgl.opengl.GL; -import org.lwjgl.system.MemoryUtil; - -import java.nio.ByteBuffer; -import java.util.Random; - -/** - * 重写后的 ModelRender 测试示例:构造一个简单的人形(头、身体、左右手、左右腿) - * 便于验证层级变换与渲染是否正确。 - * - * @author tzdwindows 7 - */ -public class ModelRenderTest { - - private static final int WINDOW_WIDTH = 800; - private static final int WINDOW_HEIGHT = 600; - private static final String WINDOW_TITLE = "Vivid2D ModelRender Test - Humanoid"; - - private long window; - private boolean running = true; - - private Model2D testModel; - private final Random random = new Random(); - - private float animationTime = 0f; - private boolean animate = true; - - public static void main(String[] args) { - new ModelRenderTest().run(); - } - - public void run() { - try { - init(); - loop(); - } catch (Throwable t) { - t.printStackTrace(); - } finally { - cleanup(); - } - } - - private void init() { - GLFWErrorCallback.createPrint(System.err).set(); - - if (!GLFW.glfwInit()) { - throw new IllegalStateException("Unable to initialize GLFW"); - } - - GLFW.glfwDefaultWindowHints(); - GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); - GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE); - - window = GLFW.glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE, MemoryUtil.NULL, MemoryUtil.NULL); - if (window == MemoryUtil.NULL) throw new RuntimeException("Failed to create GLFW window"); - - GLFWVidMode vidMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor()); - GLFW.glfwSetWindowPos(window, - (vidMode.width() - WINDOW_WIDTH) / 2, - (vidMode.height() - WINDOW_HEIGHT) / 2); - - GLFW.glfwSetKeyCallback(window, (wnd, key, scancode, action, mods) -> { - if (key == GLFW.GLFW_KEY_ESCAPE && action == GLFW.GLFW_RELEASE) running = false; - if (key == GLFW.GLFW_KEY_SPACE && action == GLFW.GLFW_RELEASE) { - animate = !animate; - System.out.println("Animation " + (animate ? "enabled" : "disabled")); - } - if (key == GLFW.GLFW_KEY_R && action == GLFW.GLFW_RELEASE) randomizeModel(); - }); - - GLFW.glfwSetWindowSizeCallback(window, (wnd, w, h) -> ModelRender.setViewport(w, h)); - - GLFW.glfwMakeContextCurrent(window); - GLFW.glfwSwapInterval(1); - GLFW.glfwShowWindow(window); - - GL.createCapabilities(); - - createTestModel(); - ModelRender.initialize(); - - System.out.println("Test initialized successfully"); - System.out.println("Controls: ESC exit | SPACE toggle anim | R randomize"); - } - - /** - * 构造一个简单的人形:body 为根,head、arms、legs 为 body 的子节点。 - * 使用 createPart 保证与 Model2D 管理一致。 - */ - private void createTestModel() { - testModel = new Model2D("Humanoid"); - - PhysicsSystem physics = testModel.getPhysics(); - physics.setGravity(new Vector2f(0, -98.0f)); - physics.setAirResistance(0.05f); - physics.setTimeScale(1.0f); - physics.setEnabled(true); - physics.initialize(); - - // body 放在屏幕中心 - ModelPart body = testModel.createPart("body"); - body.setPosition(0, 0); - // 身体网格:宽 80 高 120 - Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 80, 120); - bodyMesh.setTexture(createSolidTexture(64, 128, 0xFF4A6AFF)); // 蓝衣 - body.addMesh(bodyMesh); - - // head:相对于 body 在上方偏移 - ModelPart head = testModel.createPart("head"); - head.setPosition(0, -90); // 注意:如果 body 的坐标是屏幕位置,子部件的 position 是相对父节点(取决于你的实现);这里按常见习惯设负 y 向上 - Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 60, 60); - headMesh.setTexture(createHeadTexture()); - head.addMesh(headMesh); - - // left arm - ModelPart leftArm = testModel.createPart("left_arm"); - leftArm.setPosition(-60, -20); // 在 body 左侧稍上位置 - Mesh2D leftArmMesh = Mesh2D.createQuad("left_arm_mesh", 18, 90); - leftArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED)); // 手臂颜色 - leftArm.addMesh(leftArmMesh); - - // right arm - ModelPart rightArm = testModel.createPart("right_arm"); - rightArm.setPosition(60, -20); - Mesh2D rightArmMesh = Mesh2D.createQuad("right_arm_mesh", 18, 90); - rightArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED)); - rightArm.addMesh(rightArmMesh); - - // left leg - ModelPart leftLeg = testModel.createPart("left_leg"); - leftLeg.setPosition(-20, 90); // body 下方 - Mesh2D leftLegMesh = Mesh2D.createQuad("left_leg_mesh", 20, 100); - leftLegMesh.setTexture(createSolidTexture(20, 100, 0xFF4169E1)); - leftLeg.addMesh(leftLegMesh); - - // right leg - ModelPart rightLeg = testModel.createPart("right_leg"); - rightLeg.setPosition(20, 90); - Mesh2D rightLegMesh = Mesh2D.createQuad("right_leg_mesh", 20, 100); - rightLegMesh.setTexture(createSolidTexture(20, 100, 0xFF4169E1)); - rightLeg.addMesh(rightLegMesh); - - // 建立层级:body 为根,其他作为 body 的子节点 - //testModel.addPart(body); - - body.addChild(head); - body.addChild(leftArm); - body.addChild(rightArm); - body.addChild(leftLeg); - body.addChild(rightLeg); - - // 创建动画参数用于简单摆动 - testModel.createParameter("arm_swing", -1.0f, 1.0f, 0f); - testModel.createParameter("leg_swing", -1.0f, 1.0f, 0f); - testModel.createParameter("head_rotation", -0.5f, 0.5f, 0f); - - System.out.println("Humanoid model created with parts: " + testModel.getParts().size()); - } - - // 辅助:创建身体渐变/纯色纹理(ByteBuffer RGBA) - private Texture createSolidTexture(int w, int h, int rgba) { - ByteBuffer buf = MemoryUtil.memAlloc(w * h * 4); - byte a = (byte) ((rgba >> 24) & 0xFF); - byte r = (byte) ((rgba >> 16) & 0xFF); - byte g = (byte) ((rgba >> 8) & 0xFF); - byte b = (byte) (rgba & 0xFF); - for (int i = 0; i < w * h; i++) { - buf.put(r).put(g).put(b).put(a); - } - buf.flip(); - Texture t = new Texture("solid_" + rgba + "_" + w + "x" + h, w, h, Texture.TextureFormat.RGBA, buf); - MemoryUtil.memFree(buf); - return t; - } - - private Texture createHeadTexture() { - int width = 64, height = 64; - int[] pixels = new int[width * height]; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - float dx = (x - width / 2f) / (width / 2f); - float dy = (y - height / 2f) / (height / 2f); - float dist = (float) Math.sqrt(dx * dx + dy * dy); - int alpha = dist > 1.0f ? 0 : 255; - int r = (int) (240 * (1.0f - dist * 0.25f)); - int g = (int) (200 * (1.0f - dist * 0.25f)); - int b = (int) (180 * (1.0f - dist * 0.25f)); - pixels[y * width + x] = (alpha << 24) | (r << 16) | (g << 8) | b; - } - } - return new Texture("head_tex", width, height, Texture.TextureFormat.RGBA, pixels); - } - - private void loop() { - long last = System.nanoTime(); - double nsPerUpdate = 1_000_000_000.0 / 60.0; - double accumulator = 0.0; - - System.out.println("Entering main loop..."); - - while (running && !GLFW.glfwWindowShouldClose(window)) { - long now = System.nanoTime(); - accumulator += (now - last) / nsPerUpdate; - last = now; - - while (accumulator >= 1.0) { - update(1.0f / 60.0f); - accumulator -= 1.0; - } - - render(); - - GLFW.glfwSwapBuffers(window); - GLFW.glfwPollEvents(); - } - } - - private void update(float dt) { - if (!animate) return; - - animationTime += dt; - float armSwing = (float) Math.sin(animationTime * 3.0f) * 0.7f; // -0.7 .. 0.7 - float legSwing = (float) Math.sin(animationTime * 3.0f + Math.PI) * 0.6f; - float headRot = (float) Math.sin(animationTime * 1.4f) * 0.15f; - - testModel.setParameterValue("arm_swing", armSwing); - testModel.setParameterValue("leg_swing", legSwing); - testModel.setParameterValue("head_rotation", headRot); - - // 将参数应用到部件(直接通过 API 设置即可) - ModelPart leftArm = testModel.getPart("left_arm"); - ModelPart rightArm = testModel.getPart("right_arm"); - ModelPart leftLeg = testModel.getPart("left_leg"); - ModelPart rightLeg = testModel.getPart("right_leg"); - ModelPart head = testModel.getPart("head"); - - if (leftArm != null) leftArm.setRotation(-0.8f * armSwing - 0.2f); - if (rightArm != null) rightArm.setRotation(0.8f * armSwing + 0.2f); - if (leftLeg != null) leftLeg.setRotation(0.6f * legSwing); - if (rightLeg != null) rightLeg.setRotation(-0.6f * legSwing); - if (head != null) head.setRotation(headRot); - - testModel.update(dt); - } - - private void render() { - RenderSystem.setClearColor(0.18f, 0.18f, 0.25f, 1.0f); - ModelRender.render(1.0f / 60.0f, testModel); - - // 每 5 秒输出一次统计 - if ((int) (animationTime) % 5 == 0 && (animationTime - (int) animationTime) < 0.016) { - //System.out.println("Render stats: meshes=" + ModelRender.getRenderStats()); - } - } - - private void randomizeModel() { - System.out.println("Randomizing model..."); - ModelPart body = testModel.getPart("body"); - if (body != null) { - body.setPosition(200 + random.nextInt(400), 200 + random.nextInt(200)); - } - for (ModelPart p : testModel.getParts()) { - p.setRotation((float) (random.nextFloat() * Math.PI * 2)); - if (p.getName().equals("head")) { - p.setOpacity(0.6f + random.nextFloat() * 0.4f); - } - } - } - - private void cleanup() { - System.out.println("Cleaning up resources..."); - ModelRender.cleanup(); - Texture.cleanupAll(); - if (window != MemoryUtil.NULL) GLFW.glfwDestroyWindow(window); - GLFW.glfwTerminate(); - GLFW.glfwSetErrorCallback(null).free(); - System.out.println("Test completed"); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest2.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest2.java deleted file mode 100644 index 7672c8e..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest2.java +++ /dev/null @@ -1,231 +0,0 @@ -package com.chuangzhou.vivid2D.test; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.Texture; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import org.lwjgl.glfw.GLFW; -import org.lwjgl.glfw.GLFWErrorCallback; -import org.lwjgl.glfw.GLFWVidMode; -import org.lwjgl.opengl.GL; -import org.lwjgl.system.MemoryUtil; - -import java.nio.ByteBuffer; - -/** - * 用于测试中心点旋转 - * - * @author tzdwindows 7 - */ -public class ModelRenderTest2 { - - private static final int WINDOW_WIDTH = 800; - private static final int WINDOW_HEIGHT = 600; - private static final String WINDOW_TITLE = "Simple Pivot Test"; - - private long window; - private boolean running = true; - private Model2D testModel; - private float rotationAngle = 0f; - private int testCase = 0; - private Mesh2D squareMesh; - - public static void main(String[] args) { - new ModelRenderTest2().run(); - } - - public void run() { - try { - init(); - loop(); - } catch (Throwable t) { - t.printStackTrace(); - } finally { - cleanup(); - } - } - - private void init() { - GLFWErrorCallback.createPrint(System.err).set(); - - if (!GLFW.glfwInit()) { - throw new IllegalStateException("Unable to initialize GLFW"); - } - - GLFW.glfwDefaultWindowHints(); - GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); - GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE); - - window = GLFW.glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE, MemoryUtil.NULL, MemoryUtil.NULL); - if (window == MemoryUtil.NULL) throw new RuntimeException("Failed to create GLFW window"); - - GLFWVidMode vidMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor()); - GLFW.glfwSetWindowPos(window, - (vidMode.width() - WINDOW_WIDTH) / 2, - (vidMode.height() - WINDOW_HEIGHT) / 2); - - GLFW.glfwSetKeyCallback(window, (wnd, key, scancode, action, mods) -> { - if (key == GLFW.GLFW_KEY_ESCAPE && action == GLFW.GLFW_RELEASE) running = false; - if (key == GLFW.GLFW_KEY_SPACE && action == GLFW.GLFW_RELEASE) { - testCase = (testCase + 1) % 3; - updatePivotPoint(); - } - if (key == GLFW.GLFW_KEY_R && action == GLFW.GLFW_RELEASE) { - resetPosition(); - } - }); - - GLFW.glfwSetWindowSizeCallback(window, (wnd, w, h) -> ModelRender.setViewport(w, h)); - - GLFW.glfwMakeContextCurrent(window); - GLFW.glfwSwapInterval(1); - GLFW.glfwShowWindow(window); - - GL.createCapabilities(); - - createSimpleTestModel(); - ModelRender.initialize(); - - System.out.println("Simple pivot test initialized"); - System.out.println("Controls: ESC = exit | SPACE = change pivot | R = reset position"); - } - - /** - * Create a single square mesh with vertices centered at origin - */ - private void createSimpleTestModel() { - testModel = new Model2D("SimpleTest"); - - ModelPart square = testModel.createPart("square"); - square.setPosition(0, 0); // center of window - square.setPivot(0, 0); - // Create 80x80 quad centered at origin - squareMesh = Mesh2D.createQuad("square_mesh", 80, 80); - // Shift vertices so center is at (0,0) - //for (int i = 0; i < squareMesh.getVertexCount(); i++) { - // Vector2f v = squareMesh.getVertex(i); - // v.sub(0, 0); - // squareMesh.setVertex(i, v.x, v.y); - //} - - squareMesh.setTexture(createDiagnosticTexture()); - square.addMesh(squareMesh); // do NOT bake to world coordinates - - System.out.println("Simple test model created with one part only"); - } - - /** - * Create a diagnostic texture to see pivot visually - */ - private Texture createDiagnosticTexture() { - int width = 80, height = 80; - ByteBuffer buf = MemoryUtil.memAlloc(width * height * 4); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - // center cross - if (x == width / 2 || y == height / 2) { - buf.put((byte) 255).put((byte) 255).put((byte) 255).put((byte) 255); - } else if (x < width / 2 && y < height / 2) { - buf.put((byte) 255).put((byte) 0).put((byte) 0).put((byte) 255); // top-left red - } else if (x >= width / 2 && y < height / 2) { - buf.put((byte) 0).put((byte) 255).put((byte) 0).put((byte) 255); // top-right green - } else if (x < width / 2 && y >= height / 2) { - buf.put((byte) 0).put((byte) 0).put((byte) 255).put((byte) 255); // bottom-left blue - } else { - buf.put((byte) 255).put((byte) 255).put((byte) 0).put((byte) 255); // bottom-right yellow - } - } - } - - buf.flip(); - Texture texture = new Texture("diagnostic", width, height, Texture.TextureFormat.RGBA, buf); - MemoryUtil.memFree(buf); - return texture; - } - - private void updatePivotPoint() { - ModelPart square = testModel.getPart("square"); - if (square != null) { - switch (testCase) { - case 0: - square.setPivot(0, 0); - System.out.println("Pivot: center (0,0) - should rotate around center"); - break; - case 1: - square.setPivot(-40, 40); // top-left corner - System.out.println("Pivot: top-left (-40,40) - should rotate around top-left"); - break; - case 2: - square.setPivot(40, -40); // bottom-right corner - System.out.println("Pivot: bottom-right (40,-40) - should rotate around bottom-right"); - break; - } - } - } - - - private void resetPosition() { - ModelPart square = testModel.getPart("square"); - if (square != null) { - square.setPosition(400, 300); - square.setRotation(0); - rotationAngle = 0; - System.out.println("Position reset"); - } - } - - private void loop() { - long last = System.nanoTime(); - double nsPerUpdate = 1_000_000_000.0 / 60.0; - double accumulator = 0.0; - - while (running && !GLFW.glfwWindowShouldClose(window)) { - long now = System.nanoTime(); - accumulator += (now - last) / nsPerUpdate; - last = now; - - while (accumulator >= 1.0) { - update(1.0f / 60.0f); - accumulator -= 1.0; - } - - render(); - - GLFW.glfwSwapBuffers(window); - GLFW.glfwPollEvents(); - } - } - - private void update(float dt) { - rotationAngle += dt * 1.5f; - - ModelPart square = testModel.getPart("square"); - if (square != null) { - square.setRotation(rotationAngle); - } - - testModel.update(dt); - } - - private void render() { - RenderSystem.setClearColor(0.2f, 0.2f, 0.3f, 1.0f); - ModelRender.render(1.0f / 60.0f, testModel); - } - - private void cleanup() { - System.out.println("Cleaning up resources..."); - ModelRender.cleanup(); - Texture.cleanupAll(); - if (window != MemoryUtil.NULL) GLFW.glfwDestroyWindow(window); - GLFW.glfwTerminate(); - GLFW.glfwSetErrorCallback(null).free(); - System.out.println("Test finished"); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTextureTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTextureTest.java deleted file mode 100644 index 306101b..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTextureTest.java +++ /dev/null @@ -1,353 +0,0 @@ -package com.chuangzhou.vivid2D.test; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.Texture; -import org.lwjgl.glfw.GLFWVidMode; -import org.lwjgl.opengl.GL; -import org.lwjgl.system.MemoryUtil; - -import static org.lwjgl.glfw.GLFW.*; -import static org.lwjgl.opengl.GL11.*; - -/** - * Texture Render Test Class - Debug Version - * - * @author tzdwindows 7 - */ -public class ModelRenderTextureTest { - - private long window; - private Model2D model; - - // Window dimensions - private static final int WINDOW_WIDTH = 800; - private static final int WINDOW_HEIGHT = 600; - - // Debug flags - private int frameCount = 0; - private double lastFpsTime = 0; - private boolean enableWireframe = false; - - public static void main(String[] args) { - new ModelRenderTextureTest().run(); - } - - public void run() { - try { - init(); - loop(); - } catch (Exception e) { - e.printStackTrace(); - } finally { - cleanup(); - } - } - - private void init() { - // Initialize GLFW - if (!glfwInit()) { - throw new IllegalStateException("Unable to initialize GLFW"); - } - - // Configure GLFW - glfwDefaultWindowHints(); - glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); - glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - - // Create window - window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "ModelRender Texture Test - DEBUG", MemoryUtil.NULL, MemoryUtil.NULL); - if (window == MemoryUtil.NULL) { - throw new RuntimeException("Failed to create the GLFW window"); - } - - // Center window - GLFWVidMode vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor()); - glfwSetWindowPos( - window, - (vidMode.width() - WINDOW_WIDTH) / 2, - (vidMode.height() - WINDOW_HEIGHT) / 2 - ); - - // Set callbacks - glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> { - if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) { - glfwSetWindowShouldClose(window, true); - } - if (key == GLFW_KEY_SPACE && action == GLFW_RELEASE) { - enableWireframe = !enableWireframe; - System.out.println("Wireframe mode: " + (enableWireframe ? "ON" : "OFF")); - } - if (key == GLFW_KEY_R && action == GLFW_RELEASE) { - recreateModel(); - } - }); - - // Create OpenGL context - glfwMakeContextCurrent(window); - GL.createCapabilities(); - - // Show window - glfwShowWindow(window); - - // Initialize ModelRender - ModelRender.initialize(); - - // Create test model - createTestModel(); - - System.out.println("=== DEBUG INFO ==="); - System.out.println("Press SPACE to toggle wireframe mode"); - System.out.println("Press R to recreate model"); - System.out.println("Press ESC to exit"); - System.out.println("=================="); - } - - private void createTestModel() { - if (model != null) { - // Clean up previous model if exists - System.out.println("Cleaning up previous model..."); - } - - model = new Model2D("TextureTestModel"); - - try { - // Load Trump image texture - String texturePath = "G:\\鬼畜素材\\川普\\图片\\7(DJ0MH9}`)GJYHHADQDHYN.png"; - System.out.println("Loading texture: " + texturePath); - - Texture texture = Texture.createFromFile("trump_texture", texturePath); - model.addTexture(texture); - - System.out.println("Texture loaded: " + texture.getWidth() + "x" + texture.getHeight()); - System.out.println("Texture ID: " + texture.getTextureId()); - System.out.println("Texture format: " + texture.getFormat()); - - // 使用与工作示例相同的方式创建网格 - // 根据纹理尺寸创建合适大小的四边形 - float width = texture.getWidth() / 2.0f; // 缩小以适应屏幕 - float height = texture.getHeight() / 2.0f; - - // 使用 Mesh2D.createQuad 方法创建网格(如果可用) - Mesh2D mesh; - try { - // 尝试使用 createQuad 方法 - mesh = Mesh2D.createQuad("trump_mesh", width, height); - System.out.println("Using Mesh2D.createQuad method"); - } catch (Exception e) { - // 回退到手动创建顶点 - System.out.println("Using manual vertex creation"); - float[] vertices = { - -width / 2, -height / 2, // bottom left - width / 2, -height / 2, // bottom right - width / 2, height / 2, // top right - -width / 2, height / 2 // top left - }; - - float[] uvs = { - 0.0f, 1.0f, // bottom left - 1.0f, 1.0f, // bottom right - 1.0f, 0.0f, // top right - 0.0f, 0.0f // top left - }; - - int[] indices = { - 0, 1, 2, // first triangle - 2, 3, 0 // second triangle - }; - - mesh = model.createMesh("trump_mesh", vertices, uvs, indices); - } - - mesh.setTexture(texture); - - // Create part and add mesh - ModelPart part = model.createPart("trump_part"); - - part.addMesh(mesh); - part.setVisible(true); - - // 设置部件位置到屏幕中心(使用像素坐标) - part.setPosition(0, 0); // 800x600 窗口的中心 - - System.out.println("Model created:"); - System.out.println(" - Part count: " + model.getParts().size()); - System.out.println(" - Mesh count: " + model.getMeshes().size()); - System.out.println(" - Mesh dimensions: " + width + "x" + height); - System.out.println(" - Part position: " + part.getPosition()); - System.out.println(" - Mesh vertex count: " + mesh.getVertexCount()); - // 测试模型保存 - model.saveToFile("C:\\Users\\Administrator\\Desktop\\trump_texture.model"); - } catch (Exception e) { - System.err.println("Failed to create test model: " + e.getMessage()); - e.printStackTrace(); - createFallbackModel(); - } - } - - private void createFallbackModel() { - System.out.println("Creating fallback checkerboard model..."); - - // 使用与工作示例相同的模式 - float width = 200; - float height = 200; - - Mesh2D mesh; - try { - mesh = Mesh2D.createQuad("fallback_mesh", width, height); - } catch (Exception e) { - // 手动创建 - float[] vertices = { - -width / 2, -height / 2, - width / 2, -height / 2, - width / 2, height / 2, - -width / 2, height / 2 - }; - - float[] uvs = { - 0.0f, 1.0f, - 1.0f, 1.0f, - 1.0f, 0.0f, - 0.0f, 0.0f - }; - - int[] indices = { - 0, 1, 2, - 2, 3, 0 - }; - - mesh = model.createMesh("fallback_mesh", vertices, uvs, indices); - } - - // Create checkerboard texture - Texture fallbackTexture = Texture.createCheckerboard( - "fallback_texture", - 512, 512, 32, - 0xFFFF0000, // red - 0xFF0000FF // blue - ); - model.addTexture(fallbackTexture); - mesh.setTexture(fallbackTexture); - - ModelPart part = model.createPart("fallback_part"); - part.addMesh(mesh); - part.setVisible(true); - part.setPosition(400, 300); - - System.out.println("Fallback model created with size: " + width + "x" + height); - } - - private void recreateModel() { - System.out.println("Recreating model..."); - createTestModel(); - } - - private void loop() { - // Set clear color (light gray for better visibility) - glClearColor(0.3f, 0.3f, 0.3f, 1.0f); - - double lastTime = glfwGetTime(); - - while (!glfwWindowShouldClose(window)) { - double currentTime = glfwGetTime(); - float deltaTime = (float) (currentTime - lastTime); - lastTime = currentTime; - - // Calculate FPS - frameCount++; - if (currentTime - lastFpsTime >= 1.0) { - System.out.printf("FPS: %d, Delta: %.3fms\n", frameCount, deltaTime * 1000); - frameCount = 0; - lastFpsTime = currentTime; - } - - // Render - render(deltaTime); - - // Swap buffers and poll events - glfwSwapBuffers(window); - glfwPollEvents(); - } - } - - private void render(float deltaTime) { - // Clear screen - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - // Set wireframe mode if enabled - if (enableWireframe) { - glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); - } else { - glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - } - - // Print debug info occasionally - if (frameCount % 120 == 0) { - System.out.println("=== RENDER DEBUG ==="); - System.out.println("Model null: " + (model == null)); - if (model != null) { - System.out.println("Parts: " + model.getParts().size()); - System.out.println("Meshes: " + model.getMeshes().size()); - if (!model.getParts().isEmpty()) { - ModelPart part = model.getParts().get(0); - System.out.println("First part visible: " + part.isVisible()); - System.out.println("First part position: " + part.getPosition()); - System.out.println("First part meshes: " + part.getMeshes().size()); - } - } - System.out.println("==================="); - } - - // Render using ModelRender - try { - ModelRender.render(deltaTime, model); - } catch (Exception e) { - System.err.println("Rendering error: " + e.getMessage()); - e.printStackTrace(); - } - - // Check OpenGL errors - int error = glGetError(); - if (error != GL_NO_ERROR) { - System.err.println("OpenGL error: " + getGLErrorString(error)); - } - } - - private String getGLErrorString(int error) { - switch (error) { - case GL_INVALID_ENUM: - return "GL_INVALID_ENUM"; - case GL_INVALID_VALUE: - return "GL_INVALID_VALUE"; - case GL_INVALID_OPERATION: - return "GL_INVALID_OPERATION"; - case GL_OUT_OF_MEMORY: - return "GL_OUT_OF_MEMORY"; - default: - return "Unknown Error (0x" + Integer.toHexString(error) + ")"; - } - } - - private void cleanup() { - System.out.println("Cleaning up resources..."); - - // Cleanup ModelRender - ModelRender.cleanup(); - - // Cleanup texture cache - Texture.cleanupAll(); - - // Destroy window and terminate GLFW - if (window != MemoryUtil.NULL) { - glfwDestroyWindow(window); - } - glfwTerminate(); - - System.out.println("Cleanup completed"); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java deleted file mode 100644 index 1cf879a..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java +++ /dev/null @@ -1,825 +0,0 @@ -package com.chuangzhou.vivid2D.test; - -import com.chuangzhou.vivid2D.render.model.AnimationParameter; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.transform.WaveDeformer; -import com.chuangzhou.vivid2D.render.model.util.*; -import org.joml.Vector2f; -import org.lwjgl.glfw.GLFW; -import org.lwjgl.glfw.GLFWErrorCallback; -import org.lwjgl.opengl.GL; -import org.lwjgl.system.MemoryUtil; - -/** - * 用于测试Model2D模型的保存和加载功能 - * - * @author tzdwindows 7 - */ -public class ModelTest { - - private static long window; - private static boolean glInitialized = false; - - public static void main(String[] args) { - System.out.println("=== Model2D Extended Save and Load Test Start ==="); - - try { - // Initialize OpenGL context for texture testing - initializeOpenGL(); - - // Test 1: Create model and save (with texture) - testCreateAndSaveModelWithTexture(); - - // Test 2: Load model and verify data including textures - testLoadAndVerifyModelWithTexture(); - - // Test 3: Test compressed file operations with textures - testCompressedFileOperationsWithTexture(); - - //testModelSaveLoadIntegrity(model, "test_model.vmdl") - - // Other existing tests... - //testAnimationSystem(); - //testPhysicsSystem(); - //testComplexTransformations(); - //testPerformance(); - //Model2D model = createTestModel(); - //printModelState(model); - } finally { - // Cleanup OpenGL - cleanupOpenGL(); - } - - System.out.println("=== Model2D Extended Save and Load Test Complete ==="); - } - - public static Model2D createTestModel() { - Model2D model = new Model2D("full_test_model"); - model.setVersion("1.0.0"); - - // ==================== 创建部件层级 ==================== - ModelPart root = model.createPart("root"); - ModelPart body = model.createPart("body"); - ModelPart head = model.createPart("head"); - ModelPart leftArm = model.createPart("left_arm"); - ModelPart rightArm = model.createPart("right_arm"); - - root.addChild(body); - body.addChild(head); - body.addChild(leftArm); - body.addChild(rightArm); - - // ==================== 设置本地变换 ==================== - root.setPosition(0, 0); - root.setRotation(0f); - root.setScale(1f, 1f); - - body.setPosition(0, -50); - body.setRotation(10f); // body稍微旋转 - body.setScale(1.1f, 1.0f); - - head.setPosition(0, -50); - head.setRotation(-5f); - head.setScale(1.0f, 1.0f); - - leftArm.setPosition(-30, -20); - leftArm.setRotation(20f); - leftArm.setScale(1.0f, 0.9f); - - rightArm.setPosition(30, -20); - rightArm.setRotation(-20f); - rightArm.setScale(1.0f, 0.9f); - - // ==================== 添加网格 ==================== - Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 40, 80); - Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 50, 50); - Mesh2D leftArmMesh = Mesh2D.createQuad("left_arm_mesh", 15, 50); - Mesh2D rightArmMesh = Mesh2D.createQuad("right_arm_mesh", 15, 50); - - model.addMesh(bodyMesh); - model.addMesh(headMesh); - model.addMesh(leftArmMesh); - model.addMesh(rightArmMesh); - - body.addMesh(bodyMesh); - head.addMesh(headMesh); - leftArm.addMesh(leftArmMesh); - rightArm.addMesh(rightArmMesh); - - // ==================== 添加纹理 ==================== - Texture bodyTex = Texture.createSolidColor("body_tex", 64, 64, 0xFFFF0000); - Texture headTex = Texture.createSolidColor("head_tex", 64, 64, 0xFF00FF00); - Texture armTex = Texture.createSolidColor("arm_tex", 32, 64, 0xFF0000FF); - - bodyTex.ensurePixelDataCached(); - headTex.ensurePixelDataCached(); - armTex.ensurePixelDataCached(); - - model.addTexture(bodyTex); - model.addTexture(headTex); - model.addTexture(armTex); - - bodyMesh.setTexture(bodyTex); - headMesh.setTexture(headTex); - leftArmMesh.setTexture(armTex); - rightArmMesh.setTexture(armTex); - - // ==================== 添加动画参数 ==================== - AnimationParameter smileParam = model.createParameter("smile", 0, 1, 0.5f); - AnimationParameter walkParam = model.createParameter("walk", 0, 1, 0); - AnimationParameter waveParam = model.createParameter("wave", 0, 1, 0); - - // ==================== 添加 Deformer ==================== - root.addDeformer(new WaveDeformer("blink")); - root.addDeformer(new WaveDeformer("wave")); - root.addDeformer(new WaveDeformer("blink")); - - // ==================== 设置元数据 ==================== - model.getMetadata().setAuthor("Test Author"); - model.getMetadata().setDescription("This is a full-featured test model with transforms and deformers."); - model.getMetadata().setLicense("MIT"); - model.getMetadata().setFileFormatVersion("1.0.0"); - model.getMetadata().setUnitsPerMeter(100.0f); - model.getMetadata().setProperty("custom_prop1", "value1"); - - // ==================== 添加物理 ==================== - PhysicsSystem physics = model.getPhysics(); - if (physics != null) { - physics.initialize(); - PhysicsSystem.PhysicsParticle p1 = physics.addParticle("p1", new Vector2f(0, 0), 1f); - PhysicsSystem.PhysicsParticle p2 = physics.addParticle("p2", new Vector2f(10, 0), 1f); - physics.addSpring("spring1", p1, p2, 10f, 0.5f, 0.1f); - } - - return model; - } - - - public static void testModelSaveLoadIntegrity(Model2D model, String filePath) { - System.out.println("\n--- Test: Model Save and Load Integrity ---"); - try { - // 保存模型 - model.saveToFile(filePath); - - // 加载模型 - Model2D loaded = Model2D.loadFromFile(filePath); - - boolean integrityOk = true; - - // ==================== 基本属性 ==================== - if (!model.getName().equals(loaded.getName())) { - System.out.println("Name mismatch!"); - integrityOk = false; - } - if (!model.getVersion().equals(loaded.getVersion())) { - System.out.println("Version mismatch!"); - integrityOk = false; - } - - // ==================== 部件 ==================== - if (model.getParts().size() != loaded.getParts().size()) { - System.out.println("Parts count mismatch!"); - integrityOk = false; - } else { - for (int i = 0; i < model.getParts().size(); i++) { - ModelPart orig = model.getParts().get(i); - ModelPart loadPart = loaded.getParts().get(i); - if (!orig.getName().equals(loadPart.getName())) { - System.out.println("Part name mismatch: " + orig.getName()); - integrityOk = false; - } - // 检查变换 - if (!orig.getPosition().equals(loadPart.getPosition()) || - orig.getRotation() != loadPart.getRotation() || - !orig.getScale().equals(loadPart.getScale())) { - System.out.println("Part transform mismatch: " + orig.getName()); - integrityOk = false; - } - // 检查Deformer - if (orig.getDeformers().size() != loadPart.getDeformers().size()) { - System.out.println("Deformer count mismatch on part: " + orig.getName()); - integrityOk = false; - } - } - } - - // ==================== 网格 ==================== - if (model.getMeshes().size() != loaded.getMeshes().size()) { - System.out.println("Meshes count mismatch!"); - integrityOk = false; - } - - // ==================== 纹理 ==================== - if (model.getTextures().size() != loaded.getTextures().size()) { - System.out.println("Textures count mismatch!"); - integrityOk = false; - } - - // ==================== 参数 ==================== - if (model.getParameters().size() != loaded.getParameters().size()) { - System.out.println("Parameters count mismatch!"); - integrityOk = false; - } else { - for (String key : model.getParameters().keySet()) { - AnimationParameter origParam = model.getParameters().get(key); - AnimationParameter loadParam = loaded.getParameters().get(key); - if (origParam.getValue() != loadParam.getValue()) { - System.out.println("Parameter value mismatch: " + key); - integrityOk = false; - } - } - } - - // ==================== 物理 ==================== - PhysicsSystem origPhysics = model.getPhysics(); - PhysicsSystem loadPhysics = loaded.getPhysics(); - if ((origPhysics != null && loadPhysics == null) || (origPhysics == null && loadPhysics != null)) { - System.out.println("Physics system missing after load!"); - integrityOk = false; - } else if (origPhysics != null) { - if (origPhysics.getParticles().size() != loadPhysics.getParticles().size()) { - System.out.println("Physics particle count mismatch!"); - integrityOk = false; - } - if (origPhysics.getSprings().size() != loadPhysics.getSprings().size()) { - System.out.println("Physics spring count mismatch!"); - integrityOk = false; - } - } - - System.out.println("Integrity test " + (integrityOk ? "PASSED" : "FAILED")); - - } catch (Exception e) { - System.err.println("Error in testModelSaveLoadIntegrity: " + e.getMessage()); - e.printStackTrace(); - } - } - - - private static void printModelState(Model2D model) { - System.out.println(" - Name: " + model.getName()); - System.out.println(" - Version: " + model.getVersion()); - System.out.println(" - Parts: " + model.getParts().size()); - for (ModelPart part : model.getParts()) { - printPartHierarchy(part, 1); - } - System.out.println(" - Parameters:"); - for (AnimationParameter param : model.getParameters().values()) { - System.out.println(" * " + param.getId() + " = " + param.getValue()); - } - System.out.println(" - Textures:"); - model.getTextures().forEach((k, tex) -> { - System.out.println(" * " + tex.getName() + " (" + tex.getWidth() + "x" + tex.getHeight() + ")"); - }); - System.out.println(" - User Properties:"); - model.getMetadata().getUserProperties().forEach((k, v) -> - System.out.println(" * " + k + ": " + v) - ); - } - - - /** - * Initialize OpenGL context for texture testing - */ - private static void initializeOpenGL() { - try { - // Setup error callback - GLFWErrorCallback.createPrint(System.err).set(); - - // Initialize GLFW - if (!GLFW.glfwInit()) { - throw new IllegalStateException("Unable to initialize GLFW"); - } - - // Configure GLFW - GLFW.glfwDefaultWindowHints(); - GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); // Hide window - GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE); - - // Create window - window = GLFW.glfwCreateWindow(100, 100, "Texture Test", MemoryUtil.NULL, MemoryUtil.NULL); - if (window == MemoryUtil.NULL) { - throw new RuntimeException("Failed to create GLFW window"); - } - - // Make OpenGL context current - GLFW.glfwMakeContextCurrent(window); - GLFW.glfwSwapInterval(1); // Enable v-sync - - // Initialize OpenGL capabilities - GL.createCapabilities(); - - System.out.println("OpenGL initialized successfully"); - System.out.println("OpenGL Version: " + org.lwjgl.opengl.GL11.glGetString(org.lwjgl.opengl.GL11.GL_VERSION)); - glInitialized = true; - } catch (Exception e) { - System.err.println("Failed to initialize OpenGL: " + e.getMessage()); - // Continue without OpenGL for other tests - } - } - - /** - * Cleanup OpenGL resources - */ - private static void cleanupOpenGL() { - if (window != MemoryUtil.NULL) { - GLFW.glfwDestroyWindow(window); - } - GLFW.glfwTerminate(); - GLFW.glfwSetErrorCallback(null).free(); - } - - /** - * Test 1: Create model with textures and save to file - */ - public static void testCreateAndSaveModelWithTexture() { - System.out.println("\n--- Test 1: Create and Save Model with Textures ---"); - - if (!glInitialized) { - System.out.println("Skipping texture test - OpenGL not available"); - return; - } - - try { - // Create model - Model2D model = new Model2D("textured_character"); - model.setVersion("1.0.0"); - - // Create parts - ModelPart body = model.createPart("body"); - ModelPart head = model.createPart("head"); - - // Build hierarchy - body.addChild(head); - - // Set part properties - body.setPosition(0, 0); - head.setPosition(0, -50); - - // Create test textures - System.out.println("Creating test textures..."); - - // Create solid color texture - Texture bodyTexture = Texture.createSolidColor("body_texture", 64, 64, 0xFFFF0000); // Red - Texture headTexture = Texture.createSolidColor("head_texture", 64, 64, 0xFF00FF00); // Green - - // Create checkerboard texture - Texture checkerTexture = Texture.createCheckerboard("checker_texture", 128, 128, 16, - 0xFFFFFFFF, 0xFF0000FF); // White and Blue - - // === 关键修复:确保纹理数据被缓存 === - System.out.println("Ensuring texture data is cached..."); - bodyTexture.ensurePixelDataCached(); - headTexture.ensurePixelDataCached(); - checkerTexture.ensurePixelDataCached(); - - // 验证缓存状态 - System.out.println("Texture cache status:"); - System.out.println(" - body_texture: " + (bodyTexture.hasPixelData() ? "CACHED" : "MISSING")); - System.out.println(" - head_texture: " + (headTexture.hasPixelData() ? "CACHED" : "MISSING")); - System.out.println(" - checker_texture: " + (checkerTexture.hasPixelData() ? "CACHED" : "MISSING")); - - // Add textures to model - model.addTexture(bodyTexture); - model.addTexture(headTexture); - model.addTexture(checkerTexture); - - // Create meshes and assign textures - Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 40, 80); - Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 50, 50); - - // Set textures for meshes - bodyMesh.setTexture(bodyTexture); - headMesh.setTexture(headTexture); - - // Add meshes to model and parts - model.addMesh(bodyMesh); - model.addMesh(headMesh); - body.addMesh(bodyMesh); - head.addMesh(headMesh); - - // Create animation parameters - AnimationParameter smileParam = model.createParameter("smile", 0, 1, 0); - model.setParameterValue("smile", 0.5f); - - // Update model - model.update(0.016f); - - // Save to regular file - String regularFilePath = "textured_character.model"; - model.saveToFile(regularFilePath); - System.out.println("Textured model saved to regular file: " + regularFilePath); - - // Save to compressed file - String compressedFilePath = "textured_character.model.gz"; - model.saveToCompressedFile(compressedFilePath); - System.out.println("Textured model saved to compressed file: " + compressedFilePath); - - // Verify model state before saving - System.out.println("Textured model created successfully:"); - System.out.println(" - Name: " + model.getName()); - System.out.println(" - Textures: " + model.getTextures().size()); - System.out.println(" - Meshes: " + model.getMeshes().size()); - - // Print texture information - for (Texture texture : model.getTextures().values()) { - System.out.println(" - Texture: " + texture.getName() + - " (" + texture.getWidth() + "x" + texture.getHeight() + - ", format: " + texture.getFormat() + - ", cached: " + texture.hasPixelData() + ")"); - } - - } catch (Exception e) { - System.err.println("Error in testCreateAndSaveModelWithTexture: " + e.getMessage()); - e.printStackTrace(); - - // 提供更详细的错误信息 - if (e.getCause() != null) { - System.err.println("Caused by: " + e.getCause().getMessage()); - } - } - } - - /** - * Test 2: Load model with textures and verify data integrity - */ - public static void testLoadAndVerifyModelWithTexture() { - System.out.println("\n--- Test 2: Load and Verify Model with Textures ---"); - - if (!glInitialized) { - System.out.println("Skipping texture test - OpenGL not available"); - return; - } - - try { - // Load from regular file - String filePath = "textured_character.model"; - Model2D loadedModel = Model2D.loadFromFile(filePath); - - System.out.println("Textured model loaded successfully from: " + filePath); - - // Verify basic properties - System.out.println("Basic properties:"); - System.out.println(" - Name: " + loadedModel.getName()); - System.out.println(" - Version: " + loadedModel.getVersion()); - - // Verify textures - System.out.println("Textures verification:"); - System.out.println(" - Total textures: " + loadedModel.getTextures().size()); - - for (Texture texture : loadedModel.getTextures().values()) { - System.out.println(" - Texture '" + texture.getName() + "': " + - texture.getWidth() + "x" + texture.getHeight() + - ", format: " + texture.getFormat() + - ", disposed: " + texture.isDisposed()); - } - - // Verify parts and meshes - System.out.println("Parts and meshes verification:"); - for (ModelPart part : loadedModel.getParts()) { - System.out.println(" - Part '" + part.getName() + "': " + - part.getMeshes().size() + " meshes"); - - for (Mesh2D mesh : part.getMeshes()) { - Texture meshTexture = mesh.getTexture(); - System.out.println(" * Mesh '" + mesh.getName() + "': " + - (meshTexture != null ? "has texture '" + meshTexture.getName() + "'" : "no texture")); - } - } - - // Test texture functionality - System.out.println("Texture functionality test:"); - Texture bodyTexture = loadedModel.getTexture("body_texture"); - if (bodyTexture != null) { - System.out.println(" - Body texture validation:"); - System.out.println(" * Width: " + bodyTexture.getWidth()); - System.out.println(" * Height: " + bodyTexture.getHeight()); - System.out.println(" * Format: " + bodyTexture.getFormat()); - System.out.println(" * Memory usage: " + bodyTexture.getEstimatedMemoryUsage() + " bytes"); - - // Test texture binding (if OpenGL context is available) - try { - bodyTexture.bind(0); - System.out.println(" * Texture binding: SUCCESS"); - bodyTexture.unbind(); - } catch (Exception e) { - System.out.println(" * Texture binding: FAILED - " + e.getMessage()); - } - } - - // Test parameter modification - System.out.println("Parameter modification test:"); - loadedModel.setParameterValue("smile", 0.8f); - float newSmileValue = loadedModel.getParameterValue("smile"); - System.out.println(" - Modified smile parameter to: " + newSmileValue); - - // Test model update - loadedModel.update(0.016f); - System.out.println(" - Model update completed successfully"); - - } catch (Exception e) { - System.err.println("Error in testLoadAndVerifyModelWithTexture: " + e.getMessage()); - e.printStackTrace(); - } - } - - /** - * Test 3: Test compressed file operations with textures - */ - public static void testCompressedFileOperationsWithTexture() { - System.out.println("\n--- Test 3: Compressed File Operations with Textures ---"); - - if (!glInitialized) { - System.out.println("Skipping texture test - OpenGL not available"); - return; - } - - try { - // Load from compressed file - String compressedFilePath = "textured_character.model.gz"; - Model2D compressedModel = Model2D.loadFromCompressedFile(compressedFilePath); - - System.out.println("Textured model loaded successfully from compressed file: " + compressedFilePath); - System.out.println(" - Name: " + compressedModel.getName()); - System.out.println(" - Textures: " + compressedModel.getTextures().size()); - System.out.println(" - Parts: " + compressedModel.getParts().size()); - - // Verify textures in compressed model - System.out.println("Compressed model texture verification:"); - for (Texture texture : compressedModel.getTextures().values()) { - System.out.println(" - Texture '" + texture.getName() + "': " + - texture.getWidth() + "x" + texture.getHeight()); - } - - // Modify and re-save - compressedModel.setName("modified_textured_character"); - compressedModel.setParameterValue("smile", 0.9f); - - String newCompressedPath = "modified_textured_character.model.gz"; - compressedModel.saveToCompressedFile(newCompressedPath); - System.out.println("Modified textured model saved to new compressed file: " + newCompressedPath); - - // Verify the new compressed file can be loaded - Model2D reloadedModel = Model2D.loadFromCompressedFile(newCompressedPath); - System.out.println("Reloaded modified textured model verification:"); - System.out.println(" - Name: " + reloadedModel.getName()); - System.out.println(" - Smile parameter value: " + reloadedModel.getParameterValue("smile")); - System.out.println(" - Textures: " + reloadedModel.getTextures().size()); - - } catch (Exception e) { - System.err.println("Error in testCompressedFileOperationsWithTexture: " + e.getMessage()); - e.printStackTrace(); - } - } - - /** - * Test 4: Test animation system - */ - public static void testAnimationSystem() { - System.out.println("\n--- Test 4: Animation System Test ---"); - - try { - // Load model - Model2D model = Model2D.loadFromFile("test_character.model"); - System.out.println("Testing animation system:"); - - // Test parameter-driven animation - System.out.println("Parameter-driven animation test:"); - for (int frame = 0; frame < 10; frame++) { - float walkValue = (float) Math.sin(frame * 0.2f) * 0.5f + 0.5f; - float waveValue = (float) Math.sin(frame * 0.3f); - float blinkValue = frame % 20 == 0 ? 1.0f : 0.0f; // Blink every 20 frames - - model.setParameterValue("walk_cycle", walkValue); - model.setParameterValue("wave", waveValue); - model.setParameterValue("blink", blinkValue); - - model.update(0.016f); - - System.out.println(" - Frame " + frame + - ": walk=" + String.format("%.2f", walkValue) + - ", wave=" + String.format("%.2f", waveValue) + - ", blink=" + String.format("%.2f", blinkValue)); - } - - // Test pose system - System.out.println("Pose system test:"); - ModelPose currentPose = model.getCurrentPose(); - if (currentPose != null) { - System.out.println(" - Current pose: " + currentPose); - } - - // Test animation layer blending - System.out.println("Animation layer test:"); - for (AnimationLayer layer : model.getAnimationLayers()) { - System.out.println(" - Layer: " + layer.getName()); - } - - } catch (Exception e) { - System.err.println("Error in testAnimationSystem: " + e.getMessage()); - e.printStackTrace(); - } - } - - /** - * Test 5: Test physics system - */ - public static void testPhysicsSystem() { - System.out.println("\n--- Test 5: Physics System Test ---"); - - try { - // Load model - Model2D model = Model2D.loadFromFile("test_character.model"); - - System.out.println("Testing physics system:"); - - PhysicsSystem physics = model.getPhysics(); - System.out.println(" - Physics system: " + - (physics != null ? "available" : "not available")); - - if (physics != null) { - Vector2f gravity = physics.getGravity(); - System.out.println(" - Gravity: (" + gravity.x + ", " + gravity.y + ")"); - System.out.println(" - Air resistance: " + physics.getAirResistance()); - System.out.println(" - Time scale: " + physics.getTimeScale()); - System.out.println(" - Enabled: " + physics.isEnabled()); - } - - // Test physics simulation - System.out.println("Physics simulation test:"); - - // 初始化物理系统 - physics.initialize(); - - // 添加一些物理粒子 - PhysicsSystem.PhysicsParticle particle1 = physics.addParticle("test_particle1", new Vector2f(0, 0), 1.0f); - PhysicsSystem.PhysicsParticle particle2 = physics.addParticle("test_particle2", new Vector2f(10, 0), 1.0f); - - // 添加弹簧连接 - physics.addSpring("test_spring", particle1, particle2, 15.0f, 0.5f, 0.1f); - - for (int step = 0; step < 15; step++) { - model.update(0.016f); // Simulate physics - - if (step % 5 == 0) { - System.out.println(" - Step " + step + ": model updated with physics"); - Vector2f pos1 = particle1.getPosition(); - System.out.println(" Particle1 position: (" + - String.format("%.2f", pos1.x) + ", " + - String.format("%.2f", pos1.y) + ")"); - } - } - - // Test physics properties - System.out.println("Physics properties verification:"); - System.out.println(" - Active physics: " + physics.hasActivePhysics()); - System.out.println(" - Particle count: " + physics.getParticles().size()); - System.out.println(" - Spring count: " + physics.getSprings().size()); - - } catch (Exception e) { - System.err.println("Error in testPhysicsSystem: " + e.getMessage()); - e.printStackTrace(); - } - } - - /** - * Test 6: Test complex transformations - */ - public static void testComplexTransformations() { - System.out.println("\n--- Test 6: Complex Transformations Test ---"); - - try { - // Load model - Model2D model = Model2D.loadFromFile("test_character.model"); - - System.out.println("Testing complex transformations:"); - - // Test nested transformations - ModelPart root = model.getRootPart(); - if (root != null) { - Vector2f position = root.getPosition(); - Vector2f scale = root.getScale(); - System.out.println("Root transformation:"); - System.out.println(" - Local position: (" + position.x + ", " + position.y + ")"); - System.out.println(" - Rotation: " + root.getRotation() + " degrees"); - System.out.println(" - Scale: (" + scale.x + ", " + scale.y + ")"); - - // 获取世界变换矩阵中的位置 - float worldX = root.getWorldTransform().m02(); - float worldY = root.getWorldTransform().m12(); - System.out.println(" - World position (from matrix): (" + worldX + ", " + worldY + ")"); - } - - // Test transformation inheritance - System.out.println("Transformation inheritance test:"); - ModelPart head = model.getPart("head"); - if (head != null) { - Vector2f headPos = head.getPosition(); - float headWorldX = head.getWorldTransform().m02(); - float headWorldY = head.getWorldTransform().m12(); - System.out.println("Head transformation (relative to body):"); - System.out.println(" - Local position: (" + headPos.x + ", " + headPos.y + ")"); - System.out.println(" - World position (from matrix): (" + headWorldX + ", " + headWorldY + ")"); - } - - // Test bounds calculation - BoundingBox bounds = model.getBounds(); - if (bounds != null) { - System.out.println("Bounds calculation:"); - System.out.println(" - Min: (" + bounds.getMinX() + ", " + bounds.getMinY() + ")"); - System.out.println(" - Max: (" + bounds.getMaxX() + ", " + bounds.getMaxY() + ")"); - System.out.println(" - Width: " + bounds.getWidth()); - System.out.println(" - Height: " + bounds.getHeight()); - } - - // Test visibility system - System.out.println("Visibility system test:"); - model.setVisible(false); - System.out.println(" - Model visible: " + model.isVisible()); - model.setVisible(true); - System.out.println(" - Model visible: " + model.isVisible()); - - } catch (Exception e) { - System.err.println("Error in testComplexTransformations: " + e.getMessage()); - e.printStackTrace(); - } - } - - /** - * Test 7: Test performance with large model - */ - public static void testPerformance() { - System.out.println("\n--- Test 7: Performance Test ---"); - - try { - // Create a more complex model for performance testing - Model2D complexModel = new Model2D("complex_character"); - - // Add many parts - ModelPart root = complexModel.createPart("root"); - for (int i = 0; i < 10; i++) { - ModelPart part = complexModel.createPart("part_" + i); - root.addChild(part); - part.setPosition(i * 10, i * 5); - part.setRotation(i * 5); - - // Add mesh - Mesh2D mesh = Mesh2D.createQuad("mesh_" + i, 20, 20); - complexModel.addMesh(mesh); - part.addMesh(mesh); - } - - // Add multiple parameters - for (int i = 0; i < 8; i++) { - complexModel.createParameter("param_" + i, 0, 1, 0); - } - - System.out.println("Performance test with complex model:"); - System.out.println(" - Parts: " + complexModel.getParts().size()); - System.out.println(" - Parameters: " + complexModel.getParameters().size()); - System.out.println(" - Meshes: " + complexModel.getMeshes().size()); - - // Performance test: multiple updates - long startTime = System.currentTimeMillis(); - int frameCount = 100; - - for (int i = 0; i < frameCount; i++) { - // Animate parameters - for (int j = 0; j < 8; j++) { - float value = (float) Math.sin(i * 0.1f + j * 0.5f) * 0.5f + 0.5f; - complexModel.setParameterValue("param_" + j, value); - } - complexModel.update(0.016f); - } - - long endTime = System.currentTimeMillis(); - long totalTime = endTime - startTime; - double avgTimePerFrame = (double) totalTime / frameCount; - - System.out.println("Performance results:"); - System.out.println(" - Total time for " + frameCount + " frames: " + totalTime + "ms"); - System.out.println(" - Average time per frame: " + String.format("%.2f", avgTimePerFrame) + "ms"); - System.out.println(" - Estimated FPS: " + String.format("%.1f", 1000.0 / avgTimePerFrame)); - - } catch (Exception e) { - System.err.println("Error in testPerformance: " + e.getMessage()); - e.printStackTrace(); - } - } - - - /** - * Utility method to print part hierarchy - */ - private static void printPartHierarchy(ModelPart part, int depth) { - String indent = " ".repeat(depth); - System.out.println(indent + "- " + part.getName() + - " (children: " + part.getChildren().size() + ")"); - - for (ModelPart child : part.getChildren()) { - printPartHierarchy(child, depth + 1); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java deleted file mode 100644 index ecc7ce0..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java +++ /dev/null @@ -1,691 +0,0 @@ -package com.chuangzhou.vivid2D.test; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; -import com.chuangzhou.vivid2D.render.model.util.Texture; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import org.joml.Vector2f; -import org.lwjgl.glfw.GLFW; -import org.lwjgl.glfw.GLFWErrorCallback; -import org.lwjgl.glfw.GLFWVidMode; -import org.lwjgl.opengl.GL; -import org.lwjgl.system.MemoryUtil; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -/** - * 物理系统使用实例 - 演示弹簧、重力和碰撞效果 - * - * @author tzdwindows 7 - */ -public class ModelTest2 { - - private static final int WINDOW_WIDTH = 1000; - private static final int WINDOW_HEIGHT = 700; - private static final String WINDOW_TITLE = "Physics System Demo"; - - private long window; - private boolean running = true; - private Model2D physicsModel; - private PhysicsSystem physics; - - // 测试用例控制 - private int testCase = 5; - private boolean gravityEnabled = true; - private boolean springsEnabled = true; - - // 存储部件引用,用于清理 - private final List currentParts = new ArrayList<>(); - - // 所有测试基点(初始 xy = 0,0) - private final Vector2f initialOrigin = new Vector2f(0, 0); - - public static void main(String[] args) { - new ModelTest2().run(); - } - - public void run() { - try { - init(); - loop(); - } catch (Throwable t) { - t.printStackTrace(); - } finally { - cleanup(); - } - } - - private void init() { - GLFWErrorCallback.createPrint(System.err).set(); - - if (!GLFW.glfwInit()) { - throw new IllegalStateException("Unable to initialize GLFW"); - } - - GLFW.glfwDefaultWindowHints(); - GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); - GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE); - - window = GLFW.glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE, MemoryUtil.NULL, MemoryUtil.NULL); - if (window == MemoryUtil.NULL) throw new RuntimeException("Failed to create GLFW window"); - - GLFWVidMode vidMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor()); - GLFW.glfwSetWindowPos(window, - (vidMode.width() - WINDOW_WIDTH) / 2, - (vidMode.height() - WINDOW_HEIGHT) / 2); - - // 设置键盘回调 - GLFW.glfwSetKeyCallback(window, (wnd, key, scancode, action, mods) -> { - if (key == GLFW.GLFW_KEY_ESCAPE && action == GLFW.GLFW_RELEASE) running = false; - if (key == GLFW.GLFW_KEY_SPACE && action == GLFW.GLFW_RELEASE) { - testCase = (testCase + 1) % 6; // 支持 0..5 共 6 个用例 - setupTestCase(); - } - if (key == GLFW.GLFW_KEY_G && action == GLFW.GLFW_RELEASE) { - gravityEnabled = !gravityEnabled; - physics.setGravity(gravityEnabled ? new Vector2f(0, -98.0f) : new Vector2f(0, 0)); - System.out.println("Gravity " + (gravityEnabled ? "ENABLED" : "DISABLED")); - } - if (key == GLFW.GLFW_KEY_S && action == GLFW.GLFW_RELEASE) { - springsEnabled = !springsEnabled; - toggleSprings(springsEnabled); - System.out.println("Springs " + (springsEnabled ? "ENABLED" : "DISABLED")); - } - if (key == GLFW.GLFW_KEY_R && action == GLFW.GLFW_RELEASE) { - resetPhysics(); - } - if (key == GLFW.GLFW_KEY_C && action == GLFW.GLFW_RELEASE) { - applyRandomForce(); - } - }); - - GLFW.glfwSetWindowSizeCallback(window, (wnd, w, h) -> ModelRender.setViewport(w, h)); - - GLFW.glfwMakeContextCurrent(window); - GLFW.glfwSwapInterval(1); - GLFW.glfwShowWindow(window); - - GL.createCapabilities(); - - createPhysicsModel(); - ModelRender.initialize(); - - System.out.println("Physics System Demo Initialized"); - printControls(); - } - - private void printControls() { - System.out.println("\n=== Controls ==="); - System.out.println("ESC - Exit"); - System.out.println("SPACE - Change test case"); - System.out.println("G - Toggle gravity"); - System.out.println("S - Toggle springs"); - System.out.println("R - Reset physics"); - System.out.println("C - Apply random force"); - System.out.println("================\n"); - } - - /** - * 创建物理测试模型 - */ - private void createPhysicsModel() { - physicsModel = new Model2D("PhysicsDemo"); - physics = physicsModel.getPhysics(); - - // 配置物理系统 - physics.setGravity(new Vector2f(0, -98.0f)); - physics.setAirResistance(0.05f); - physics.setTimeScale(1.0f); - physics.setEnabled(true); - physics.initialize(); - - setupTestCase(); - } - - /** - * 设置不同的测试用例 - */ - private void setupTestCase() { - // 清理之前的设置 - clearCurrentParts(); - physics.reset(); - - switch (testCase) { - case 0: - setupSpringChain(); - break; - case 1: - setupClothSimulation(); - break; - case 2: - setupPendulum(); - break; - case 3: - setupSoftBody(); - break; - case 4: - setupWindTest(); - break; - case 5: - setupFreeFallTest(); - break; - } - - System.out.println("Test Case " + testCase + ": " + getTestCaseName(testCase)); - } - - /** - * 清理当前部件 - */ - private void clearCurrentParts() { - // 由于无法直接清除模型的parts列表,我们创建一个新模型 - physicsModel = new Model2D("PhysicsDemo"); - currentParts.clear(); - - // 重新配置物理系统 - physics = physicsModel.getPhysics(); - physics.setGravity(new Vector2f(0, -98.0f)); - physics.setAirResistance(0.05f); - physics.setTimeScale(1.0f); - physics.setEnabled(true); - physics.initialize(); - } - - /** - * 测试用例1: 弹簧链 - */ - private void setupSpringChain() { - // 创建5个连接的粒子,基于 initialOrigin(因此首个粒子是 (0,0)) - for (int i = 0; i < 5; i++) { - Vector2f position = new Vector2f(initialOrigin.x + i * 60, initialOrigin.y + i * 20); - PhysicsSystem.PhysicsParticle particle = physics.addParticle("particle_" + i, position, 1.0f); - - // 第一个粒子固定(位于 initialOrigin) - if (i == 0) { - particle.setMovable(false); - } - - // 创建对应的模型部件 - ModelPart part = physicsModel.createPart("part_" + i); - part.setPosition(position.x, position.y); - currentParts.add(part); - - // 创建圆形网格 - Mesh2D circleMesh = createCircleMesh("circle_" + i, 20, getColorForIndex(i)); - part.addMesh(circleMesh); - physicsModel.addMesh(circleMesh); - - // 将部件设置为粒子的用户数据,用于同步位置 - particle.setUserData(part); - - // 添加弹簧连接(除了第一个粒子) - if (i > 0) { - PhysicsSystem.PhysicsParticle prevParticle = physics.getParticle("particle_" + (i - 1)); - physics.addSpring("spring_" + (i - 1), prevParticle, particle, 60.0f, 0.3f, 0.1f); - } - } - } - - /** - * 测试用例2: 布料模拟 - */ - private void setupClothSimulation() { - int rows = 4; - int cols = 6; - float spacing = 40.0f; - - // 创建布料网格,基于 initialOrigin - for (int y = 0; y < rows; y++) { - for (int x = 0; x < cols; x++) { - int index = y * cols + x; - Vector2f position = new Vector2f(initialOrigin.x + x * spacing, initialOrigin.y + y * spacing); - - PhysicsSystem.PhysicsParticle particle = physics.addParticle("cloth_" + index, position, 0.8f); - - // 固定顶部行的粒子(y==0) - if (y == 0) { - particle.setMovable(false); - } - - ModelPart part = physicsModel.createPart("cloth_part_" + index); - part.setPosition(position.x, position.y); - currentParts.add(part); - - Mesh2D squareMesh = createSquareMesh("square_" + index, 15, getColorForIndex(index)); - part.addMesh(squareMesh); - physicsModel.addMesh(squareMesh); - - // 将部件设置为粒子的用户数据 - particle.setUserData(part); - - // 添加水平弹簧连接 - if (x > 0) { - PhysicsSystem.PhysicsParticle leftParticle = physics.getParticle("cloth_" + (index - 1)); - physics.addSpring("h_spring_" + index, leftParticle, particle, spacing, 0.4f, 0.05f); - } - - // 添加垂直弹簧连接 - if (y > 0) { - PhysicsSystem.PhysicsParticle topParticle = physics.getParticle("cloth_" + (index - cols)); - physics.addSpring("v_spring_" + index, topParticle, particle, spacing, 0.4f, 0.05f); - } - } - } - } - - /** - * 测试用例3: 钟摆系统 - */ - private void setupPendulum() { - // 创建钟摆锚点(位于 initialOrigin) - Vector2f anchorPos = new Vector2f(initialOrigin); - PhysicsSystem.PhysicsParticle anchor = physics.addParticle("anchor", anchorPos, 0.0f); - anchor.setMovable(false); // 固定锚点 - - // 创建钟摆摆锤(相对锚点水平分布) - for (int i = 0; i < 3; i++) { - Vector2f pendulumPos = new Vector2f(initialOrigin.x + (i - 1) * 120, initialOrigin.y + 200); - PhysicsSystem.PhysicsParticle particle = physics.addParticle("pendulum_" + i, pendulumPos, 2.0f); - - // 检查粒子是否成功创建 - if (particle == null) { - System.err.println("Failed to create pendulum particle: pendulum_" + i); - continue; - } - - ModelPart part = physicsModel.createPart("pendulum_part_" + i); - part.setPosition(pendulumPos.x, pendulumPos.y); - currentParts.add(part); - - Mesh2D ballMesh = createCircleMesh("ball_" + i, 25, getColorForIndex(i)); - part.addMesh(ballMesh); - physicsModel.addMesh(ballMesh); - - // 将部件设置为粒子的用户数据 - particle.setUserData(part); - - // 连接到锚点 - 确保anchor和particle都不为null - if (anchor != null && particle != null) { - float length = 200 + i * 50; - physics.addSpring("pendulum_spring_" + i, anchor, particle, length, 0.1f, 0.02f); - } - } - } - - /** - * 测试用例4: 软体模拟 - */ - private void setupSoftBody() { - // 创建软体圆形,中心在 initialOrigin - int points = 8; - float radius = 60.0f; - Vector2f center = new Vector2f(initialOrigin); - - // 第一步:先创建所有粒子 - List particlesList = new ArrayList<>(); - for (int i = 0; i < points; i++) { - float angle = (float) (i * 2 * Math.PI / points); - Vector2f position = new Vector2f( - center.x + radius * (float) Math.cos(angle), - center.y + radius * (float) Math.sin(angle) - ); - - PhysicsSystem.PhysicsParticle particle = physics.addParticle("soft_" + i, position, 0.5f); - particlesList.add(particle); - - ModelPart part = physicsModel.createPart("soft_part_" + i); - part.setPosition(position.x, position.y); - currentParts.add(part); - - Mesh2D pointMesh = createCircleMesh("point_" + i, 12, 0xFF00FFFF); - part.addMesh(pointMesh); - physicsModel.addMesh(pointMesh); - - // 将部件设置为粒子的用户数据 - particle.setUserData(part); - } - - // 第二步:再创建所有弹簧连接 - for (int i = 0; i < points; i++) { - PhysicsSystem.PhysicsParticle particle = particlesList.get(i); - - // 连接到相邻点 - int next = (i + 1) % points; - PhysicsSystem.PhysicsParticle nextParticle = particlesList.get(next); - physics.addSpring("soft_spring_" + i, particle, nextParticle, - radius * 2 * (float) Math.sin(Math.PI / points), 0.5f, 0.1f); - - // 连接到对面的点(增加稳定性) - if (i < points / 2) { - int opposite = (i + points / 2) % points; - PhysicsSystem.PhysicsParticle oppositeParticle = particlesList.get(opposite); - physics.addSpring("cross_spring_" + i, particle, oppositeParticle, - radius * 2, 0.2f, 0.05f); - } - } - } - - /** - * 测试用例5: 自由落体测试 - */ - private void setupFreeFallTest() { - // 创建地面(位于 initialOrigin) - Vector2f groundPos = new Vector2f(initialOrigin); - PhysicsSystem.PhysicsParticle ground = physics.addParticle("ground", groundPos, 0.0f); - ground.setMovable(false); - - // 创建多个不同质量的物体从不同高度掉落(相对于 initialOrigin) - for (int i = 0; i < 5; i++) { - Vector2f position = new Vector2f(initialOrigin.x + 300 + i * 100, initialOrigin.y + 600 - i * 50); - float mass = 1.0f + i * 0.5f; // 不同质量 - - PhysicsSystem.PhysicsParticle particle = physics.addParticle("fall_" + i, position, mass); - - ModelPart part = physicsModel.createPart("fall_part_" + i); - part.setPosition(position.x, position.y); - currentParts.add(part); - - Mesh2D ballMesh = createCircleMesh("fall_ball_" + i, 15 + i * 3, getColorForIndex(i)); - part.addMesh(ballMesh); - physicsModel.addMesh(ballMesh); - - particle.setUserData(part); - } - - // 添加地面碰撞体(基于 initialOrigin) - physics.addRectangleCollider("ground_collider", groundPos, 800, 20); - } - - /** - * 测试用例6: 风力测试 - */ - private void setupWindTest() { - // 创建布料用于测试风力,基于 initialOrigin - int rows = 6; - int cols = 8; - float spacing = 35.0f; - - for (int y = 0; y < rows; y++) { - for (int x = 0; x < cols; x++) { - int index = y * cols + x; - // 布料放在 initialOrigin.x + ..., initialOrigin.y - y*spacing + 500 以方便显示 - Vector2f position = new Vector2f(initialOrigin.x + x * spacing, initialOrigin.y - y * spacing + 500); - - PhysicsSystem.PhysicsParticle particle = physics.addParticle("wind_cloth_" + index, position, 0.6f); - - // 固定顶部行的粒子(y==0) - if (y == 0) { - particle.setMovable(false); - } - - ModelPart part = physicsModel.createPart("wind_part_" + index); - part.setPosition(position.x, position.y); - currentParts.add(part); - - Mesh2D squareMesh = createSquareMesh("wind_square_" + index, 12, getColorForIndex(index)); - part.addMesh(squareMesh); - physicsModel.addMesh(squareMesh); - - particle.setUserData(part); - - // 添加水平弹簧连接 - if (x > 0) { - PhysicsSystem.PhysicsParticle leftParticle = physics.getParticle("wind_cloth_" + (index - 1)); - physics.addSpring("wind_h_spring_" + index, leftParticle, particle, spacing, 0.3f, 0.05f); - } - - // 添加垂直弹簧连接 - if (y > 0) { - PhysicsSystem.PhysicsParticle topParticle = physics.getParticle("wind_cloth_" + (index - cols)); - physics.addSpring("wind_v_spring_" + index, topParticle, particle, spacing, 0.3f, 0.05f); - } - } - } - } - - /** - * 应用风力效果 - */ - private void applyWindEffect() { - // 随机风力方向 - float windStrength = 50.0f; - float windDirection = (float) (Math.random() * 2 * Math.PI); // 随机方向 - - Vector2f windForce = new Vector2f( - (float) Math.cos(windDirection) * windStrength, - (float) Math.sin(windDirection) * windStrength - ); - - // 对所有可移动粒子应用风力 - for (PhysicsSystem.PhysicsParticle particle : physics.getParticles().values()) { - if (particle.isMovable()) { - // 风力随粒子高度变化(模拟真实风) - float heightFactor = particle.getPosition().y / 500.0f; - Vector2f adjustedWind = new Vector2f(windForce).mul(heightFactor); - particle.addForce(adjustedWind); - } - } - - System.out.println("Wind applied: " + windForce); - } - - /** - * 应用持续风力(周期性) - */ - private void applyContinuousWind(float deltaTime) { - // 模拟周期性风力 - float time = System.nanoTime() * 0.000000001f; - float windStrength = 30.0f + (float) Math.sin(time * 2) * 20.0f; // 周期性变化 - - Vector2f windForce = new Vector2f(windStrength, 0); // 主要水平方向 - - for (PhysicsSystem.PhysicsParticle particle : physics.getParticles().values()) { - if (particle.isMovable()) { - particle.addForce(new Vector2f(windForce)); - } - } - } - - /** - * 创建圆形网格 - 修正版本 - */ - private Mesh2D createCircleMesh(String name, float radius, int color) { - int segments = 16; - int vertexCount = segments + 1; // 中心点 + 圆周点 - float[] vertices = new float[vertexCount * 2]; - float[] uvs = new float[vertexCount * 2]; - int[] indices = new int[segments * 3]; - - // 中心点 (索引0) - vertices[0] = 0; - vertices[1] = 0; - uvs[0] = 0.5f; - uvs[1] = 0.5f; - - // 圆周点 (索引1到segments) - for (int i = 0; i < segments; i++) { - float angle = (float) (i * 2 * Math.PI / segments); - int vertexIndex = (i + 1) * 2; - vertices[vertexIndex] = radius * (float) Math.cos(angle); - vertices[vertexIndex + 1] = radius * (float) Math.sin(angle); - uvs[vertexIndex] = (float) Math.cos(angle) * 0.5f + 0.5f; - uvs[vertexIndex + 1] = (float) Math.sin(angle) * 0.5f + 0.5f; - } - - // 三角形索引 - 每个三角形连接中心点和两个相邻的圆周点 - for (int i = 0; i < segments; i++) { - int triangleIndex = i * 3; - indices[triangleIndex] = 0; // 中心点 - indices[triangleIndex + 1] = i + 1; // 当前圆周点 - indices[triangleIndex + 2] = (i + 1) % segments + 1; // 下一个圆周点 - } - - Mesh2D mesh = new Mesh2D(name, vertices, uvs, indices); - mesh.setTexture(createSolidColorTexture(name + "_tex", color)); - return mesh; - } - - /** - * 创建方形网格 - */ - private Mesh2D createSquareMesh(String name, float size, int color) { - float halfSize = size / 2; - float[] vertices = { - -halfSize, -halfSize, - halfSize, -halfSize, - halfSize, halfSize, - -halfSize, halfSize - }; - float[] uvs = { - 0, 0, - 1, 0, - 1, 1, - 0, 1 - }; - int[] indices = {0, 1, 2, 0, 2, 3}; - - Mesh2D mesh = new Mesh2D(name, vertices, uvs, indices); - mesh.setTexture(createSolidColorTexture(name + "_tex", color)); - return mesh; - } - - /** - * 创建纯色纹理 - */ - private Texture createSolidColorTexture(String name, int color) { - int width = 64, height = 64; - ByteBuffer buf = MemoryUtil.memAlloc(width * height * 4); - - byte r = (byte) ((color >> 16) & 0xFF); - byte g = (byte) ((color >> 8) & 0xFF); - byte b = (byte) (color & 0xFF); - - for (int i = 0; i < width * height; i++) { - buf.put(r).put(g).put(b).put((byte) 255); - } - - buf.flip(); - Texture texture = new Texture(name, width, height, Texture.TextureFormat.RGBA, buf); - MemoryUtil.memFree(buf); - return texture; - } - - /** - * 根据索引获取不同颜色 - */ - private int getColorForIndex(int index) { - int[] colors = { - 0xFF00FF00, // 绿色 - 0xFFFF0000, // 红色 - 0xFF0000FF, // 蓝色 - 0xFFFFFF00, // 黄色 - 0xFFFF00FF, // 紫色 - 0xFF00FFFF // 青色 - }; - return colors[index % colors.length]; - } - - /** - * 获取测试用例名称 - */ - private String getTestCaseName(int testCase) { - switch (testCase) { - case 0: - return "Spring Chain"; - case 1: - return "Cloth Simulation"; - case 2: - return "Pendulum System"; - case 3: - return "Soft Body"; - case 4: - return "Wind Test"; - case 5: - return "Free Fall Test"; - default: - return "Unknown"; - } - } - - /** - * 切换弹簧状态 - */ - private void toggleSprings(boolean enabled) { - for (PhysicsSystem.PhysicsSpring spring : physics.getSprings()) { - spring.setEnabled(enabled); - } - } - - /** - * 重置物理系统 - */ - private void resetPhysics() { - setupTestCase(); - System.out.println("Physics reset"); - } - - /** - * 施加随机力 - */ - private void applyRandomForce() { - for (PhysicsSystem.PhysicsParticle particle : physics.getParticles().values()) { - if (particle.isMovable()) { - float forceX = (float) (Math.random() - 0.5) * 200f; - float forceY = (float) (Math.random() - 0.5) * 200f; - particle.addForce(new Vector2f(forceX, forceY)); - } - } - System.out.println("Random forces applied"); - } - - private void loop() { - long last = System.nanoTime(); - double nsPerUpdate = 1_000_000_000.0 / 60.0; - double accumulator = 0.0; - - while (running && !GLFW.glfwWindowShouldClose(window)) { - long now = System.nanoTime(); - accumulator += (now - last) / nsPerUpdate; - last = now; - - while (accumulator >= 1.0) { - update(1.0f / 60.0f); - accumulator -= 1.0; - } - - render(last); - - GLFW.glfwSwapBuffers(window); - GLFW.glfwPollEvents(); - } - } - - private void update(float dt) { - // 更新物理系统 - 会自动同步到模型部件 - physicsModel.update(dt); - } - - private void render(long last) { - RenderSystem.setClearColor(0.1f, 0.1f, 0.15f, 1.0f); - ModelRender.render(last, physicsModel); - } - - private void cleanup() { - System.out.println("Cleaning up physics demo resources..."); - ModelRender.cleanup(); - Texture.cleanupAll(); - if (window != MemoryUtil.NULL) GLFW.glfwDestroyWindow(window); - GLFW.glfwTerminate(); - GLFW.glfwSetErrorCallback(null).free(); - System.out.println("Physics demo finished"); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java b/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java deleted file mode 100644 index ddff295..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java +++ /dev/null @@ -1,211 +0,0 @@ -package com.chuangzhou.vivid2D.test; - -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; -import com.chuangzhou.vivid2D.render.model.util.Texture; -import org.joml.Vector2f; -import org.lwjgl.system.MemoryUtil; - -import javax.swing.*; -import java.awt.event.ActionEvent; -import java.nio.ByteBuffer; - -/** - * 在原 TestModelGLPanel 的基础上增加简单动画(手臂、腿、头部摆动) - * - * @author tzdwindows 7 - */ -public class TestModelGLPanel { - - private static final String MODEL_PATH = "C:\\Users\\Administrator\\Desktop\\trump_texture.model"; - - // 使 testModel 与动画计时可访问 - private static Model2D testModel; - private static float animationTime = 0f; - private static boolean animate = true; - - public static void main(String[] args) { - SwingUtilities.invokeLater(() -> { - JFrame frame = new JFrame("ModelGLPanel Demo"); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - - ModelRenderPanel glPanel = null; - try { - // 先创建一个空的 Model2D 实例(将在 GL 上下文中初始化更详细内容) - testModel = new Model2D("Humanoid"); - - glPanel = new ModelRenderPanel(testModel, 800, 600); - - // 在 GL 上下文中创建 mesh / part / physics 等资源 - ModelRenderPanel finalGlPanel = glPanel; - glPanel.getGlContextManager().executeInGLContext(() -> { - setupModelInGL(testModel); - return null; - }); - - // 创建一个 Swing Timer,用于驱动动画(~60 FPS) - int fps = 60; - int delayMs = 1000 / fps; - Timer timer = new Timer(delayMs, (ActionEvent e) -> { - if (!animate) return; - float dt = 1.0f / fps; - // 在 GL 上下文中更新模型状态(旋转、参数、物理更新等) - finalGlPanel.getGlContextManager().executeInGLContext(() -> { - updateAnimation(testModel, dt); - return null; - }); - // 请求重绘(ModelGLPanel 应在其 paintGL 中处理渲染) - finalGlPanel.repaint(); - }); - timer.start(); - - // 可选:在窗口上添加键盘控制开关(Space 切换动画) - frame.addKeyListener(new java.awt.event.KeyAdapter() { - @Override - public void keyReleased(java.awt.event.KeyEvent e) { - if (e.getKeyCode() == java.awt.event.KeyEvent.VK_SPACE) { - animate = !animate; - System.out.println("Animation " + (animate ? "enabled" : "disabled")); - } - } - }); - } catch (Exception e) { - throw new RuntimeException(e); - } - - // 将 GL 面板加入窗体并显示 - frame.add(glPanel); - frame.pack(); - frame.setLocationRelativeTo(null); - frame.setVisible(true); - }); - } - - private static void setupModelInGL(Model2D model) { - PhysicsSystem physics = model.getPhysics(); - physics.setGravity(new Vector2f(0, -98.0f)); - physics.setAirResistance(0.05f); - physics.setTimeScale(1.0f); - physics.setEnabled(true); - physics.initialize(); - - // body 放在屏幕中心 - ModelPart body = model.createPart("body"); - body.setPosition(0, 0); - // 身体网格:宽 80 高 120 - Mesh2D bodyMesh = Mesh2D.createQuad("body_mesh", 80, 120); - bodyMesh.setTexture(createSolidTexture(64, 128, 0xFF4A6AFF)); // 蓝衣 - body.addMesh(bodyMesh); - - // head:相对于 body 在上方偏移 - ModelPart head = model.createPart("head"); - head.setPosition(0, -90); - Mesh2D headMesh = Mesh2D.createQuad("head_mesh", 60, 60); - headMesh.setTexture(createHeadTexture()); - head.addMesh(headMesh); - - // left arm - ModelPart leftArm = model.createPart("left_arm"); - leftArm.setPosition(-60, -20); - Mesh2D leftArmMesh = Mesh2D.createQuad("left_arm_mesh", 18, 90); - leftArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED)); - leftArm.addMesh(leftArmMesh); - - // right arm - ModelPart rightArm = model.createPart("right_arm"); - rightArm.setPosition(60, -20); - Mesh2D rightArmMesh = Mesh2D.createQuad("right_arm_mesh", 18, 90); - rightArmMesh.setTexture(createSolidTexture(16, 90, 0xFF6495ED)); - rightArm.addMesh(rightArmMesh); - - // left leg - ModelPart leftLeg = model.createPart("left_leg"); - leftLeg.setPosition(-20, 90); - Mesh2D leftLegMesh = Mesh2D.createQuad("left_leg_mesh", 20, 100); - leftLegMesh.setTexture(createSolidTexture(20, 100, 0xFF4169E1)); - leftLeg.addMesh(leftLegMesh); - - // right leg - ModelPart rightLeg = model.createPart("right_leg"); - rightLeg.setPosition(20, 90); - Mesh2D rightLegMesh = Mesh2D.createQuad("right_leg_mesh", 20, 100); - rightLegMesh.setTexture(createSolidTexture(20, 100, 0xFF4169E1)); - rightLeg.addMesh(rightLegMesh); - - // 建立层级:body 为根 - body.addChild(head); - body.addChild(leftArm); - body.addChild(rightArm); - body.addChild(leftLeg); - body.addChild(rightLeg); - - // 创建动画参数用于简单摆动(可选,示例中也直接对 Part 旋转) - model.createParameter("arm_swing", -1.0f, 1.0f, 0f); - model.createParameter("leg_swing", -1.0f, 1.0f, 0f); - model.createParameter("head_rotation", -0.5f, 0.5f, 0f); - - System.out.println("Humanoid model created with parts: " + model.getParts().size()); - } - - private static void updateAnimation(Model2D model, float dt) { - animationTime += dt; - float armSwing = (float) Math.sin(animationTime * 3.0f) * 0.7f; // -0.7 .. 0.7 - float legSwing = (float) Math.sin(animationTime * 3.0f + Math.PI) * 0.6f; - float headRot = (float) Math.sin(animationTime * 1.4f) * 0.15f; - - model.setParameterValue("arm_swing", armSwing); - model.setParameterValue("leg_swing", legSwing); - model.setParameterValue("head_rotation", headRot); - - ModelPart leftArm = model.getPart("left_arm"); - ModelPart rightArm = model.getPart("right_arm"); - ModelPart leftLeg = model.getPart("left_leg"); - ModelPart rightLeg = model.getPart("right_leg"); - ModelPart head = model.getPart("head"); - - if (leftArm != null) leftArm.setRotation(-0.8f * armSwing - 0.2f); - if (rightArm != null) rightArm.setRotation(0.8f * armSwing + 0.2f); - if (leftLeg != null) leftLeg.setRotation(0.6f * legSwing); - if (rightLeg != null) rightLeg.setRotation(-0.6f * legSwing); - if (head != null) head.setRotation(headRot); - - // 更新物理与层级(如果 Model2D.update 会进行必要的矩阵/物理计算) - model.update(dt); - } - - private static Texture createSolidTexture(int w, int h, int rgba) { - ByteBuffer buf = MemoryUtil.memAlloc(w * h * 4); - byte a = (byte) ((rgba >> 24) & 0xFF); - byte r = (byte) ((rgba >> 16) & 0xFF); - byte g = (byte) ((rgba >> 8) & 0xFF); - byte b = (byte) (rgba & 0xFF); - for (int i = 0; i < w * h; i++) { - buf.put(r).put(g).put(b).put(a); - } - buf.flip(); - Texture t = new Texture("solid_" + rgba + "_" + w + "x" + h, w, h, Texture.TextureFormat.RGBA, buf); - MemoryUtil.memFree(buf); - return t; - } - - private static Texture createHeadTexture() { - int width = 64, height = 64; - int[] pixels = new int[width * height]; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - float dx = (x - width / 2f) / (width / 2f); - float dy = (y - height / 2f) / (height / 2f); - float dist = (float) Math.sqrt(dx * dx + dy * dy); - int alpha = dist > 1.0f ? 0 : 255; - int r = (int) (240 * (1.0f - dist * 0.25f)); - int g = (int) (200 * (1.0f - dist * 0.25f)); - int b = (int) (180 * (1.0f - dist * 0.25f)); - pixels[y * width + x] = (alpha << 24) | (r << 16) | (g << 8) | b; - } - } - return new Texture("head_tex", width, height, Texture.TextureFormat.RGBA, pixels); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/util/ManagementDataToJsonConverter.java b/src/main/java/com/chuangzhou/vivid2D/util/ManagementDataToJsonConverter.java deleted file mode 100644 index d73a58a..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/util/ManagementDataToJsonConverter.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.chuangzhou.vivid2D.util; - -import com.chuangzhou.vivid2D.render.awt.manager.data.LayerOperationManagerData; -import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.util.HashMap; -import java.util.Map; - -public class ManagementDataToJsonConverter { - /** - * 从 Java 序列化文件加载两个对象,并将它们组合成一个 JSON 对象保存。 - * - * @param inputFilePath Java 序列化文件(.data)的路径 - * @param outputFilePath 目标 JSON 文件(.json)的路径 - */ - public static void convert(String inputFilePath, String outputFilePath) { - LayerOperationManagerData layerData = null; - ParametersManagementData managementData = null; - - File inputFile = new File(inputFilePath); - if (!inputFile.exists()) { - System.err.println("错误:输入文件未找到:" + inputFilePath); - return; - } - - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(inputFile))) { - layerData = (LayerOperationManagerData) ois.readObject(); - System.out.println("成功读取第一个对象: LayerOperationManagerData"); - managementData = (ParametersManagementData) ois.readObject(); - System.out.println("成功读取第二个对象: ParametersManagementData"); - - } catch (IOException e) { - System.err.println("读取序列化文件失败(可能是文件已损坏或写入方式不匹配): " + e.getMessage()); - e.printStackTrace(); - return; - } catch (ClassNotFoundException e) { - System.err.println("无法找到类定义,请检查类路径是否包含 LayerOperationManagerData 和 ParametersManagementData: " + e.getMessage()); - e.printStackTrace(); - return; - } - - try { - Map dataMap = new HashMap<>(); - dataMap.put("layerData", layerData); - dataMap.put("parametersManagementData", managementData); - - ObjectMapper mapper = new ObjectMapper(); - - // ============================================================== - // 关键修改:禁用 FAIL_ON_EMPTY_BEANS 以避免 InvalidDefinitionException - // 但会导致 LayerInfo 内部数据丢失 - mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); - // ============================================================== - - mapper.enable(SerializationFeature.INDENT_OUTPUT); - mapper.writeValue(new File(outputFilePath), dataMap); - System.out.println("成功将序列化数据转换为 JSON 并保存至: " + outputFilePath); - - } catch (IOException e) { - System.err.println("写入 JSON 文件失败: " + e.getMessage()); - e.printStackTrace(); - } - } - - public static void main(String[] args) { - String input = "C:\\Users\\Administrator\\Desktop\\testing.model.data"; - String output = "C:\\Users\\Administrator\\Desktop\\management_data.json"; - convert(input, output); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/util/ModelDataJsonConverter.java b/src/main/java/com/chuangzhou/vivid2D/util/ModelDataJsonConverter.java deleted file mode 100644 index 15928f6..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/util/ModelDataJsonConverter.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.chuangzhou.vivid2D.util; - -import com.chuangzhou.vivid2D.render.model.data.ModelData; // 确保导入了你的 ModelData 类 -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; - -import java.io.File; -import java.io.IOException; - -/** - * 将 Java 序列化的 ModelData 文件转换为 JSON 格式的工具。 - */ -public class ModelDataJsonConverter { - - /** - * 从 Java 序列化文件加载 ModelData,并将其保存为 JSON 文件。 - * * @param inputFilePath Java 序列化文件(.model)的路径 - * @param outputFilePath 目标 JSON 文件(.json)的路径 - * @param isCompressed 输入文件是否使用了 GZIP 压缩(ModelData.saveToCompressedFile 生成) - */ - public static void convert(String inputFilePath, String outputFilePath, boolean isCompressed) { - ModelData modelData; - - try { - File inputFile = new File(inputFilePath); - - // 1. 从 Java 序列化文件加载 ModelData 对象 - if (isCompressed) { - // 使用 ModelData 中已有的 loadFromCompressedFile 方法 - modelData = ModelData.loadFromCompressedFile(inputFile); - System.out.println("成功从压缩文件加载模型数据: " + modelData.getName()); - } else { - // 使用 ModelData 中已有的 loadFromFile 方法 - modelData = ModelData.loadFromFile(inputFile); - System.out.println("成功从标准序列化文件加载模型数据: " + modelData.getName()); - } - - // 2. 将对象转换为 JSON 格式 - ObjectMapper mapper = new ObjectMapper(); - - // 启用 Pretty Print 使 JSON 文件格式化,方便阅读和调试 - mapper.enable(SerializationFeature.INDENT_OUTPUT); - - // 3. 将 JSON 写入文件 - mapper.writeValue(new File(outputFilePath), modelData); - - System.out.println("成功将数据转换为 JSON 并保存至: " + outputFilePath); - - } catch (IOException e) { - System.err.println("文件操作失败: " + e.getMessage()); - e.printStackTrace(); - } catch (ClassNotFoundException e) { - System.err.println("类定义未找到(ModelData 可能已更改): " + e.getMessage()); - e.printStackTrace(); - } catch (Exception e) { - System.err.println("转换过程中发生未知错误: " + e.getMessage()); - e.printStackTrace(); - } - } - - // 示例运行入口 - public static void main(String[] args) { - String input = "C:\\Users\\Administrator\\Desktop\\testing.model"; - String output = "C:\\Users\\Administrator\\Desktop\\model.json"; - convert(input, output, false); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/window/KeyBindingManager.java b/src/main/java/com/chuangzhou/vivid2D/window/KeyBindingManager.java deleted file mode 100644 index fb9ab19..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/window/KeyBindingManager.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.chuangzhou.vivid2D.window; - -import javax.swing.*; -import java.awt.event.ActionEvent; -import java.awt.event.InputEvent; -import java.awt.event.KeyEvent; - -/** - * 负责管理主窗口的全局快捷键。 - */ -public class KeyBindingManager { - - // Action 名称常量 - private static final String ACTION_SAVE = "saveAction"; - private static final String ACTION_SAVE_AS = "saveAsAction"; - - private final JRootPane rootPane; - private final MainWindow mainWindow; - - public KeyBindingManager(MainWindow mainWindow) { - this.mainWindow = mainWindow; - this.rootPane = mainWindow.getRootPane(); - setupGlobalKeyBindings(); - } - - /** - * 设置全局快捷键绑定到 RootPane。 - * 使用 JComponent.WHEN_IN_FOCUSED_WINDOW 确保在窗口获得焦点时生效。 - */ - private void setupGlobalKeyBindings() { - // 获取 InputMap 和 ActionMap - InputMap inputMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); - ActionMap actionMap = rootPane.getActionMap(); - - // 绑定动作 - actionMap.put(ACTION_SAVE, new SaveAction()); - actionMap.put(ACTION_SAVE_AS, new SaveAsAction()); - - // 绑定快捷键 KeyStroke - bindKey(inputMap, KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK, ACTION_SAVE); - bindKey(inputMap, KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK, ACTION_SAVE_AS); - } - - /** - * 辅助方法:简化 KeyStroke 和 Action 的绑定。 - */ - public void bindKey(InputMap inputMap, int keyCode, int modifiers, String actionName) { - KeyStroke key = KeyStroke.getKeyStroke(keyCode, modifiers); - inputMap.put(key, actionName); - } - - /** - * 内部类:Ctrl + S 保存动作。 - */ - private class SaveAction extends AbstractAction { - @Override - public void actionPerformed(ActionEvent e) { - // 调用 MainWindow 的保存方法 (不退出) - mainWindow.saveData(false); - } - } - - /** - * 内部类:Ctrl + Shift + S 另存为动作。 - */ - private class SaveAsAction extends AbstractAction { - @Override - public void actionPerformed(ActionEvent e) { - // 另存为操作:强制进入 "另存为" 逻辑 - String originalPath = mainWindow.currentModelPath; - - // 临时将路径设为 null - mainWindow.currentModelPath = null; - mainWindow.saveData(false); - - // 如果用户取消另存为 (saveData 中 currentModelPath 仍为 null),恢复原来的路径 - if (mainWindow.currentModelPath == null) { - mainWindow.currentModelPath = originalPath; - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java b/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java deleted file mode 100644 index 012c5ff..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java +++ /dev/null @@ -1,568 +0,0 @@ -package com.chuangzhou.vivid2D.window; - -import com.chuangzhou.vivid2D.render.awt.*; -import com.chuangzhou.vivid2D.render.awt.manager.LayerOperationManager; -import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement; -import com.chuangzhou.vivid2D.render.awt.manager.data.LayerOperationManagerData; -import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData; -import com.chuangzhou.vivid2D.render.awt.tools.VertexDeformationTool; -import com.chuangzhou.vivid2D.render.model.Model2D; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.Mesh2D; -import com.chuangzhou.vivid2D.util.ManagementDataToJsonConverter; -import jnafilechooser.api.JnaFileChooser; -import org.jetbrains.annotations.NotNull; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -/** - * 现代化的主应用程序窗口,布局类似于 Live2D Cubism Editor。 - * 它组织并展示核心的渲染和编辑面板。 - */ -public class MainWindow extends JFrame { - private final ModelRenderPanel renderPanel; - private final ModelLayerPanel layerPanel; - private final TransformPanel transformPanel; - private final ParametersPanel parametersPanel; - private final ModelPartInfoPanel partInfoPanel; - - private final KeyBindingManager keyBindingManager; - public String currentModelPath = null; - private JLabel statusBarLabel; - private JMenuBar menuBar; - private boolean isModelModified = false; - - // 【新增】进度条 - private JProgressBar loadingProgressBar; - - /** - * 构造主窗口。 - */ - public MainWindow() { - setTitle("Vivid2D Editor - [未加载文件]"); - setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - setLayout(new BorderLayout()); - this.renderPanel = new ModelRenderPanel("", 1024, 768); - this.layerPanel = new ModelLayerPanel(renderPanel); - this.transformPanel = new TransformPanel(renderPanel); - this.parametersPanel = new ParametersPanel(renderPanel); - this.partInfoPanel = new ModelPartInfoPanel(renderPanel); - - createMenuBar(); - createToolBar(); - createMainLayout(); - createStatusBar(); - setEditComponentsEnabled(false); - setupInitialListeners(); - setSize(1600, 900); - setLocationRelativeTo(null); - keyBindingManager = new KeyBindingManager(this); - } - - /** - * 创建顶部菜单栏并设置事件。 - */ - private void createMenuBar() { - menuBar = new JMenuBar(); - JMenu fileMenu = new JMenu("文件"); - - // 新增:新建模型菜单项 - JMenuItem newItem = new JMenuItem("新建模型..."); - newItem.addActionListener(e -> createNewModel()); - fileMenu.add(newItem); - - JMenuItem openItem = new JMenuItem("打开模型..."); - openItem.addActionListener(e -> openModelFile()); - fileMenu.add(openItem); - fileMenu.addSeparator(); - - JMenuItem saveItem = new JMenuItem("保存"); - saveItem.setName("saveItem"); - saveItem.addActionListener(e -> saveData(false)); - fileMenu.add(saveItem); - - JMenuItem exitItem = new JMenuItem("退出"); - exitItem.addActionListener(e -> dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING))); - fileMenu.add(exitItem); - - menuBar.add(fileMenu); - JMenu editMenu = new JMenu("编辑"); - editMenu.setName("editMenu"); - menuBar.add(editMenu); - menuBar.add(new JMenu("显示")); - setJMenuBar(menuBar); - } - - /** - * 处理新建模型的操作。 - */ - private void createNewModel() { - String modelName = JOptionPane.showInputDialog(this, "请输入新模型的名称:", "新建模型", JOptionPane.PLAIN_MESSAGE); - if (modelName != null && !modelName.trim().isEmpty()) { - modelName = modelName.trim(); - String finalModelName = modelName; - - // 【进度条】开始 - showProgressBar(true, "正在创建并加载新模型: " + finalModelName); - - SwingUtilities.invokeLater(() -> { - Model2D newModel = new Model2D(finalModelName); - setEditComponentsEnabled(false); - try { - renderPanel.loadModel(newModel); - renderPanel.setParametersManagement(ParametersManagement.getInstance(parametersPanel)); - layerPanel.loadMetadata(); - currentModelPath = null; - setTitle("Vivid2D Editor - " + finalModelName + " [新建]"); - statusBarLabel.setText("新模型 " + finalModelName + " 创建并加载完毕。"); - setEditComponentsEnabled(true); - layerPanel.setModel(newModel); - setModelModified(false); - } catch (Exception e) { - System.err.println("新建模型加载失败: " + e.getMessage()); - currentModelPath = null; - setTitle("Vivid2D Editor - [加载失败]"); - statusBarLabel.setText("新模型加载失败!无法加载: " + finalModelName); - JOptionPane.showMessageDialog(this, - "无法加载新模型: " + finalModelName + "\n错误: " + e.getMessage(), - "加载错误", - JOptionPane.ERROR_MESSAGE); - setEditComponentsEnabled(false); - } finally { - // 【进度条】结束 - showProgressBar(false, ""); - } - }); - } else if (modelName != null) { - JOptionPane.showMessageDialog(this, "模型名称不能为空。", "输入错误", JOptionPane.WARNING_MESSAGE); - } - } - - - /** - * 创建顶部工具栏。 - */ - private void createToolBar() { - JToolBar toolBar = new JToolBar(); - toolBar.setFloatable(false); - toolBar.setName("toolBar"); - toolBar.add(new JButton("建模")); - toolBar.add(new JButton("动画")); - toolBar.addSeparator(); - add(toolBar, BorderLayout.NORTH); - } - - /** - * 创建主工作区布局(左、中、右面板)。 - */ - private void createMainLayout() { - JScrollPane layerScroll = new JScrollPane(layerPanel); - layerScroll.setMinimumSize(new Dimension(240, 100)); - layerScroll.setPreferredSize(new Dimension(260, 600)); - layerScroll.setBorder(BorderFactory.createTitledBorder("图层")); - - JPanel centerPanelWrapper = new JPanel(new BorderLayout()); - centerPanelWrapper.add(renderPanel, BorderLayout.CENTER); - centerPanelWrapper.setMinimumSize(new Dimension(400, 300)); - - JScrollPane transformScroll = new JScrollPane(transformPanel); - transformScroll.setBorder(BorderFactory.createTitledBorder("变换控制")); - transformScroll.setPreferredSize(new Dimension(300, 200)); - - JScrollPane paramScroll = new JScrollPane(parametersPanel); - paramScroll.setBorder(BorderFactory.createTitledBorder("参数管理")); - paramScroll.setPreferredSize(new Dimension(300, 200)); - - JSplitPane infoSplit = new JSplitPane( - JSplitPane.VERTICAL_SPLIT, - partInfoPanel, - paramScroll - ); - infoSplit.setResizeWeight(0.5); - infoSplit.setOneTouchExpandable(true); - infoSplit.setPreferredSize(new Dimension(300, 300)); - - // 右侧面板从上到下:参数、变换控制、顶点信息 - JSplitPane rightPanelSplit = getjSplitPane(paramScroll, transformScroll, infoSplit); - - // 【修改主分割】使用新的 leftPanelSplit 替换原来的 layerScroll - JSplitPane mainSplit = getjSplitPane(new JSplitPane( - JSplitPane.HORIZONTAL_SPLIT, - centerPanelWrapper, - rightPanelSplit - ), 0.75, JSplitPane.HORIZONTAL_SPLIT, layerScroll, 0.2); - - add(mainSplit, BorderLayout.CENTER); - } - - /** - * 辅助方法:创建主分割面板。 - * 修复:将第四个参数从 JScrollPane 更改为 Component。 - */ - private static @NotNull JSplitPane getjSplitPane(JSplitPane HORIZONTAL_SPLIT, double value, int horizontalSplit, Component componentForLeft, double value1) { - HORIZONTAL_SPLIT.setResizeWeight(value); - HORIZONTAL_SPLIT.setOneTouchExpandable(true); - JSplitPane mainSplit = new JSplitPane( - horizontalSplit, - componentForLeft, // 接受 JSplitPane 或 JScrollPane - HORIZONTAL_SPLIT - ); - mainSplit.setResizeWeight(value1); - mainSplit.setOneTouchExpandable(true); - return mainSplit; - } - - // 辅助方法:调整右侧面板的布局逻辑 - private @NotNull JSplitPane getjSplitPane(JScrollPane paramScroll, JScrollPane transformScroll, JSplitPane infoSplit) { - // 上层分割:参数面板 (上) + 下层分割 (下) - JSplitPane upperSplit = new JSplitPane( - JSplitPane.VERTICAL_SPLIT, - paramScroll, - transformScroll - ); - upperSplit.setResizeWeight(0.5); // 参数和变换面板各占一半 - - // 整个右侧面板: upperSplit (上) + infoSplit (下) - JSplitPane rightPanelSplit = new JSplitPane( - JSplitPane.VERTICAL_SPLIT, - upperSplit, - infoSplit // 包含 ModelPartInfoPanel 和 SecondaryVertexPanel 的分割面板 - ); - rightPanelSplit.setResizeWeight(0.66); // 调整上下比例 - rightPanelSplit.setOneTouchExpandable(true); - rightPanelSplit.setPreferredSize(new Dimension(300, 600)); - - return rightPanelSplit; - } - - /** - * 创建底部状态栏。 - */ - private void createStatusBar() { - JPanel statusBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); // 调整间距 - statusBar.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Color.GRAY)); - - // 【新增】进度条初始化 - this.loadingProgressBar = new JProgressBar(); - loadingProgressBar.setIndeterminate(true); // 不确定进度条,表示正在进行中 - loadingProgressBar.setVisible(false); - loadingProgressBar.setPreferredSize(new Dimension(150, 18)); - - this.statusBarLabel = new JLabel("未加载模型。请通过 [文件] -> [打开模型...] 启动编辑。"); - - statusBar.add(statusBarLabel); - statusBar.add(loadingProgressBar); - - add(statusBar, BorderLayout.SOUTH); - } - - /** - * 【新增方法】控制进度条的显示和隐藏,并更新状态标签。 - */ - private void showProgressBar(boolean show, String statusText) { - SwingUtilities.invokeLater(() -> { - if (show) { - loadingProgressBar.setVisible(true); - loadingProgressBar.setIndeterminate(true); - if (statusText != null && !statusText.isEmpty()) { - statusBarLabel.setText(statusText); - } - } else { - loadingProgressBar.setVisible(false); - loadingProgressBar.setIndeterminate(false); - } - }); - } - - /** - * 设置初始的监听器,特别是窗口关闭监听。 - */ - private void setupInitialListeners() { - addWindowListener(new WindowAdapter() { - @Override - public void windowClosing(WindowEvent e) { - if (shouldAskUserToSave()) { - int confirm = JOptionPane.showConfirmDialog( - MainWindow.this, - "模型已修改。是否在退出前保存更改?", - "退出确认", - JOptionPane.YES_NO_CANCEL_OPTION - ); - if (confirm == JOptionPane.CANCEL_OPTION) { - return; - } - if (confirm == JOptionPane.YES_OPTION) { - saveData(true); - } else { - shutdown(); - } - } else { - shutdown(); - } - } - }); - - renderPanel.addModelClickListener(new ModelClickListener() { - @Override - public void onModelClicked(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) { - List selectedPart = renderPanel.getSelectedParts(); - SwingUtilities.invokeLater(() -> { - layerPanel.setSelectedLayers(selectedPart); - transformPanel.setSelectedParts(selectedPart); - if (!selectedPart.isEmpty()) { - setModelModified(true); - ModelPart selected = selectedPart.get(0); - partInfoPanel.updatePanel(selected); - } else { - partInfoPanel.updatePanel(null); - } - - // 当点击模型,但未选中顶点时,清空面板 (依赖 ToolChangeListener) - // 如果 ToolManagement 中没有 getActiveTool(),此处的逻辑依赖 ToolChangeListener - }); - } - - @Override - public void onModelHover(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) { - onModelClicked(mesh, modelX, modelY, screenX, screenY); - } - }); - - VertexDeformationTool vertexDeformationTool = (VertexDeformationTool) renderPanel.getToolManagement().getTool("顶点变形工具"); - renderPanel.getToolManagement().addToolChangeListener(newTool -> SwingUtilities.invokeLater(() -> { - })); - } - - /** - * 根据是否有模型加载来启用或禁用编辑组件。 - */ - private void setEditComponentsEnabled(boolean enabled) { - layerPanel.setEnabled(enabled); - transformPanel.setEnabled(enabled); - parametersPanel.setEnabled(enabled); - partInfoPanel.setEnabled(enabled); - renderPanel.setEnabled(enabled); - for (Component comp : menuBar.getComponents()) { - if (comp instanceof JMenu menu) { - if ("编辑".equals(menu.getName())) { - menu.setEnabled(enabled); - } - for (Component item : menu.getMenuComponents()) { - if ("saveItem".equals(item.getName())) { - item.setEnabled(enabled); - } - } - } - } - for (Component comp : getContentPane().getComponents()) { - if (comp instanceof JToolBar && "toolBar".equals(comp.getName())) { - comp.setEnabled(enabled); - for (Component button : ((JToolBar) comp).getComponents()) { - button.setEnabled(enabled); - } - } - } - } - - /** - * 打开文件对话框并加载模型。 - */ - private void openModelFile() { - JnaFileChooser jnaFileChooser = new JnaFileChooser(); - jnaFileChooser.setTitle("选择 Vivid2D 模型文件 (*.model)"); - jnaFileChooser.addFilter("Vivid2D 模型文件 (*.model)", "model"); - jnaFileChooser.setMultiSelectionEnabled(false); - jnaFileChooser.setMode(JnaFileChooser.Mode.Files); - if (jnaFileChooser.showOpenDialog(this)) { - File file = jnaFileChooser.getSelectedFile(); - loadModel(file.getAbsolutePath()); - } - } - - /** - * 加载模型并更新 UI 状态。 - */ - public void loadModel(String modelPath) { - setEditComponentsEnabled(false); - - // 【进度条】开始 - showProgressBar(true, "正在加载模型: " + modelPath); - - CompletableFuture.runAsync(() -> { - Model2D model = null; - try { - // 假设 renderPanel.loadModel(String modelPath) 返回一个 CompletableFuture - model = renderPanel.loadModel(modelPath).get(); - } catch (InterruptedException | ExecutionException e) { - System.err.println("模型异步加载失败: " + e.getMessage()); - } - Model2D finalModel = model; - SwingUtilities.invokeLater(() -> { - // 【进度条】结束 - showProgressBar(false, ""); - - if (finalModel == null || !renderPanel.getGlContextManager().isRunning()) { - currentModelPath = null; - setTitle("Vivid2D Editor - [加载失败]"); - statusBarLabel.setText("模型加载失败!无法加载: " + modelPath); - JOptionPane.showMessageDialog(this, - "无法加载模型: " + modelPath, - "加载错误", - JOptionPane.ERROR_MESSAGE); - } else { - renderPanel.setParametersManagement(ParametersManagement.getInstance(parametersPanel)); - layerPanel.loadMetadata(); - currentModelPath = modelPath; - setTitle("Vivid2D Editor - " + new File(modelPath).getName()); - statusBarLabel.setText("模型加载完毕。"); - setEditComponentsEnabled(true); - layerPanel.setModel(finalModel); - setModelModified(false); - } - }); - }); - } - - /** - * 保存模型和参数数据。 - * @param exitOnComplete 如果为 true,则在保存后调用 shutdown()。 - */ - public void saveData(boolean exitOnComplete) { - if (currentModelPath == null) { - JnaFileChooser jnaFileChooser = getJnaFileChooser(); - if (jnaFileChooser.showSaveDialog(this)) { - File fileToSave = jnaFileChooser.getSelectedFile(); - String path = fileToSave.getAbsolutePath(); - if (!path.toLowerCase().endsWith(".model")) { - path += ".model"; - fileToSave = new File(path); - } - this.currentModelPath = path; - setTitle("Vivid2D Editor - " + fileToSave.getName()); - } else { - statusBarLabel.setText("保存操作已取消。"); - return; - } - } - - // 【进度条】开始 - showProgressBar(true, "正在保存模型..."); - - // 将保存操作放入后台线程,以避免阻塞 EDT - new SwingWorker() { - @Override - protected Void doInBackground() throws Exception { - if (renderPanel.getModel() != null) { - renderPanel.getModel().saveToFile(currentModelPath); - } - LayerOperationManager layerManager = layerPanel.getLayerOperationManager(); - LayerOperationManagerData layerData = new LayerOperationManagerData(layerManager.layerMetadata); - ParametersManagementData managementData = new ParametersManagementData(renderPanel.getParametersManagement()); - String managementFilePath = currentModelPath + ".data"; - String managementJsonFilePath = managementFilePath + ".json"; - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(managementFilePath))) { - oos.writeObject(layerData); - oos.writeObject(managementData); - } catch (IOException ex) { - throw ex; - } - ManagementDataToJsonConverter.convert(managementFilePath, managementJsonFilePath); - return null; - } - - @Override - protected void done() { - // 【进度条】结束 - showProgressBar(false, ""); - - try { - get(); // 检查是否有异常抛出 - statusBarLabel.setText("保存成功。"); - setModelModified(false); - if (exitOnComplete) { - shutdown(); - } - } catch (InterruptedException ignore) { - // 线程中断,通常发生在关闭时 - } catch (ExecutionException e) { - // 实际异常被包装在 ExecutionException 中 - Throwable cause = e.getCause(); - e.printStackTrace(); - System.err.println("保存失败: " + cause.getMessage()); - statusBarLabel.setText("保存失败!错误: " + cause.getMessage()); - JOptionPane.showMessageDialog(MainWindow.this, - "保存操作失败: " + cause.getMessage(), - "保存错误", - JOptionPane.ERROR_MESSAGE); - } - } - }.execute(); - } - - private @NotNull JnaFileChooser getJnaFileChooser() { - JnaFileChooser jnaFileChooser = new JnaFileChooser(); - jnaFileChooser.setTitle("另存为 Vivid2D 模型文件 (*.model)"); - jnaFileChooser.addFilter("Vivid2D 模型文件 (*.model)", "model"); - jnaFileChooser.setMultiSelectionEnabled(false); - jnaFileChooser.setOpenButtonText("保存"); - jnaFileChooser.setMode(JnaFileChooser.Mode.Files); - String defaultFileName; - Model2D currentModel = renderPanel.getModel(); - if (currentModel != null && currentModel.getName() != null && !currentModel.getName().trim().isEmpty()) { - defaultFileName = currentModel.getName() + ".model"; - } else { - defaultFileName = "model.model"; - } - jnaFileChooser.setDefaultFileName(defaultFileName); - return jnaFileChooser; - } - - /** - * 获取模型是否已修改的状态。 - */ - public boolean isModelModified() { - return isModelModified; - } - - /** - * 编辑面板在进行任何修改操作时应调用 setModelModified(true)。 - * 保存或加载成功后应调用 setModelModified(false)。 - */ - public void setModelModified(boolean modified) { - this.isModelModified = modified; - } - - /** - * 判断当前是否需要询问用户保存模型。 - * 只要模型被修改过,就应该询问。 - */ - public boolean shouldAskUserToSave() { - return isModelModified; - } - - public KeyBindingManager getKeyBindingManager() { - return keyBindingManager; - } - - /** - * 清理资源并退出应用程序。 - */ - private void shutdown() { - statusBarLabel.setText("正在关闭..."); - try { - renderPanel.getGlContextManager().dispose(); - } catch (Throwable ignored) {} - dispose(); - System.exit(0); - } -} \ No newline at end of file diff --git a/src/main/java/org/cef/CefApp.java b/src/main/java/org/cef/CefApp.java index bd23199..82d0517 100644 --- a/src/main/java/org/cef/CefApp.java +++ b/src/main/java/org/cef/CefApp.java @@ -149,7 +149,7 @@ public class CefApp extends CefAppHandlerAdapter { SystemBootstrap.loadLibrary("jawt"); LibraryLoad.loadLibrary("jcef/lib/win64/chrome_elf"); //SystemBootstrap.loadLibrary("libcef"); - LibraryLoad.loadLibrary("jcef/lib/win64/libcef.dll"); + LibraryLoad.loadLibrary("jcef/lib/win64/libcef"); // Other platforms load this library in CefApp.startup(). //SystemBootstrap.loadLibrary("jcef"); diff --git a/src/main/java/org/tzd/awt/GLAwt.java b/src/main/java/org/tzd/awt/GLAwt.java new file mode 100644 index 0000000..ce2af30 --- /dev/null +++ b/src/main/java/org/tzd/awt/GLAwt.java @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2025 tzdwindows 7. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.tzd.awt; + +/** + * GLAwt - Java OpenGL 与 AWT/Swing 窗口系统集成工具类 + * + *

本类提供了在 Java AWT/Swing 环境中初始化和使用 OpenGL 渲染上下文的基本功能。 + * 通过 JNI(Java Native Interface)调用原生系统库实现窗口系统的集成。

+ * + *

许可证信息

+ *

本项目基于 Mozilla Public License 2.0 (MPL-2.0) 开源许可证发布。 + * 这意味着您可以自由地使用、修改和分发本代码,但需要遵守以下条件:

+ *
    + *
  • 如果您修改了本文件,必须保留原有的版权声明和许可证信息
  • + *
  • 如果您分发包含本文件修改版本的软件,必须将修改后的源代码以 MPL-2.0 许可证公开
  • + *
  • 您可以将本代码与私有代码结合使用,但本文件本身始终受 MPL-2.0 约束
  • + *
+ * + *

完整的许可证文本请访问: https://mozilla.org/MPL/2.0/

+ * + *

使用前提

+ *
    + *
  1. 需要对应的原生库文件(Windows: GLAwt.dll, Linux: libGLAwt.so, macOS: libGLAwt.dylib)
  2. + *
  3. 原生库文件必须位于 Java 库路径中(可通过 -Djava.library.path 指定)
  4. + *
  5. 系统需支持 OpenGL 图形接口
  6. + *
  7. 建议使用 OpenGL 3.3 或更高版本以获得最佳兼容性
  8. + *
+ * + *

示例用法

+ *
+ * // 创建 OpenGL 渲染画布
+ * Canvas canvas = new Canvas();
+ * frame.add(canvas);
+ *
+ * // 初始化 GL 上下文
+ * long context = GLAwt.init(canvas);
+ *
+ * try {
+ *     // 激活上下文
+ *     GLAwt.makeCurrent(context);
+ *
+ *     // 设置垂直同步
+ *     GLAwt.setVSync(context, true);
+ *
+ *     // GL 渲染循环
+ *     while (!Thread.currentThread().isInterrupted()) {
+ *         // OpenGL 渲染代码...
+ *         glClear(GL_COLOR_BUFFER_BIT);
+ *
+ *         // 交换缓冲区
+ *         GLAwt.swapBuffers(context);
+ *     }
+ * } finally {
+ *     // 确保释放资源
+ *     GLAwt.destroy(context);
+ * }
+ * 
+ * + * @author [您的姓名/组织名称] + * @version 1.0.0 + * @since 2025-01-01 + */ +public class GLAwt { + + // 静态初始化块:加载原生库 + static { + try { + // 尝试加载名为 "GLAwt" 的原生库 + System.load("C:\\Users\\Administrator\\source\\repos\\GLAwt\\x64\\Release\\GLAwt.dll"); + } catch (UnsatisfiedLinkError e) { + System.err.println("无法加载原生库: " + e.getMessage()); + System.err.println("请确保:"); + System.err.println("1. 原生库文件存在且可访问"); + System.err.println("2. Java 库路径配置正确(-Djava.library.path)"); + System.err.println("3. 操作系统架构匹配(32位/64位)"); + System.err.println("4. 必要的系统依赖库已安装(如 OpenGL 运行时库)"); + e.printStackTrace(); + } + } + + /** + * 初始化 OpenGL 渲染上下文并与指定的 AWT 组件关联 + * + *

此方法创建一个与指定 AWT 窗口组件关联的 OpenGL 上下文。 + * 创建的上下文是跨平台的抽象,内部封装了不同操作系统的原生资源。

+ * + *

线程安全说明:此方法应在 AWT 事件分发线程(EDT)中调用, + * 因为涉及窗口系统资源的获取。但实际渲染可以在单独的渲染线程中进行。

+ * + *

资源管理:返回的上下文句柄必须在使用完毕后通过 + * {@link #destroy(long)} 方法释放,以避免资源泄漏。

+ * + * @param component AWT 窗口组件,必须是 {@link java.awt.Canvas}、{@link java.awt.Panel} + * 或 {@link java.awt.Window} 的实例,用于绑定 OpenGL 渲染表面。 + * 不支持无窗口组件或透明组件。 + * @return 成功创建的 OpenGL 上下文句柄,作为不透明的 64 位长整型值。 + * 该句柄内部封装了平台特定的资源引用,不应被解释为实际的指针值。 + * 如果初始化失败,返回 0。 + * @throws NullPointerException 如果 component 参数为 null + * @throws IllegalArgumentException 如果 component 参数不是有效的 AWT 窗口组件 + * @throws IllegalStateException 如果底层窗口系统不支持 OpenGL, + * 或必要的系统资源不可用 + * @throws UnsatisfiedLinkError 如果原生库加载失败或原生方法不存在 + * + * @see #destroy(long) + * @see #makeCurrent(long) + * @see java.awt.Canvas + * @see java.awt.Window + */ + public static native long init(Object component); + + /** + * 交换 OpenGL 渲染上下文的前后缓冲区(Swap Buffers) + * + *

此方法将当前渲染的后缓冲区与显示的前缓冲区交换,使渲染结果可见于屏幕。 + * 这是双缓冲渲染机制的关键操作,通常在每一帧渲染结束后调用。

+ * + *

线程要求:此方法必须在拥有 OpenGL 上下文的渲染线程中调用。 + * 在调用前,应确保上下文已通过 {@link #makeCurrent(long)} 在当前线程激活。

+ * + *

性能说明:此操作可能会被垂直同步(VSync)阻塞。 + * 可以使用 {@link #setVSync(long, boolean)} 控制垂直同步行为。

+ * + * @param context OpenGL 上下文句柄,由 {@link #init} 方法返回 + * @return 交换操作成功返回 {@code true},失败返回 {@code false}。 + * 失败原因可能包括: + *
    + *
  • 无效的上下文句柄
  • + *
  • 上下文未在当前线程激活
  • + *
  • 窗口表面已失效(如窗口被关闭)
  • + *
  • 系统图形资源不足
  • + *
+ * @throws IllegalStateException 如果当前线程未拥有有效的 GL 上下文 + * + * @see #init(Object) + * @see #makeCurrent(long) + * @see #setVSync(long, boolean) + */ + public static native boolean swapBuffers(long context); + + /** + * 将指定的 OpenGL 上下文设置为当前线程的激活上下文 + * + *

此方法将 OpenGL 上下文与调用线程关联,使后续的所有 OpenGL 调用 + * 都在此上下文中执行。OpenGL 上下文是线程相关的,一个线程同一时间 + * 只能激活一个上下文。

+ * + *

重要:在调用任何 OpenGL 函数(包括 {@link #swapBuffers(long)}) + * 之前,必须先在当前线程激活上下文。

+ * + *

线程同步:如果一个上下文需要在多个线程中使用, + * 必须由应用程序确保适当的同步,避免多个线程同时操作同一上下文。

+ * + * @param context OpenGL 上下文句柄,由 {@link #init} 方法返回 + * @throws IllegalStateException 如果上下文句柄无效, + * 或上下文已在另一个线程激活且不支持共享 + * @throws UnsatisfiedLinkError 如果原生库调用失败 + * + * @see #init(Object) + * @see #destroy(long) + */ + public static native void makeCurrent(long context); + + /** + * 释放 OpenGL 上下文及其关联的所有系统资源 + * + *

此方法销毁 OpenGL 上下文并释放所有关联的原生资源,包括: + * 图形设备上下文(Windows DC)、X11 显示连接(Linux)、CGL 上下文(macOS)等。

+ * + *

重要:此方法必须在所有使用该上下文的渲染操作完成后调用。 + * 一旦调用此方法,上下文句柄将失效,不能再用于任何后续操作。

+ * + *

资源管理:为预防资源泄漏,建议在 try-finally 块中 + * 确保此方法被调用,或使用 try-with-resources 模式封装上下文生命周期。

+ * + * @param context 要销毁的 OpenGL 上下文句柄。 + * 传入 0 或已销毁的句柄是安全的(无操作)。 + * @throws IllegalStateException 如果尝试在仍有未完成渲染操作时销毁上下文 + * + * @see #init(Object) + * @see java.lang.AutoCloseable + */ + public static native void destroy(long context); + + /** + * 启用或禁用垂直同步(Vertical Synchronization) + * + *

垂直同步控制缓冲区交换与显示器刷新率的同步。启用时(默认): + * - 防止画面撕裂(tearing) + * - 限制帧率不超过显示器刷新率 + * - 可能导致额外的渲染延迟

+ * + *

禁用时: + * - 允许无限制的帧率(GPU 限制) + * - 可能产生画面撕裂 + * - 减少渲染延迟

+ * + *

平台支持:此功能需要底层驱动支持相应的扩展 + * (如 WGL_EXT_swap_control、GLX_EXT_swap_control 等)。 + * 在不支持的系统上,此调用可能被静默忽略。

+ * + * @param context OpenGL 上下文句柄 + * @param enabled 为 {@code true} 时启用垂直同步,为 {@code false} 时禁用 + * @throws IllegalStateException 如果上下文未在当前线程激活 + * @throws IllegalArgumentException 如果上下文句柄无效 + * + * @see #swapBuffers(long) + */ + public static native void setVSync(long context, boolean enabled); + + /** + * 获取最后发生的错误信息 + * + *

此方法返回本机库中最后发生的错误描述。适用于调试和错误报告。 + * 错误信息是平台特定的,可能包含系统错误代码或 OpenGL 错误状态。

+ * + *

注意:此方法不保证返回完整的错误历史,某些错误 + * 可能被后续成功操作覆盖。建议在操作失败后立即调用此方法。

+ * + *

线程安全:返回的字符串是当前线程的最后错误状态。 + * 多线程环境下,每个线程维护独立的错误状态。

+ * + * @return 错误描述字符串,如果没有错误发生则返回空字符串或 "No error"。 + * 返回的字符串适合记录到日志或显示给用户(已本地化)。 + * + * @see #init(Object) + * @see #swapBuffers(long) + */ + public static native String getLastError(); + + /** + * 检查当前线程是否拥有激活的 OpenGL 上下文 + * + *

此工具方法可用于调试和运行时检查,确保 OpenGL 调用在正确的线程上下文中执行。

+ * + *

实现说明:此方法通过尝试获取当前 OpenGL 错误状态 + * 来推断上下文是否存在。某些 OpenGL 实现可能支持更直接的查询方式。

+ * + * @return 如果当前线程有激活的 OpenGL 上下文返回 {@code true}, + * 否则返回 {@code false} + */ + public static native boolean hasCurrentContext(); + + /** + * 获取当前线程关联的 OpenGL 上下文句柄 + * + *

此方法返回当前线程最近通过 {@link #makeCurrent(long)} 方法设置的 + * OpenGL 上下文句柄。该句柄最初由 {@link #init(Object)} 方法创建, + * 可在后续的 OpenGL 操作中用于重新激活上下文。

+ * + *

线程安全性:此方法返回的是线程本地的上下文引用, + * 每个线程维护独立的上下文状态。如果当前线程尚未通过 {@link #makeCurrent(long)} + * 设置过任何上下文,将返回 0。

+ * + *

典型使用模式:

+ *
{@code
+     * long context = GLAwt.getContext();
+     * if (context != 0) {
+     *     // 当前线程已有激活的上下文
+     *     // 可在此执行 OpenGL 操作
+     * } else {
+     *     // 需要先通过 makeCurrent() 设置上下文
+     *     context = GLAwt.init(component);
+     *     GLAwt.makeCurrent(context);
+     * }
+     * }
+ * + *

返回值说明:

+ *
    + *
  • 大于 0:有效的 OpenGL 上下文句柄
  • + *
  • 等于 0:当前线程没有激活的 OpenGL 上下文
  • + *
+ * + *

注意:返回的句柄是 JNI 层 {@code GLContext*} 结构体的指针值, + * 仅在当前进程内有效,不应跨进程传递或长期存储。上下文生命周期应由 + * {@link #init(Object)} 和 {@link #destroy(long)} 配对管理。

+ * + * @return 当前线程的 OpenGL 上下文句柄,如果没有激活的上下文则返回 0 + * @see #hasCurrentContext() + * @see #init(Object) + * @see #makeCurrent(long) + * @see #destroy(long) + */ + public static native long getContext(); + + /** + * 获取库版本信息 + * + * @return 库版本字符串,格式为 "主版本.次版本.修订版本[ 后缀]" + */ + public static String getVersion() { + return "1.0.0"; + } + + /** + * 获取支持的平台列表 + * + * @return 支持的操作系统平台数组 + */ + public static String[] getSupportedPlatforms() { + return new String[] { + "Windows (WGL)", + "Linux (GLX)", + "macOS (CGL)" + }; + } + + /** + * 私有构造函数,防止类被实例化 + * + *

这是一个工具类,所有方法都是静态的,不应创建其实例。 + * 违反此约束将抛出 {@link AssertionError}。

+ * + * @throws AssertionError 总是抛出,防止实例化 + */ + private GLAwt() { + throw new AssertionError("GLAwt 是一个工具类,不应被实例化"); + } +} \ No newline at end of file diff --git a/src/main/resources/build/build.properties b/src/main/resources/build/build.properties new file mode 100644 index 0000000..7f2ac7e --- /dev/null +++ b/src/main/resources/build/build.properties @@ -0,0 +1,4 @@ +# Auto-generated build information +version=0.0.1 +buildTimestamp=2026-01-02T17:08:37.387878 +buildSystem=WINDOWS diff --git a/src/main/resources/icons/database_icon.png b/src/main/resources/icons/database_icon.png new file mode 100644 index 0000000..07603a5 Binary files /dev/null and b/src/main/resources/icons/database_icon.png differ diff --git a/src/main/resources/icons/programming/linux.png b/src/main/resources/icons/programming/linux.png new file mode 100644 index 0000000..2f30d74 Binary files /dev/null and b/src/main/resources/icons/programming/linux.png differ diff --git a/src/main/resources/icons/programming/mysql.png b/src/main/resources/icons/programming/mysql.png new file mode 100644 index 0000000..3fbbeed Binary files /dev/null and b/src/main/resources/icons/programming/mysql.png differ diff --git a/src/main/resources/icons/startup_background.png b/src/main/resources/icons/startup_background.png new file mode 100644 index 0000000..c731e46 Binary files /dev/null and b/src/main/resources/icons/startup_background.png differ diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index 6f3b3a9..b936a91 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,7 +1,13 @@ - + + - + + + + + + @@ -15,12 +21,11 @@ - + + immediateFlush="true"> %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n @@ -29,14 +34,17 @@ - + + + - + + diff --git a/src/main/resources/web/blockly_editor.html b/src/main/resources/web/blockly_editor.html deleted file mode 100644 index a0629f4..0000000 --- a/src/main/resources/web/blockly_editor.html +++ /dev/null @@ -1,239 +0,0 @@ - - - - - Blockly for Vivid2D - 最终修正版 - - - - -
- -
日志:初始化中...
-
-
- - - - - - diff --git a/src/main/resources/web/custom_blocks.js b/src/main/resources/web/custom_blocks.js deleted file mode 100644 index f3787aa..0000000 --- a/src/main/resources/web/custom_blocks.js +++ /dev/null @@ -1,81 +0,0 @@ -// custom_blocks.js — 更稳健的注册方式(只替换此文件) -(function() { - // 在全局上只注册一次 - if (window.__vivid2d_custom_blocks_registered) return; - window.__vivid2d_custom_blocks_registered = true; - - // 尝试获取 Blockly,若不存在则轮询直到就绪(最多重试 200 次,约 10s) - var attempts = 0; - var maxAttempts = 200; - function tryRegister() { - attempts++; - if (typeof Blockly !== 'undefined' && Blockly.defineBlocksWithJsonArray && Blockly.JavaScript) { - // 定义积木 - Blockly.defineBlocksWithJsonArray([ - { - "type": "move_object", - "message0": "移动对象 %1 到 x: %2 y: %3", - "args0": [ - { "type": "field_input", "name": "OBJECT_ID", "text": "player" }, - { "type": "field_number", "name": "X_POS", "value": 0 }, - { "type": "field_number", "name": "Y_POS", "value": 0 } - ], - "previousStatement": null, - "nextStatement": null, - "colour": 230, - "tooltip": "移动一个指定ID的对象到新的坐标", - "helpUrl": "" - }, - { - "type": "change_color", - "message0": "改变对象 %1 的颜色为 %2 (例如 #ff0000)", - "args0": [ - { "type": "field_input", "name": "OBJECT_ID", "text": "player" }, - { "type": "field_input", "name": "COLOR", "text": "#ff0000" } - ], - "previousStatement": null, - "nextStatement": null, - "colour": 20, - "tooltip": "改变一个指定ID的对象的颜色(输入十六进制或颜色名)", - "helpUrl": "" - } - ]); - - // 安全地注册 JavaScript 生成器:先检查命名空间是否存在 - Blockly.JavaScript = Blockly.JavaScript || {}; - - Blockly.JavaScript['move_object'] = function(block) { - var objectId = (block.getFieldValue('OBJECT_ID') || '').replace(/'/g, "\\'"); - var xPos = Number(block.getFieldValue('X_POS') || 0); - var yPos = Number(block.getFieldValue('Y_POS') || 0); - return "callJava('moveObject', { objectId: '" + objectId + "', x: " + xPos + ", y: " + yPos + " });\n"; - }; - - Blockly.JavaScript['change_color'] = function(block) { - var objectId = (block.getFieldValue('OBJECT_ID') || '').replace(/'/g, "\\'"); - var color = (block.getFieldValue('COLOR') || '').replace(/'/g, "\\'"); - return "callJava('changeColor', { objectId: '" + objectId + "', colorHex: '" + color + "' });\n"; - }; - - // 兼容:如果 workspaceToCode 在某些版本内部使用不同的生成器对象路径,提供一个宽松的回退 - try { - if (!Blockly.Generator.prototype['blockToCode']) { - // nothing — 只避免某些极端环境报错 - } - } catch (e) { - // ignore - } - - console.log('custom_blocks.js: 自定义积木与生成器已注册。'); - return; - } - - if (attempts < maxAttempts) { - setTimeout(tryRegister, 50); - } else { - console.error('custom_blocks.js: 超时,未检测到 Blockly(请确认 blockly.min.js 已正确加载)'); - } - } - - tryRegister(); -})();