chore(jcef): 更新缓存数据库日志文件

- 更新 shared_proto_db/metadata/000003.log 文件内容
- 更新 Site Characteristics Database/00003.log 文件内容
- 添加新的数据库条目和元数据记录
- 保持数据库文件格式的一致性
- 删除Vivid2D的内容
- 重写启动加载界面
This commit is contained in:
2026-01-02 17:12:54 +08:00
parent 75bdca05f2
commit 8de2b0f2fe
261 changed files with 4375 additions and 59810 deletions

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);

View File

@@ -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<BrowserWindow> 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<BrowserWindowJDialog> 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();

View File

@@ -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<BrowserWindow> 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<String, String> 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"};
}
}
}

View File

@@ -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"));

View File

@@ -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;
}
}
/**

View File

@@ -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

View File

@@ -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) {}
}
}

View File

@@ -1,5 +1,7 @@
package com.axis.innovators.box.util;
import com.axis.innovators.box.util.build.BuildSystem;
import java.io.IOException;
/**

View File

@@ -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<RegisterTray.Item> menuItems = null;
private static final List<Integer> idx = new ArrayList<>();
private static final List<TrayLabels> 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() {

View File

@@ -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;
}
}

View File

@@ -0,0 +1,8 @@
package com.axis.innovators.box.util.build;
public enum BuildSystem {
UNKNOWN,
WINDOWS,
LINUX,
MACOS
}

View File

@@ -0,0 +1,4 @@
# build.properties
Version=1.0.0
Build_Timestamp=2023-10-01T12:00:00Z
Build_System=LINUX

View File

@@ -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

View File

@@ -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 7UI 改造版)
* 现代简约风格启动窗口 - 已添加 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<String, Integer> 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);
}
});
}
}
}
}

View File

@@ -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);
});
}
}

View File

@@ -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;
}

View File

@@ -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<String, Class<?>> models = new ConcurrentHashMap<>();
private final Map<String, List<String>> modelsByCategory = new ConcurrentHashMap<>();
private final List<String> modelDisplayNames = new ArrayList<>();
private final Map<String, String> 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<String> getAllModelDisplayNames() {
return Collections.unmodifiableList(modelDisplayNames);
}
/**
* 获取所有模型的注册名称
*/
public Set<String> getAllModelRegistrationNames() {
return Collections.unmodifiableSet(models.keySet());
}
/**
* 按类别获取模型注册名称
*/
public List<String> getModelsByCategory(String category) {
return Collections.unmodifiableList(
modelsByCategory.getOrDefault(category, new ArrayList<>())
);
}
/**
* 获取所有可用的类别
*/
public Set<String> 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<String, String> 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<String> 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;
}
}

View File

@@ -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<Integer, String> labels;
// 类别名称 -> ARGB 颜色
private final Map<String, Integer> palette;
public SegmentationResult(BufferedImage maskImage, Map<Integer, String> labels, Map<String, Integer> palette) {
this.maskImage = maskImage;
this.labels = labels;
this.palette = palette;
}
public BufferedImage getMaskImage() {
return maskImage;
}
public Map<Integer, String> getLabels() {
return labels;
}
public Map<String, Integer> getPalette() {
return palette;
}
}

View File

@@ -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<Image, Segmenter.SegmentationData> modelWrapper;
protected final Predictor<Image, Segmenter.SegmentationData> predictor;
protected final List<String> labels;
protected final Map<String, Integer> palette;
public Segmenter(Path modelDir, List<String> labels) throws IOException, MalformedModelException, ModelNotFoundException {
this.labels = new ArrayList<>(labels);
this.palette = BiSeNetLabelPalette.defaultPalette();
Translator<Image, Segmenter.SegmentationData> translator = new Translator<Image, Segmenter.SegmentationData>() {
@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<Image, Segmenter.SegmentationData> 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<Integer, String> 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) {
}
}
}

View File

@@ -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<s extends Segmenter> implements AutoCloseable{
protected final s segmenter;
protected final List<String> labels; // index -> name
protected final Map<String, Integer> palette; // name -> ARGB
protected VividModelWrapper(s segmenter, List<String> labels, Map<String, Integer> palette) {
this.segmenter = segmenter;
this.labels = labels;
this.palette = palette;
}
public List<String> getLabels() {
return Collections.unmodifiableList(labels);
}
public Map<String, Integer> getPalette() {
return Collections.unmodifiableMap(palette);
}
/**
* 直接返回分割结果SegmentationResult
*/
public SegmentationResult segment(File inputImage) throws Exception {
return segmenter.segment(inputImage);
}
/**
* 把指定 targets标签名集合从输入图片中分割并保存到 outDir。
* 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。
* <p>
* 返回值Map<labelName, ResultFiles>ResultFiles 包含 maskFile、overlayFile两个 PNG
*/
public abstract Map<String, ResultFiles> segmentAndSave(File inputImage, Set<String> targets, Path outDir) throws Exception;
protected static String safeFileName(String s) {
return s.replaceAll("[^a-zA-Z0-9_\\-\\.]", "_");
}
protected static Set<String> parseTargetsSet(Set<String> 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<String> 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<List<String>> loadLabelsFromSynset(Path modelDir) {
Path syn = modelDir.resolve("synset.txt");
if (Files.exists(syn)) {
try {
List<String> lines = Files.readAllLines(syn);
List<String> 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;
}
}
}

View File

@@ -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<String> 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<String, Integer> defaultPalette() {
Map<String, Integer> 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<Integer, String> getIndexToLabelMap() {
List<String> labels = defaultLabels();
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < labels.size(); i++) {
map.put(i, labels.get(i));
}
return map;
}
}

View File

@@ -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<AnimeSegmenter> {
private AnimeModelWrapper(AnimeSegmenter segmenter, List<String> labels, Map<String, Integer> palette) {
super(segmenter, labels, palette);
}
/**
* 加载模型
*/
public static AnimeModelWrapper load(Path modelDir) throws Exception {
List<String> labels = loadLabelsFromSynset(modelDir).orElseGet(AnimeLabelPalette::defaultLabels);
AnimeSegmenter segmenter = new AnimeSegmenter(modelDir, labels);
Map<String, Integer> 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<String, ResultFiles> segmentAndSave(File inputImage, Set<String> 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<String> realTargets = parseTargetsSet(targets);
Map<String, ResultFiles> saved = new LinkedHashMap<>();
for (String target : realTargets) {
if (!palette.containsKey(target)) {
// 尝试忽略大小写匹配
String finalTarget = target;
Optional<String> 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};
}
}

View File

@@ -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<Integer, String> labels;
// 类别名称 -> ARGB 颜色
private final Map<String, Integer> palette;
public AnimeSegmentationResult(BufferedImage maskImage, float[][][] probabilityMap,
Map<Integer, String> labels, Map<String, Integer> 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<Integer, String> getLabels() {
return labels;
}
public Map<String, Integer> 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;
}
}

View File

@@ -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<Image, SegmentationData> modelWrapper;
private final Predictor<Image, SegmentationData> predictor;
private final Map<String, Integer> palette;
public AnimeSegmenter(Path modelDir, List<String> labels) throws IOException, MalformedModelException, ModelNotFoundException {
super(modelDir, labels);
this.palette = AnimeLabelPalette.defaultPalette();
Translator<Image, SegmentationData> translator = new Translator<Image, SegmentationData>() {
@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<Image, SegmentationData> 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<Integer, String> 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) {
}
}
}

View File

@@ -1,46 +0,0 @@
package com.chuangzhou.vivid2D.ai.anime_segmentation;
import java.util.*;
/**
* 动漫分割模型的标签和颜色调色板。
* 这是一个二分类模型:背景和前景(动漫人物)
*/
public class Anime2LabelPalette {
/**
* 动漫分割模型的标准标签2个类别
*/
public static List<String> defaultLabels() {
return Arrays.asList(
"background", // 0
"foreground" // 1
);
}
/**
* 返回动漫分割模型的调色板
*/
public static Map<String, Integer> defaultPalette() {
Map<String, Integer> map = new HashMap<>();
// 索引 0: background - 黑色
map.put("background", 0xFF000000);
// 索引 1: foreground - 白色
map.put("foreground", 0xFFFFFFFF);
return map;
}
/**
* 专门为动漫分割模型设计的调色板(可视化更友好)
*/
public static Map<String, Integer> animeSegmentationPalette() {
Map<String, Integer> map = new HashMap<>();
// 背景 - 透明
map.put("background", 0x00000000);
// 前景 - 红色(用于可视化)
map.put("foreground", 0xFFFF0000);
return map;
}
}

View File

@@ -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<Integer, String> labels, Map<String, Integer> palette) {
super(maskImage, labels, palette);
}
}

View File

@@ -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<String> 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<Integer, String> 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) {
}
}
}

View File

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

View File

@@ -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<String> 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<String, Integer> defaultPalette() {
Map<String, Integer> 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;
}
}

View File

@@ -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<Integer, String> labels, Map<String, Integer> palette) {
super(maskImage, labels, palette);
}
}

View File

@@ -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<String> 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();
}
}

View File

@@ -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<String, VividModelWrapper.ResultFiles> 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<BiSeNetSegmenter> {
private BiSeNetVividModelWrapper(BiSeNetSegmenter segmenter, List<String> labels, Map<String, Integer> palette) {
super(segmenter, labels, palette);
}
/**
* 读取 modelDir/synset.txt每行一个标签若不存在则使用 LabelPalette.defaultLabels()
* 并创建 Segmenter 实例。
*/
public static BiSeNetVividModelWrapper load(Path modelDir) throws Exception {
List<String> labels = loadLabelsFromSynset(modelDir).orElseGet(BiSeNetLabelPalette::defaultLabels);
BiSeNetSegmenter s = new BiSeNetSegmenter(modelDir, labels);
Map<String, Integer> palette = BiSeNetLabelPalette.defaultPalette();
return new BiSeNetVividModelWrapper(s, labels, palette);
}
public List<String> getLabels() {
return super.getLabels();
}
public Map<String, Integer> getPalette() {
return super.getPalette();
}
/**
* 直接返回分割结果SegmentationResult
*/
public SegmentationResult segment(File inputImage) throws Exception {
return segmenter.segment(inputImage);
}
/**
* 把指定 targets标签名集合从输入图片中分割并保存到 outDir。
* 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。
* <p>
* 返回值Map<labelName, ResultFiles>ResultFiles 包含 maskFile、overlayFile两个 PNG
*/
public Map<String, VividModelWrapper.ResultFiles> segmentAndSave(File inputImage, Set<String> 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<String> realTargets = parseTargetsSet(targets);
Map<String, ResultFiles> saved = new LinkedHashMap<>();
for (String target : realTargets) {
if (!palette.containsKey(target)) {
String finalTarget = target;
Optional<String> 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) {}
}
}

View File

@@ -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<String, Object> 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;
}
}
}

View File

@@ -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<String, Object> params;
}
}

View File

@@ -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<BrowserWindowJDialog> 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
);
}
});
}
}

View File

@@ -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 渲染器代码
}
}

View File

@@ -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 completewindow.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<String> 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);
});
}
}

View File

@@ -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 completewindow.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 completewindow.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<String> 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);
});
}
}

View File

@@ -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());
}
}

View File

@@ -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) {
}

View File

@@ -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<String, Consumer<String>> operations;
private final Component attachedComponent;
public static class Builder {
private WindowRegistry registry = WindowRegistry.getInstance();
private final Map<String, Consumer<String>> 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<String> 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<String> handler = operations.get(operation.type());
if (handler != null) {
handler.accept(operation.targetWindow());
operation.callback().success("操作成功: " + operation.type());
} else {
operation.callback().failure(-1, "未定义的操作: " + operation.type());
}
}
}

View File

@@ -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<String, BrowserWindow> windows =
new ConcurrentHashMap<>();
private final ConcurrentMap<String, BrowserWindowJDialog> childWindows =
new ConcurrentHashMap<>();
private final Map<String, Consumer<String>> 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<String> handler) {
callbacks.put(requestId, handler);
}
public Consumer<String> 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<BrowserWindow.Builder> 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<BrowserWindowJDialog.Builder> 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();
}
}
}
});
}
}

View File

@@ -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<String> 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<String, String> 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<String, String> 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<StringBuilder> 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 <stdio.h>\n" +
"\n" +
"int main() {\n" +
" while (1){\n" +
" printf(\"Hello World\\n\");\n" +
"}\n" +
" return 0;\n" +
"}";
executeCode(pythonCode, "c", null);
}
}

View File

@@ -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<String, Connection> connections = new java.util.concurrent.ConcurrentHashMap<>();
private static final Map<String, DatabaseInfo> 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 不需要 propsURL 已经是文件路径(已做过替换)
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":
// 对于 SQLitedatabase 可能是绝对路径或相对文件名,先把反斜杠替成正斜杠
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()) {
// 创建 userschema示例
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();
}
}
}
}
}
}

View File

@@ -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);
}

View File

@@ -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<Class<?>, List<Subscriber>> eventSubscribers = new ConcurrentHashMap<>();
private final Map<Object, List<Subscriber>> targetSubscribers = new ConcurrentHashMap<>();
private volatile boolean shutdown;
public EventBus() {
this.busID = maxID++;
}
private static class Subscriber implements Comparable<Subscriber> {
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<Subscriber> 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<Subscriber> 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<Method> getAnnotatedMethods(Object target) {
Set<Method> 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<Subscriber> subs = targetSubscribers.remove(target);
if (subs == null) {
return;
}
for (Subscriber sub : subs) {
List<Subscriber> 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<Subscriber> 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<String, Boolean> 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();
}
}

View File

@@ -1,11 +0,0 @@
package com.chuangzhou.vivid2D.events;
/**
* @author tzdwindows 7
*/
public class GlobalEventBus {
/**
* 全局事件总线
*/
public static final EventBus EVENT_BUS = new EventBus();
}

View File

@@ -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<String, Boolean> additionalInfo;
public PostResult(boolean cancelled, Map<String, Boolean> additionalInfo) {
this.cancelled = cancelled;
this.additionalInfo = additionalInfo != null ? additionalInfo : Collections.emptyMap();
}
/**
* @return 事件是否在处理过程中被取消
*/
public boolean isCancelled() {
return cancelled;
}
/**
* @return 一个包含附加信息的Map默认为空
*/
public Map<String, Boolean> getAdditionalInfo() {
return additionalInfo;
}
}

View File

@@ -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) {
// 不支持取消
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -1,6 +0,0 @@
package com.chuangzhou.vivid2D.render.awt;
import javax.swing.*;
public class EventPanel extends JPanel {
}

View File

@@ -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<Float> 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<RelatedParameterInfo> allRelatedParameters = new ArrayList<>();
public KeyframeDetailsDialog(Window owner, AnimationParameter parameter, Float value, SortedSet<Float> 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<RelatedParameterInfo> data = new ArrayList<>();
private final ModelPart modelPart;
private final ParametersManagement management;
private final Runnable refreshCallback;
// [新增] 参数ID 中文映射
private final Map<String, String> 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<RelatedParameterInfo> 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<Integer> 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<AnimationParameter> anims = fullRecord.animationParameter();
List<Float> keyframes = fullRecord.keyframe();
List<String> paramIds = fullRecord.paramId();
List<Object> values = fullRecord.value();
int size = Math.min(keyframes.size(), Math.min(anims.size(), Math.min(paramIds.size(), values.size())));
List<RelatedParameterInfo> 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<RelatedParameterInfo> 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<Float> 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;
}
}

View File

@@ -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<Float> 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<Float> keyframes = new ArrayList<>();
public void setData(SortedSet<Float> 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();
}
}

View File

@@ -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<ChangeListener> 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);
}
}
}

View File

@@ -1,4 +0,0 @@
package com.chuangzhou.vivid2D.render.awt;
public class ModelAIPanel {
}

View File

@@ -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) {
}
}

View File

@@ -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;
}
}
}

View File

@@ -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 渲染面板
*
* <p>该类提供了 vivid2D 模型在 Java 环境下的图形渲染功能,
* 包含基本的 2D 图形绘制、模型显示和交互操作。</p>
*
* @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<Model2D> modelRef = new AtomicReference<>();
private final KeyboardManager keyboardManager;
private final CopyOnWriteArrayList<ModelClickListener> clickListeners = new CopyOnWriteArrayList<>();
private final StatusRecordManagement statusRecordManagement;
private final RanderToolsManager randerToolsManager = RanderToolsManager.getInstance();
private final AtomicReference<ParametersManagement> 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<ModelPart> 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<ModelPart> 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<Mesh2D> 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<Model2D> loadModel(String modelPath) {
CompletableFuture<Model2D> 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();
}
}
/**
* 重新设置面板大小
* <p>
* 说明:当 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
}
}

View File

@@ -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<AnimationParameter> listModel = new DefaultListModel<>();
private final JList<AnimationParameter> 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 ? "<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<String, AnimationParameter> 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<String, AnimationParameter> 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<String, AnimationParameter> 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<String, AnimationParameter> pm = (Map<String, AnimationParameter>) 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<ParameterEventListener> 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) {}
}
}
});
}
}
}

View File

@@ -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<ModelPart> 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<ModelPart, Float> oldRotations = new HashMap<>();
Map<ModelPart, Float> 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<ModelPart, Float> oldRotations = new HashMap<>();
Map<ModelPart, Float> 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<ModelPart, Vector2f> oldScales = new HashMap<>();
Map<ModelPart, Vector2f> 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<ModelPart, Vector2f> oldScales = new HashMap<>();
Map<ModelPart, Vector2f> 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<ModelPart, Vector2f> oldScales = new HashMap<>();
Map<ModelPart, Vector2f> 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<ModelPart, Object>
*/
private void recordMultiPartOperation(String operationType, Map<ModelPart, Object> oldValues, Map<ModelPart, Object> newValues) {
if (operationHistory != null && !selectedParts.isEmpty()) {
List<Object> 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<ModelPart, Object> oldStates = new HashMap<>();
Map<ModelPart, Object> 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<ModelPart, Object> oldPositions = new HashMap<>();
Map<ModelPart, Object> 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<ModelPart> 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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<Void> contextReady = new CompletableFuture<>();
private String modelPath;
private final AtomicReference<Model2D> 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<Runnable> 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<Model2D> 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<Void> 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<Void> 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<Void> executeInGLContext(Runnable task) {
CompletableFuture<Void> 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 <T> CompletableFuture<T> executeInGLContext(Callable<T> task) {
CompletableFuture<T> 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> T executeInGLContextSync(Callable<T> 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<Model2D> 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<Model2D> waitForModel() {
return modelReady;
}
}

View File

@@ -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<String, KeyStroke> customShortcuts = new HashMap<>();
private final Map<String, AbstractAction> 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<Tool> 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<String, String> getShortcutInfo() {
Map<String, String> info = new HashMap<>();
for (Map.Entry<String, KeyStroke> 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("键盘管理器已清理");
}
}

View File

@@ -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<LayerInfo> layerMetadata;
public LayerOperationManager(Model2D model) {
this.model = model;
this.layerMetadata = new ArrayList<>();
initializeMetadata();
}
/**
* 加载并替换当前的图层元数据列表,并根据加载的顺序重新排列 Model2D 内部的图层。
* 通常在反序列化(加载文件)时调用。
* @param loadedMetadata 从文件加载的 LayerInfo 列表
*/
public void loadMetadata(List<LayerInfo> loadedMetadata) {
if (loadedMetadata == null || loadedMetadata.isEmpty()) return;
this.layerMetadata.clear();
this.layerMetadata.addAll(loadedMetadata);
Map<String, ModelPart> partMap = model.getPartMap();
if (partMap == null || partMap.isEmpty()) return;
List<ModelPart> 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<ModelPart> 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<ModelPart> parts = model.getParts();
if (parts != null) parts.remove(part);
Map<String, ModelPart> partMap = model.getPartMap();
if (partMap != null) partMap.remove(part.getName());
initializeMetadata();
model.markNeedsUpdate();
}
public void moveLayer(List<ModelPart> visualOrder) {
List<ModelPart> 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<ModelPart> 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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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<Parameter> 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<Object> originalValues = existingParameter.value();
List<Object> 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<String, Object> payload = (Map<String, Object>) 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<String, Object> 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<ModelPart> 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<AnimationParameter> newAnimationParameters = new ArrayList<>(existingParameter.animationParameter());
List<String> newParamIds = new ArrayList<>(existingParameter.paramId());
List<Object> newValues = new ArrayList<>(existingParameter.value());
List<Float> newKeyframes = new ArrayList<>(existingParameter.keyframe());
List<Boolean> 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<String, Object> payload = (Map<String, Object>) 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<AnimationParameter> newAnimationParameters = new ArrayList<>(existingParameter.animationParameter());
List<String> newParamIds = new ArrayList<>(existingParameter.paramId());
List<Object> newValues = new ArrayList<>(existingParameter.value());
List<Float> newKeyframes = new ArrayList<>(existingParameter.keyframe());
List<Boolean> 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<String, Object> oldPayload = (Map<String, Object>) 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<AnimationParameter> newAnimationParameters = new ArrayList<>(existingParameter.animationParameter());
List<String> newParamIds = new ArrayList<>(existingParameter.paramId());
List<Object> newValues = new ArrayList<>(existingParameter.value());
List<Float> newKeyframes = new ArrayList<>(existingParameter.keyframe());
List<Boolean> 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<Integer> 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<AnimationParameter> anims = new ArrayList<>();
List<String> ids = new ArrayList<>();
List<Object> values = new ArrayList<>();
List<Float> keyframes = new ArrayList<>();
List<Boolean> 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> animationParameter,
List<String> paramId,
List<Object> value,
List<Float> keyframe,
List<Boolean> 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();
}
}

View File

@@ -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<ModelPart> parts, Map<ModelPart, Vector2f> startPositions) {
if (operationHistory != null && parts != null && !parts.isEmpty()) {
List<Object> 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<ModelPart> parts, Map<ModelPart, Vector2f> startScales) {
if (operationHistory != null && parts != null && !parts.isEmpty()) {
List<Object> 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<ModelPart> parts, Map<ModelPart, Float> startRotations) {
if (operationHistory != null && parts != null && !parts.isEmpty()) {
List<Object> 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<ModelPart> parts, Map<ModelPart, Vector2f> startPivots) {
if (operationHistory != null && parts != null && !parts.isEmpty()) {
List<Object> 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("操作历史已清除");
}
}
}

View File

@@ -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<ModelPart, BufferedImage> 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<Mesh2D> 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;
}
}

View File

@@ -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<String, Tool> registeredTools;
private final RanderToolsManager randerToolsManager;
private Tool currentTool = null;
private Tool previousTool = null;
// 【新增】工具切换监听器列表
private final List<ToolChangeListener> 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<Tool> 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() : "");
}
}

View File

@@ -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};
}
}

View File

@@ -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<LayerOperationManager.LayerInfo> layerMetadata;
public LayerOperationManagerData(List<LayerOperationManager.LayerInfo> layerMetadata) {
this.layerMetadata = layerMetadata;
}
public LayerOperationManagerData() {}
}

View File

@@ -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<ManagementParameterRecord> 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<ModelPart> 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<ParameterData> animationParameters;
public List<String> paramIds;
public List<Object> values; // 这里将存储 SerializableVertex 而不是 Vertex
public List<Float> keyframes;
public List<Boolean> 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<String, Object> 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<String, Object> 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<ModelPart> 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<AnimationParameter> animParams = new ArrayList<>();
if (this.animationParameters != null) {
for (ParameterData p : this.animationParameters) {
animParams.add(p.toAnimationParameter());
}
}
// [核心修复]:还原 values 中的 Vertex 对象
List<Object> 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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<Vertex> 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<Vertex> orderedControlVertices = new ArrayList<>();
private boolean isPushPullMode = false;
private Vector2f dragStartPoint = null;
private List<Vertex> 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<String, Object> 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<Vertex> 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;}
}

View File

@@ -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<Integer> findIndicesForParam(ParametersManagement.Parameter fullParam, String paramId, AnimationParameter currentAnimationParameter) {
List<Integer> indices = new ArrayList<>();
if (fullParam == null || fullParam.paramId() == null || currentAnimationParameter == null) return indices;
List<String> pids = fullParam.paramId();
List<AnimationParameter> 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<Integer> findIndicesForDeformationVertex(ParametersManagement.Parameter fullParam, String vertexId, AnimationParameter currentAnimationParameter) {
List<Integer> indices = new ArrayList<>();
if (fullParam == null || fullParam.paramId() == null || currentAnimationParameter == null || vertexId == null) return indices;
List<String> pids = fullParam.paramId();
List<Object> values = fullParam.value();
List<AnimationParameter> 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<Float> keyframes, List<Integer> 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<Integer> 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<Object> 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<Integer> 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<Object> 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<String, float[]> computeMeshVerticesTarget(ParametersManagement.Parameter fullParam, float current, AnimationParameter animParam) {
Map<String, float[]> targetDeformations = new HashMap<>();
if (fullParam == null) return targetDeformations;
Set<String> uniqueVertexIds = new HashSet<>();
List<String> pids = fullParam.paramId();
List<Object> values = fullParam.value();
List<AnimationParameter> 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<Integer> 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<ModelPart> 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<String, float[]> 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<Vertex> allVerticesInMesh = targetMesh.getActiveVertexList().getVertices();
for (Map.Entry<String, float[]> 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<String, float[]> 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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<OperationRecord> undoStack;
private final LinkedList<OperationRecord> redoStack;
// 最大记录数量
private final int maxHistorySize;
// 操作记录器映射
private final Map<String, OperationRecorder> 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;
}
}
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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<ModelPart> createdParts = createPartsFromPSDLayers(result.layers);
SwingUtilities.invokeLater(() -> notifyImportComplete(createdParts));
} catch (Exception e) {
SwingUtilities.invokeLater(() ->
showError("导入PSD图层失败: " + e.getMessage()));
}
});
} else {
List<ModelPart> createdParts = createPartsFromPSDLayers(result.layers);
notifyImportComplete(createdParts);
}
}
private List<ModelPart> createPartsFromPSDLayers(List<PsdParser.PSDLayerInfo> layers) {
List<ModelPart> 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<String, ModelPart> 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<Mesh2D> 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<String, ModelPart> 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<ModelPart> createdParts) {
if (model != null) {
model.markNeedsUpdate();
}
// 通知监听器导入完成
}
private void showError(String message) {
JOptionPane.showMessageDialog(null, message, "错误", JOptionPane.ERROR_MESSAGE);
}
}

View File

@@ -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);
}
}

View File

@@ -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<PSDLayerInfo> 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<Object> layerInfos = (List<Object>) 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<ImageReader> 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();
}
}
}

View File

@@ -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<ModelPart> {
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<ModelPart> layerList, javax.swing.ListModel<ModelPart> 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<? extends ModelPart> 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<Float> 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<Float> 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<Float> head = keyframes.headSet(value);
SortedSet<Float> 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();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 模型核心数据结构
*
* <p>定义 vivid2D 模型系统中的核心数据结构和基础数据类型,包括:</p>
*
* <ul>
* <li>几何数据:顶点、边、面等基本几何元素</li>
* <li>拓扑结构:模型的组织关系和连接信息</li>
* <li>属性数据:颜色、纹理坐标、法向量等附加属性</li>
* <li>层次结构:模型的父子关系和变换信息</li>
* </ul>
*
* <h3>主要包含:</h3>
* <ul>
* <li>基础几何类(点、向量、矩阵)</li>
* <li>模型节点和组件类</li>
* <li>数据容器和缓冲区</li>
* <li>序列化和反序列化支持</li>
* </ul>
*
* @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<ModelPart> parts;
private final Map<String, ModelPart> partMap; // 快速查找
private ModelPart rootPart;
// ==================== 网格系统 ====================
private final List<Mesh2D> meshes;
private final Map<String, Texture> textures; // 纹理映射
// ==================== 动画系统 ====================
private final Map<String, AnimationParameter> parameters;
private final List<AnimationLayer> animationLayers;
private final PhysicsSystem physics;
// ==================== 渲染状态 ====================
private transient boolean needsUpdate = true;
private transient BoundingBox bounds;
// ==================== 姿态系统 ====================
private final Map<String, ModelPose> 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<LightSource> lights;
private final List<ModelEvent> 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<LightSource> 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<String, ModelPart> getPartMap() {
return partMap;
}
public List<ModelPart> 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<String, Texture> 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<String> 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<AnimationLayer> 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<Mesh2D> getMeshes() {
return Collections.unmodifiableList(meshes);
}
public Map<String, AnimationParameter> getParameters() {
return Collections.unmodifiableMap(parameters);
}
public List<AnimationLayer> 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<String, ModelPose> getPoses() {
return Collections.unmodifiableMap(poses);
}
public BoundingBox getBounds() {
return bounds;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
}

View File

@@ -1,10 +0,0 @@
package com.chuangzhou.vivid2D.render.model;
/**
* 模型事件
*
* @author tzdwindows 7
*/
public interface ModelEvent {
void trigger(String eventName, Object eventBus);
}

View File

@@ -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<String, AnimationTrackData> tracks;
public List<String> 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<String, AnimationTrackData> 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<KeyframeData> 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;
}
}
}

View File

@@ -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 +
'}';
}
}

Some files were not shown because too many files have changed in this diff Show More