chore(jcef): 更新缓存数据库日志文件
- 更新 shared_proto_db/metadata/000003.log 文件内容 - 更新 Site Characteristics Database/00003.log 文件内容 - 添加新的数据库条目和元数据记录 - 保持数据库文件格式的一致性 - 删除Vivid2D的内容 - 重写启动加载界面
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"};
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"));
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.axis.innovators.box.util;
|
||||
|
||||
import com.axis.innovators.box.util.build.BuildSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.axis.innovators.box.util.build;
|
||||
|
||||
public enum BuildSystem {
|
||||
UNKNOWN,
|
||||
WINDOWS,
|
||||
LINUX,
|
||||
MACOS
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# build.properties
|
||||
Version=1.0.0
|
||||
Build_Timestamp=2023-10-01T12:00:00Z
|
||||
Build_System=LINUX
|
||||
@@ -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
|
||||
|
||||
@@ -1,462 +1,163 @@
|
||||
package com.axis.innovators.box.window;
|
||||
|
||||
import com.axis.innovators.box.AxisInnovatorsBox;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.geom.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
|
||||
/**
|
||||
* 启动窗口的任务系统(已改造为现代流畅动画视觉效果)
|
||||
* 注意:保留了原有对外方法和签名(updateMainProgress/updateSubProgress/setTotalTasks/close)
|
||||
* 作者: tzdwindows 7(UI 改造版)
|
||||
* 现代简约风格启动窗口 - 已添加 Logo 支持
|
||||
*/
|
||||
public class ProgressBarManager extends WindowsJDialog {
|
||||
private JFrame loadingFrame;
|
||||
private SmoothProgressBar mainProgressBar;
|
||||
private SmoothProgressBar subProgressBar;
|
||||
private JLabel statusLabel;
|
||||
private JLabel timeLabel;
|
||||
private long startTime;
|
||||
|
||||
private int totalTasks;
|
||||
private int completedTasks;
|
||||
private Map<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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 渲染器代码
|
||||
}
|
||||
}
|
||||
@@ -1,821 +0,0 @@
|
||||
package com.chuangzhou.vivid2D.browser;
|
||||
|
||||
import com.axis.innovators.box.AxisInnovatorsBox;
|
||||
import com.axis.innovators.box.events.BrowserCreationCallback;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.CefSettings;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.browser.CefFrame;
|
||||
import org.cef.browser.CefMessageRouter;
|
||||
import org.cef.callback.CefContextMenuParams;
|
||||
import org.cef.callback.CefJSDialogCallback;
|
||||
import org.cef.callback.CefMenuModel;
|
||||
import org.cef.callback.CefQueryCallback;
|
||||
import org.cef.handler.*;
|
||||
import org.cef.misc.BoolRef;
|
||||
import org.cef.network.CefRequest;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class BrowserWindow extends JFrame {
|
||||
private final String windowId;
|
||||
private final String htmlUrl;
|
||||
private CefApp cefApp;
|
||||
private CefClient client;
|
||||
private CefBrowser browser;
|
||||
private final Component browserComponent;
|
||||
private final String htmlPath;
|
||||
private static boolean isInitialized = false;
|
||||
private WindowOperationHandler operationHandler;
|
||||
private static Thread cefThread;
|
||||
private CefMessageRouter msgRouter;
|
||||
|
||||
public static class Builder {
|
||||
private BrowserCreationCallback browserCreationCallback;
|
||||
private String windowId;
|
||||
private String title = "JCEF Window";
|
||||
private Dimension size = new Dimension(800, 600);
|
||||
private WindowOperationHandler operationHandler;
|
||||
private String htmlPath;
|
||||
private Image icon;
|
||||
private boolean resizable = true; // 默认允许调整大小
|
||||
private boolean maximizable = true; // 默认允许最大化
|
||||
private boolean minimizable = true; // 默认允许最小化
|
||||
private String htmlUrl = "";
|
||||
private boolean openLinksInExternalBrowser = true; // 默认使用外部浏览器
|
||||
|
||||
public Builder resizable(boolean resizable) {
|
||||
this.resizable = resizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maximizable(boolean maximizable) {
|
||||
this.maximizable = maximizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置链接打开方式
|
||||
*
|
||||
* @param openInBrowser 是否在当前浏览器窗口中打开链接
|
||||
* true - 在当前浏览器窗口中打开链接(本地跳转)
|
||||
* false - 使用系统默认浏览器打开链接(外部跳转)
|
||||
* @return Builder实例,支持链式调用
|
||||
*
|
||||
* @apiNote 此方法控制两种不同的链接打开行为:
|
||||
* 1. 当设置为true时:
|
||||
* - 所有链接将在当前CEF浏览器窗口内打开
|
||||
*
|
||||
* 2. 当设置为false时(默认值):
|
||||
* - 所有链接将在系统默认浏览器中打开
|
||||
* - 更安全,避免潜在的安全风险
|
||||
* - 适用于简单的信息展示场景
|
||||
*
|
||||
* @implNote 内部实现说明:
|
||||
* - 实际存储的是反向值(openLinksInExternalBrowser)
|
||||
* - 这样设置是为了保持与历史版本的兼容性
|
||||
* - 方法名使用"openInBrowser"更符合用户直觉
|
||||
*
|
||||
* @example 使用示例:
|
||||
* // 在当前窗口打开链接
|
||||
* new Builder().openLinksInBrowser(true).build();
|
||||
*
|
||||
* // 使用系统浏览器打开链接(默认)
|
||||
* new Builder().openLinksInBrowser(false).build();
|
||||
*
|
||||
* @see #openLinksInExternalBrowser 内部存储字段
|
||||
* @see CefLifeSpanHandler#onBeforePopup 弹窗处理实现
|
||||
* @see CefRequestHandler#onBeforeBrowse 导航处理实现
|
||||
*/
|
||||
public Builder openLinksInBrowser(boolean openInBrowser) {
|
||||
this.openLinksInExternalBrowser = !openInBrowser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder minimizable(boolean minimizable) {
|
||||
this.minimizable = minimizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder(String windowId) {
|
||||
this.windowId = windowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器创建回调
|
||||
* @param callback 回调
|
||||
*/
|
||||
public Builder setBrowserCreationCallback(BrowserCreationCallback callback){
|
||||
this.browserCreationCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口标题
|
||||
* @param title 标题
|
||||
*/
|
||||
public Builder title(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口大小
|
||||
* @param width 宽度
|
||||
* @param height 高度
|
||||
*/
|
||||
public Builder size(int width, int height) {
|
||||
this.size = new Dimension(width, height);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器触发事件
|
||||
* @param handler 事件处理器
|
||||
*/
|
||||
public Builder operationHandler(WindowOperationHandler handler) {
|
||||
this.operationHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器图标
|
||||
* @param icon 图标
|
||||
*/
|
||||
public Builder icon(Image icon) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
*/
|
||||
public BrowserWindow build() {
|
||||
if (htmlUrl.isEmpty()) {
|
||||
if (this.htmlPath == null || this.htmlPath.isEmpty()) {
|
||||
throw new IllegalArgumentException("HTML paths cannot be empty");
|
||||
}
|
||||
File htmlFile = new File(this.htmlPath);
|
||||
if (!htmlFile.exists()) {
|
||||
throw new RuntimeException("The HTML file does not exist: " + htmlFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
return new BrowserWindow(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
* @param path HTML路径
|
||||
*/
|
||||
public Builder htmlPath(String path) {
|
||||
this.htmlPath = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用Url
|
||||
* @param htmlUrl Url路径
|
||||
*/
|
||||
public Builder htmlUrl(String htmlUrl) {
|
||||
this.htmlUrl = htmlUrl;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private BrowserWindow(Builder builder) {
|
||||
this.windowId = builder.windowId;
|
||||
this.htmlPath = builder.htmlPath;
|
||||
this.operationHandler = builder.operationHandler;
|
||||
this.htmlUrl = builder.htmlUrl;
|
||||
|
||||
// 设置图标(如果存在)
|
||||
if (builder.icon != null) {
|
||||
setIconImage(builder.icon);
|
||||
}
|
||||
|
||||
// 初始化浏览器组件
|
||||
try {
|
||||
this.browserComponent = initializeCef(builder);
|
||||
if (operationHandler != null) {
|
||||
setupMessageHandlers(operationHandler);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Component initializeCef(Builder builder) throws MalformedURLException {
|
||||
if (!isInitialized) {
|
||||
isInitialized = true;
|
||||
try {
|
||||
this.cefApp = CefAppManager.getInstance();
|
||||
//CefAppManager.incrementBrowserCount();
|
||||
client = cefApp.createClient();
|
||||
client.addDisplayHandler(new CefDisplayHandler (){
|
||||
@Override
|
||||
public void onAddressChange(CefBrowser browser, CefFrame frame, String url) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChange(CefBrowser browser, String title) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTooltip(CefBrowser browser, String text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusMessage(CefBrowser browser, String value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onConsoleMessage(
|
||||
CefBrowser browser,
|
||||
CefSettings.LogSeverity level,
|
||||
String message,
|
||||
String source,
|
||||
int line
|
||||
) {
|
||||
// 格式化输出到 Java 控制台
|
||||
//if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) {
|
||||
String log = String.format(
|
||||
"[Browser Console] %s %s (Line %d) -> %s",
|
||||
getLogLevelSymbol(level),
|
||||
source,
|
||||
line,
|
||||
message
|
||||
);
|
||||
System.out.println(log);
|
||||
//}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCursorChange(CefBrowser browser, int cursorType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getLogLevelSymbol(CefSettings.LogSeverity level) {
|
||||
switch (level) {
|
||||
case LOGSEVERITY_ERROR: return "⛔";
|
||||
case LOGSEVERITY_WARNING: return "⚠️";
|
||||
case LOGSEVERITY_DEFAULT: return "🐞";
|
||||
default: return "ℹ️";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) {
|
||||
client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
|
||||
// 检测 F12
|
||||
if (event.windows_key_code == 123) {
|
||||
browser.getDevTools().createImmediately();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
|
||||
String targetUrl, String targetFrameName) {
|
||||
// 处理弹出窗口:根据配置决定打开方式
|
||||
if (builder.openLinksInExternalBrowser) {
|
||||
// 使用默认浏览器打开
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(targetUrl));
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
return true; // 拦截弹窗
|
||||
} else {
|
||||
// 在当前浏览器中打开
|
||||
browser.loadURL(targetUrl);
|
||||
return true; // 拦截弹窗并在当前窗口打开
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.addRequestHandler(new CefRequestHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame,
|
||||
CefRequest request, boolean userGesture, boolean isRedirect) {
|
||||
// 处理主窗口导航
|
||||
if (userGesture) {
|
||||
if (builder.openLinksInExternalBrowser) {
|
||||
// 使用默认浏览器打开
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(request.getURL()));
|
||||
return true; // 取消内置浏览器导航
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 允许在当前浏览器中打开
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
client.addContextMenuHandler(new CefContextMenuHandlerAdapter() {
|
||||
@Override
|
||||
public void onBeforeContextMenu(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, CefMenuModel model) {
|
||||
model.clear();
|
||||
if (!params.getSelectionText().isEmpty() || params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST, "复制");
|
||||
}
|
||||
|
||||
if (params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST + 1, "粘贴");
|
||||
model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, int commandId, int eventFlags) {
|
||||
if (commandId == MENU_ID_USER_FIRST) {
|
||||
if (params.isEditable()) {
|
||||
browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0);
|
||||
} else {
|
||||
browser.executeJavaScript(
|
||||
"window.getSelection().toString();",
|
||||
browser.getURL(),
|
||||
0
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 1) {
|
||||
pasteContent(browser, false);
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 2) {
|
||||
pasteContent(browser, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理粘贴操作
|
||||
* @param plainText 是否去除格式(纯文本模式)
|
||||
*/
|
||||
private void pasteContent(CefBrowser browser, boolean plainText) {
|
||||
try {
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
String text = (String) clipboard.getData(DataFlavor.stringFlavor);
|
||||
|
||||
if (plainText) {
|
||||
text = text.replaceAll("<[^>]+>", "");
|
||||
}
|
||||
|
||||
String escapedText = text
|
||||
.replace("\\", "\\\\")
|
||||
.replace("'", "\\'")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r");
|
||||
|
||||
String script = String.format(
|
||||
"if (document.activeElement) {\n" +
|
||||
" document.activeElement.value += '%s';\n" + // 简单追加文本
|
||||
" document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件
|
||||
"}",
|
||||
escapedText
|
||||
);
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
} catch (UnsupportedFlavorException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.addJSDialogHandler(new CefJSDialogHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onJSDialog(CefBrowser browser, String origin_url, JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) {
|
||||
if (dialog_type == JSDialogType.JSDIALOGTYPE_ALERT) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(
|
||||
BrowserWindow.this,
|
||||
message_text,
|
||||
"警告",
|
||||
JOptionPane.INFORMATION_MESSAGE
|
||||
);
|
||||
});
|
||||
callback.Continue(true, "");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 3. 拦截所有新窗口(关键修复点!)
|
||||
//client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
// @Override
|
||||
// public boolean onBeforePopup(CefBrowser browser,
|
||||
// CefFrame frame, String target_url, String target_frame_name) {
|
||||
// return true; // 返回true表示拦截弹窗
|
||||
// }
|
||||
//});
|
||||
|
||||
|
||||
Thread.currentThread().setName("BrowserRenderThread");
|
||||
|
||||
// 4. 加载HTML
|
||||
if (htmlUrl.isEmpty()){
|
||||
String fileUrl = new File(htmlPath).toURI().toURL().toString();
|
||||
System.out.println("Loading HTML from: " + fileUrl);
|
||||
|
||||
// 5. 创建浏览器组件(直接添加到内容面板)
|
||||
browser = client.createBrowser(fileUrl, false, false);
|
||||
} else {
|
||||
System.out.println("Loading Url from: " + htmlUrl);
|
||||
browser = client.createBrowser(htmlUrl, false, false);
|
||||
}
|
||||
|
||||
Component browserComponent = browser.getUIComponent();
|
||||
if (builder.browserCreationCallback != null) {
|
||||
boolean handled = builder.browserCreationCallback.onLayoutCustomization(
|
||||
this, // 当前窗口
|
||||
getContentPane(), // 内容面板
|
||||
browserComponent, // 浏览器组件
|
||||
builder // 构建器对象
|
||||
);
|
||||
|
||||
// 如果回调返回true,跳过默认布局
|
||||
if (handled) {
|
||||
// 设置窗口基本属性
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
// 添加资源释放监听器
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true);
|
||||
client.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
setVisible(true);
|
||||
return browserComponent; // 直接返回,跳过默认布局
|
||||
}
|
||||
}
|
||||
|
||||
CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
|
||||
config.jsQueryFunction = "javaQuery";// 定义方法
|
||||
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
|
||||
|
||||
updateTheme();
|
||||
|
||||
// 6. 配置窗口布局(确保只添加一次)
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
getContentPane().removeAll();
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
|
||||
// 透明拖拽层(仅顶部可拖拽)
|
||||
JPanel dragPanel = new JPanel(new BorderLayout());
|
||||
dragPanel.setOpaque(false);
|
||||
|
||||
JPanel titleBar = new JPanel();
|
||||
titleBar.setOpaque(false);
|
||||
titleBar.setPreferredSize(new Dimension(builder.size.width, 20));
|
||||
final Point[] dragStart = new Point[1];
|
||||
titleBar.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
dragStart[0] = e.getPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
dragStart[0] = null;
|
||||
}
|
||||
});
|
||||
titleBar.addMouseMotionListener(new MouseMotionAdapter() {
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
if (dragStart[0] != null) {
|
||||
Point curr = e.getLocationOnScreen();
|
||||
setLocation(curr.x - dragStart[0].x, curr.y - dragStart[0].y);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dragPanel.add(titleBar, BorderLayout.NORTH);
|
||||
getContentPane().add(dragPanel, BorderLayout.CENTER);
|
||||
getContentPane().add(browserComponent, BorderLayout.CENTER);
|
||||
|
||||
// 7. 窗口属性设置
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size);
|
||||
setLocationRelativeTo(null);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
// 8. 资源释放
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true);
|
||||
client.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
setVisible(true);
|
||||
|
||||
});
|
||||
return browserComponent;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
} else {
|
||||
isInitialized = false;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
dispose();
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
*/
|
||||
public void updateTheme() {
|
||||
// 1. 获取Java字体信息
|
||||
String fontInfo = getSystemFontsInfo();
|
||||
boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
|
||||
injectFontInfoToPage(browser, fontInfo, isDarkTheme);
|
||||
|
||||
// 2. 注入主题信息
|
||||
//injectThemeInfoToPage(browser, isDarkTheme);
|
||||
|
||||
//// 3. 刷新浏览器
|
||||
//SwingUtilities.invokeLater(() -> {
|
||||
// browser.reload();
|
||||
//});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Java字体信息(从UIManager获取)
|
||||
*/
|
||||
private String getSystemFontsInfo() {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
JsonObject fontInfo = new JsonObject();
|
||||
JsonObject uiFonts = new JsonObject();
|
||||
|
||||
String[] fontKeys = {
|
||||
"Label.font", "Button.font", "ToggleButton.font", "RadioButton.font",
|
||||
"CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font",
|
||||
"TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font",
|
||||
"FormattedTextField.font", "Table.font", "TableHeader.font", "List.font",
|
||||
"Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font",
|
||||
"PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font",
|
||||
"Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont",
|
||||
"OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font"
|
||||
};
|
||||
|
||||
for (String key : fontKeys) {
|
||||
Font font = UIManager.getFont(key);
|
||||
if (font != null) {
|
||||
JsonObject fontObj = new JsonObject();
|
||||
fontObj.addProperty("name", font.getFontName());
|
||||
fontObj.addProperty("family", font.getFamily());
|
||||
fontObj.addProperty("size", font.getSize());
|
||||
fontObj.addProperty("style", font.getStyle());
|
||||
fontObj.addProperty("bold", font.isBold());
|
||||
fontObj.addProperty("italic", font.isItalic());
|
||||
fontObj.addProperty("plain", font.isPlain());
|
||||
uiFonts.add(key, fontObj);
|
||||
}
|
||||
}
|
||||
|
||||
fontInfo.add("uiFonts", uiFonts);
|
||||
fontInfo.addProperty("timestamp", System.currentTimeMillis());
|
||||
fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName());
|
||||
|
||||
return gson.toJson(fontInfo);
|
||||
} catch (Exception e) {
|
||||
return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入主题信息到页面
|
||||
*/
|
||||
private void injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String themeInfo = String.format(
|
||||
"{\"isDarkTheme\": %s, \"timestamp\": %d}",
|
||||
isDarkTheme,
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
// 最简单的脚本 - 直接设置和分发事件
|
||||
String script = String.format(
|
||||
"window.javaThemeInfo = %s;" +
|
||||
"console.log('主题信息已设置:', window.javaThemeInfo);" +
|
||||
"" +
|
||||
"var event = new CustomEvent('javaThemeChanged', {" +
|
||||
" detail: window.javaThemeInfo" +
|
||||
"});" +
|
||||
"document.dispatchEvent(event);" +
|
||||
"console.log('javaThemeChanged事件已分发');",
|
||||
themeInfo);
|
||||
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入字体信息到页面并设置字体
|
||||
*/
|
||||
private void injectFontInfoToPage(CefBrowser browser, String fontInfo,boolean isDarkTheme) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
client.addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
// 使用更简单的脚本来注入字体信息
|
||||
String script =
|
||||
"if (typeof window.javaFontInfo === 'undefined') {" +
|
||||
" window.javaFontInfo = " + fontInfo + ";" +
|
||||
" console.log('Java font information has been loaded:', window.javaFontInfo);" +
|
||||
" " +
|
||||
" var event = new CustomEvent('javaFontsLoaded', {" +
|
||||
" detail: window.javaFontInfo" +
|
||||
" });" +
|
||||
" document.dispatchEvent(event);" +
|
||||
" console.log('The javaFontsLoaded event is dispatched');" +
|
||||
"}";
|
||||
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
|
||||
// 添加调试信息
|
||||
browser.executeJavaScript(
|
||||
"console.log('Font information injection is complete,window.javaFontInfo:', typeof window.javaFontInfo);" +
|
||||
"console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');",
|
||||
browser.getURL(), 0
|
||||
);
|
||||
|
||||
String themeInfo = String.format(
|
||||
"{\"isDarkTheme\": %s, \"timestamp\": %d}",
|
||||
isDarkTheme,
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
script = String.format(
|
||||
"window.javaThemeInfo = %s;" +
|
||||
"console.log('主题信息已设置:', window.javaThemeInfo);" +
|
||||
"" +
|
||||
"var event = new CustomEvent('javaThemeChanged', {" +
|
||||
" detail: window.javaThemeInfo" +
|
||||
"});" +
|
||||
"document.dispatchEvent(event);" +
|
||||
"console.log('javaThemeChanged事件已分发');",
|
||||
themeInfo);
|
||||
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public static void printStackTrace() {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
for (int i = 2; i < stackTrace.length; i++) {
|
||||
StackTraceElement element = stackTrace[i];
|
||||
System.out.println(element.getClassName() + "." + element.getMethodName() +
|
||||
"(" + (element.getFileName() != null ? element.getFileName() : "Unknown Source") +
|
||||
":" + element.getLineNumber() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean b) {
|
||||
if (b) {
|
||||
if (browser != null) {
|
||||
updateTheme();
|
||||
}
|
||||
}
|
||||
super.setVisible(b);
|
||||
}
|
||||
|
||||
public Component getBrowserComponent() {
|
||||
return browserComponent;
|
||||
}
|
||||
|
||||
private void setupMessageHandlers(WindowOperationHandler handler) {
|
||||
if (client != null) {
|
||||
msgRouter = CefMessageRouter.create();
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onQuery(CefBrowser browser,
|
||||
CefFrame frame,
|
||||
long queryId,
|
||||
String request,
|
||||
boolean persistent,
|
||||
CefQueryCallback callback) {
|
||||
if (request.startsWith("system:")) {
|
||||
String[] parts = request.split(":");
|
||||
String operation = parts.length >= 2 ? parts[1] : null;
|
||||
String targetWindow = parts.length > 2 ? parts[2] : null;
|
||||
handler.handleOperation(
|
||||
new WindowOperation(operation, targetWindow, callback) // [!code ++]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (request.startsWith("java-response:")) {
|
||||
String[] parts = request.split(":");
|
||||
String requestId = parts[1];
|
||||
String responseData = parts.length > 2 ? parts[2] : "";
|
||||
Consumer<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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,833 +0,0 @@
|
||||
package com.chuangzhou.vivid2D.browser;
|
||||
|
||||
import com.axis.innovators.box.AxisInnovatorsBox;
|
||||
import com.axis.innovators.box.events.BrowserCreationCallback;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.CefSettings;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.browser.CefFrame;
|
||||
import org.cef.browser.CefMessageRouter;
|
||||
import org.cef.callback.CefContextMenuParams;
|
||||
import org.cef.callback.CefJSDialogCallback;
|
||||
import org.cef.callback.CefMenuModel;
|
||||
import org.cef.callback.CefQueryCallback;
|
||||
import org.cef.handler.*;
|
||||
import org.cef.misc.BoolRef;
|
||||
import org.cef.network.CefRequest;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class BrowserWindowJDialog extends JDialog {
|
||||
private final String windowId;
|
||||
private final String htmlUrl;
|
||||
private CefApp cefApp;
|
||||
private CefClient client;
|
||||
private CefBrowser browser;
|
||||
private final Component browserComponent;
|
||||
private final String htmlPath;
|
||||
//private boolean isInitialized = false;
|
||||
private WindowOperationHandler operationHandler;
|
||||
private static Thread cefThread;
|
||||
private CefMessageRouter msgRouter;
|
||||
|
||||
public static class Builder {
|
||||
private String windowId;
|
||||
private String title = "JCEF Window";
|
||||
private Dimension size = new Dimension(800, 600);
|
||||
private WindowOperationHandler operationHandler;
|
||||
private String htmlPath;
|
||||
private Image icon;
|
||||
private JFrame parentFrame;
|
||||
private boolean resizable = true; // 默认允许调整大小
|
||||
private boolean maximizable = true; // 默认允许最大化
|
||||
private boolean minimizable = true; // 默认允许最小化
|
||||
private String htmlUrl = "";
|
||||
private BrowserCreationCallback browserCreationCallback;
|
||||
private boolean openLinksInExternalBrowser = true; // 默认使用外部浏览器
|
||||
|
||||
public Builder resizable(boolean resizable) {
|
||||
this.resizable = resizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maximizable(boolean maximizable) {
|
||||
this.maximizable = maximizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder minimizable(boolean minimizable) {
|
||||
this.minimizable = minimizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder(String windowId) {
|
||||
this.windowId = windowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口标题
|
||||
* @param title 标题
|
||||
*/
|
||||
public Builder title(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置链接打开方式
|
||||
*
|
||||
* @param openInBrowser 是否在当前浏览器窗口中打开链接
|
||||
* true - 在当前浏览器窗口中打开链接(本地跳转)
|
||||
* false - 使用系统默认浏览器打开链接(外部跳转)
|
||||
* @return Builder实例,支持链式调用
|
||||
*
|
||||
* @apiNote 此方法控制两种不同的链接打开行为:
|
||||
* 1. 当设置为true时:
|
||||
* - 所有链接将在当前CEF浏览器窗口内打开
|
||||
*
|
||||
* 2. 当设置为false时(默认值):
|
||||
* - 所有链接将在系统默认浏览器中打开
|
||||
* - 更安全,避免潜在的安全风险
|
||||
* - 适用于简单的信息展示场景
|
||||
*
|
||||
* @implNote 内部实现说明:
|
||||
* - 实际存储的是反向值(openLinksInExternalBrowser)
|
||||
* - 这样设置是为了保持与历史版本的兼容性
|
||||
* - 方法名使用"openInBrowser"更符合用户直觉
|
||||
*
|
||||
* @example 使用示例:
|
||||
* // 在当前窗口打开链接
|
||||
* new Builder().openLinksInBrowser(true).build();
|
||||
*
|
||||
* // 使用系统浏览器打开链接(默认)
|
||||
* new Builder().openLinksInBrowser(false).build();
|
||||
*
|
||||
* @see #openLinksInExternalBrowser 内部存储字段
|
||||
* @see CefLifeSpanHandler#onBeforePopup 弹窗处理实现
|
||||
* @see CefRequestHandler#onBeforeBrowse 导航处理实现
|
||||
*/
|
||||
public Builder openLinksInBrowser(boolean openInBrowser) {
|
||||
this.openLinksInExternalBrowser = !openInBrowser;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置浏览器创建回调
|
||||
* @param callback 回调
|
||||
*/
|
||||
public Builder setBrowserCreationCallback(BrowserCreationCallback callback){
|
||||
this.browserCreationCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口大小
|
||||
* @param width 宽度
|
||||
* @param height 高度
|
||||
*/
|
||||
public Builder size(int width, int height) {
|
||||
this.size = new Dimension(width, height);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口父窗口
|
||||
* @param parent 父窗口
|
||||
*/
|
||||
public Builder parentFrame(JFrame parent) {
|
||||
this.parentFrame = parent;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置浏览器触发事件
|
||||
* @param handler 事件处理器
|
||||
*/
|
||||
public Builder operationHandler(WindowOperationHandler handler) {
|
||||
this.operationHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器图标
|
||||
* @param icon 图标
|
||||
*/
|
||||
public Builder icon(Image icon) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
*/
|
||||
public BrowserWindowJDialog build() {
|
||||
if (htmlUrl.isEmpty()) {
|
||||
if (this.htmlPath == null || this.htmlPath.isEmpty()) {
|
||||
throw new IllegalArgumentException("HTML paths cannot be empty");
|
||||
}
|
||||
File htmlFile = new File(this.htmlPath);
|
||||
if (!htmlFile.exists()) {
|
||||
throw new RuntimeException("The HTML file does not exist: " + htmlFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
return new BrowserWindowJDialog(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
* @param path HTML路径
|
||||
*/
|
||||
public Builder htmlPath(String path) {
|
||||
this.htmlPath = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用Url
|
||||
* @param htmlUrl Url路径
|
||||
*/
|
||||
public Builder htmlUrl(String htmlUrl) {
|
||||
this.htmlUrl = htmlUrl;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private BrowserWindowJDialog(Builder builder) {
|
||||
// 根据父窗口是否存在,设置是否为模态对话框
|
||||
super(builder.parentFrame, builder.title, builder.parentFrame != null);
|
||||
this.windowId = builder.windowId;
|
||||
this.htmlPath = builder.htmlPath;
|
||||
this.htmlUrl = builder.htmlUrl;
|
||||
this.operationHandler = builder.operationHandler;
|
||||
|
||||
// 设置图标(如果存在)
|
||||
if (builder.icon != null) {
|
||||
setIconImage(builder.icon);
|
||||
}
|
||||
|
||||
// 初始化浏览器组件
|
||||
try {
|
||||
this.browserComponent = initializeCef(builder);
|
||||
if (operationHandler != null) {
|
||||
setupMessageHandlers(operationHandler);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
private static boolean isInitialized = false;
|
||||
private Component initializeCef(Builder builder) throws MalformedURLException {
|
||||
if (!isInitialized) {
|
||||
isInitialized = true;
|
||||
try {
|
||||
this.cefApp = CefAppManager.getInstance();
|
||||
//CefAppManager.incrementBrowserCount();
|
||||
client = cefApp.createClient();
|
||||
client.addDisplayHandler(new CefDisplayHandler (){
|
||||
@Override
|
||||
public void onAddressChange(CefBrowser browser, CefFrame frame, String url) {}
|
||||
|
||||
@Override
|
||||
public void onTitleChange(CefBrowser browser, String title) {}
|
||||
|
||||
@Override
|
||||
public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) {}
|
||||
|
||||
@Override
|
||||
public boolean onTooltip(CefBrowser browser, String text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusMessage(CefBrowser browser, String value) {}
|
||||
|
||||
@Override
|
||||
public boolean onConsoleMessage(
|
||||
CefBrowser browser,
|
||||
CefSettings.LogSeverity level,
|
||||
String message,
|
||||
String source,
|
||||
int line
|
||||
) {
|
||||
// 格式化输出到 Java 控制台
|
||||
//if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) {
|
||||
String log = String.format(
|
||||
"[Browser Console] %s %s (Line %d) -> %s",
|
||||
getLogLevelSymbol(level),
|
||||
source,
|
||||
line,
|
||||
message
|
||||
);
|
||||
System.out.println(log);
|
||||
//}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCursorChange(CefBrowser browser, int cursorType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getLogLevelSymbol(CefSettings.LogSeverity level) {
|
||||
switch (level) {
|
||||
case LOGSEVERITY_ERROR: return "⛔";
|
||||
case LOGSEVERITY_WARNING: return "⚠️";
|
||||
case LOGSEVERITY_DEFAULT: return "🐞";
|
||||
default: return "ℹ️";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) {
|
||||
client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
|
||||
// 检测 F12
|
||||
if (event.windows_key_code == 123) {
|
||||
browser.getDevTools().createImmediately();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
|
||||
String targetUrl, String targetFrameName) {
|
||||
// 处理弹出窗口:根据配置决定打开方式
|
||||
if (builder.openLinksInExternalBrowser) {
|
||||
// 使用默认浏览器打开
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(targetUrl));
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
return true; // 拦截弹窗
|
||||
} else {
|
||||
// 在当前浏览器中打开
|
||||
browser.loadURL(targetUrl);
|
||||
return true; // 拦截弹窗并在当前窗口打开
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.addRequestHandler(new CefRequestHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame,
|
||||
CefRequest request, boolean userGesture, boolean isRedirect) {
|
||||
// 处理主窗口导航
|
||||
if (userGesture) {
|
||||
if (builder.openLinksInExternalBrowser) {
|
||||
// 使用默认浏览器打开
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(request.getURL()));
|
||||
return true; // 取消内置浏览器导航
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 允许在当前浏览器中打开
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
client.addContextMenuHandler(new CefContextMenuHandlerAdapter() {
|
||||
@Override
|
||||
public void onBeforeContextMenu(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, CefMenuModel model) {
|
||||
model.clear();
|
||||
if (!params.getSelectionText().isEmpty() || params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST, "复制");
|
||||
}
|
||||
|
||||
if (params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST + 1, "粘贴");
|
||||
model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, int commandId, int eventFlags) {
|
||||
if (commandId == MENU_ID_USER_FIRST) {
|
||||
if (params.isEditable()) {
|
||||
browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0);
|
||||
} else {
|
||||
browser.executeJavaScript(
|
||||
"window.getSelection().toString();",
|
||||
browser.getURL(),
|
||||
0
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 1) {
|
||||
pasteContent(browser, false);
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 2) {
|
||||
pasteContent(browser, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理粘贴操作
|
||||
* @param plainText 是否去除格式(纯文本模式)
|
||||
*/
|
||||
private void pasteContent(CefBrowser browser, boolean plainText) {
|
||||
try {
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
String text = (String) clipboard.getData(DataFlavor.stringFlavor);
|
||||
|
||||
if (plainText) {
|
||||
text = text.replaceAll("<[^>]+>", "");
|
||||
}
|
||||
|
||||
String escapedText = text
|
||||
.replace("\\", "\\\\")
|
||||
.replace("'", "\\'")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r");
|
||||
|
||||
String script = String.format(
|
||||
"if (document.activeElement) {\n" +
|
||||
" document.activeElement.value += '%s';\n" + // 简单追加文本
|
||||
" document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件
|
||||
"}",
|
||||
escapedText
|
||||
);
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
} catch (UnsupportedFlavorException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 添加 alert 弹窗监控处理
|
||||
client.addJSDialogHandler(new CefJSDialogHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onJSDialog(CefBrowser browser, String origin_url, JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) {
|
||||
if (dialog_type == JSDialogType.JSDIALOGTYPE_ALERT) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(
|
||||
BrowserWindowJDialog.this,
|
||||
message_text,
|
||||
"警告",
|
||||
JOptionPane.INFORMATION_MESSAGE
|
||||
);
|
||||
});
|
||||
callback.Continue(true, "");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 3. 拦截所有新窗口(关键修复点!)
|
||||
//client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
// @Override
|
||||
// public boolean onBeforePopup(CefBrowser browser,
|
||||
// CefFrame frame, String target_url, String target_frame_name) {
|
||||
// return true; // 返回true表示拦截弹窗
|
||||
// }
|
||||
//});
|
||||
|
||||
|
||||
Thread.currentThread().setName("BrowserRenderThread");
|
||||
|
||||
|
||||
// 4. 加载HTML
|
||||
if (htmlUrl.isEmpty()) {
|
||||
String fileUrl = new File(htmlPath).toURI().toURL().toString();
|
||||
System.out.println("Loading HTML from: " + fileUrl);
|
||||
|
||||
// 5. 创建浏览器组件(直接添加到内容面板)
|
||||
browser = client.createBrowser(fileUrl, false, false);
|
||||
} else {
|
||||
System.out.println("Loading HTML from: " + htmlUrl);
|
||||
browser = client.createBrowser(htmlUrl, false, false);
|
||||
}
|
||||
|
||||
Component browserComponent = browser.getUIComponent();
|
||||
|
||||
if (builder.browserCreationCallback != null) {
|
||||
boolean handled = builder.browserCreationCallback.onLayoutCustomization(
|
||||
this, // 当前窗口
|
||||
getContentPane(), // 内容面板
|
||||
browserComponent, // 浏览器组件
|
||||
builder // 构建器对象
|
||||
);
|
||||
|
||||
// 如果回调返回true,跳过默认布局
|
||||
if (handled) {
|
||||
// 设置窗口基本属性
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size);
|
||||
setLocationRelativeTo(builder.parentFrame);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
// 添加资源释放监听器
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true);
|
||||
client.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
setVisible(true);
|
||||
return browserComponent; // 直接返回,跳过默认布局
|
||||
}
|
||||
}
|
||||
|
||||
updateTheme();
|
||||
|
||||
CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
|
||||
config.jsQueryFunction = "javaQuery";// 定义方法
|
||||
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
|
||||
|
||||
|
||||
// 6. 配置窗口布局(确保只添加一次)
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
getContentPane().removeAll();
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
|
||||
// 透明拖拽层(仅顶部可拖拽)
|
||||
JPanel dragPanel = new JPanel(new BorderLayout());
|
||||
dragPanel.setOpaque(false);
|
||||
|
||||
JPanel titleBar = new JPanel();
|
||||
titleBar.setOpaque(false);
|
||||
titleBar.setPreferredSize(new Dimension(builder.size.width, 20));
|
||||
final Point[] dragStart = new Point[1];
|
||||
titleBar.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
dragStart[0] = e.getPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
dragStart[0] = null;
|
||||
}
|
||||
});
|
||||
titleBar.addMouseMotionListener(new MouseMotionAdapter() {
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
if (dragStart[0] != null) {
|
||||
Point curr = e.getLocationOnScreen();
|
||||
setLocation(curr.x - dragStart[0].x, curr.y - dragStart[0].y);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
dragPanel.add(titleBar, BorderLayout.NORTH);
|
||||
getContentPane().add(dragPanel, BorderLayout.CENTER);
|
||||
getContentPane().add(browserComponent, BorderLayout.CENTER);
|
||||
|
||||
// 7. 窗口属性设置
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size);
|
||||
setLocationRelativeTo(builder.parentFrame);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
// 8. 资源释放
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true);
|
||||
client.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
setVisible(true);
|
||||
|
||||
});
|
||||
return browserComponent;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
} else {
|
||||
isInitialized = false;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
dispose();
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
*/
|
||||
public void updateTheme() {
|
||||
// 1. 获取Java字体信息
|
||||
String fontInfo = getSystemFontsInfo();
|
||||
injectFontInfoToPage(browser, fontInfo);
|
||||
|
||||
// 2. 注入主题信息
|
||||
if (AxisInnovatorsBox.getMain() != null) {
|
||||
boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
|
||||
injectThemeInfoToPage(browser, isDarkTheme);
|
||||
}
|
||||
|
||||
//// 3. 刷新浏览器
|
||||
//SwingUtilities.invokeLater(() -> {
|
||||
// browser.reload();
|
||||
//});
|
||||
|
||||
}
|
||||
private void injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
String themeInfo = String.format(
|
||||
"{\"isDarkTheme\": %s, \"timestamp\": %d}",
|
||||
isDarkTheme,
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
String script =
|
||||
"window.javaThemeInfo = " + themeInfo + ";\n" +
|
||||
"console.log('Java theme information has been loaded:', window.javaThemeInfo);\n" +
|
||||
"\n" +
|
||||
"if (typeof applyJavaTheme === 'function') {\n" +
|
||||
" applyJavaTheme(window.javaThemeInfo);\n" +
|
||||
"}\n" +
|
||||
"\n" +
|
||||
"var event = new CustomEvent('javaThemeChanged', {\n" +
|
||||
" detail: window.javaThemeInfo\n" +
|
||||
"});\n" +
|
||||
"document.dispatchEvent(event);\n" +
|
||||
"console.log('The javaThemeChanged event is dispatched');";
|
||||
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
|
||||
browser.executeJavaScript(
|
||||
"console.log('Theme information injection is complete,window.javaThemeInfo:', typeof window.javaThemeInfo);" +
|
||||
"console.log('Number of theme event listeners:', document.eventListeners ? document.eventListeners('javaThemeChanged') : '无法获取');",
|
||||
browser.getURL(), 0
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取Java字体信息(从UIManager获取)
|
||||
*/
|
||||
private String getSystemFontsInfo() {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
JsonObject fontInfo = new JsonObject();
|
||||
JsonObject uiFonts = new JsonObject();
|
||||
|
||||
String[] fontKeys = {
|
||||
"Label.font", "Button.font", "ToggleButton.font", "RadioButton.font",
|
||||
"CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font",
|
||||
"TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font",
|
||||
"FormattedTextField.font", "Table.font", "TableHeader.font", "List.font",
|
||||
"Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font",
|
||||
"PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font",
|
||||
"Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont",
|
||||
"OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font"
|
||||
};
|
||||
|
||||
for (String key : fontKeys) {
|
||||
Font font = UIManager.getFont(key);
|
||||
if (font != null) {
|
||||
JsonObject fontObj = new JsonObject();
|
||||
fontObj.addProperty("name", font.getFontName());
|
||||
fontObj.addProperty("family", font.getFamily());
|
||||
fontObj.addProperty("size", font.getSize());
|
||||
fontObj.addProperty("style", font.getStyle());
|
||||
fontObj.addProperty("bold", font.isBold());
|
||||
fontObj.addProperty("italic", font.isItalic());
|
||||
fontObj.addProperty("plain", font.isPlain());
|
||||
uiFonts.add(key, fontObj);
|
||||
}
|
||||
}
|
||||
|
||||
fontInfo.add("uiFonts", uiFonts);
|
||||
fontInfo.addProperty("timestamp", System.currentTimeMillis());
|
||||
fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName());
|
||||
|
||||
return gson.toJson(fontInfo);
|
||||
} catch (Exception e) {
|
||||
return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入字体信息到页面并设置字体
|
||||
*/
|
||||
private void injectFontInfoToPage(CefBrowser browser, String fontInfo) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
client.addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
// 使用更简单的脚本来注入字体信息
|
||||
String script =
|
||||
"if (typeof window.javaFontInfo === 'undefined') {" +
|
||||
" window.javaFontInfo = " + fontInfo + ";" +
|
||||
" console.log('Java font information has been loaded:', window.javaFontInfo);" +
|
||||
" " +
|
||||
" var event = new CustomEvent('javaFontsLoaded', {" +
|
||||
" detail: window.javaFontInfo" +
|
||||
" });" +
|
||||
" document.dispatchEvent(event);" +
|
||||
" console.log('The javaFontsLoaded event is dispatched');" +
|
||||
"}";
|
||||
|
||||
System.out.println("正在注入字体信息到页面...");
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
|
||||
// 添加调试信息
|
||||
browser.executeJavaScript(
|
||||
"console.log('Font information injection is complete,window.javaFontInfo:', typeof window.javaFontInfo);" +
|
||||
"console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');",
|
||||
browser.getURL(), 0
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public static void printStackTrace() {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
for (int i = 2; i < stackTrace.length; i++) {
|
||||
StackTraceElement element = stackTrace[i];
|
||||
System.out.println(element.getClassName() + "." + element.getMethodName() +
|
||||
"(" + (element.getFileName() != null ? element.getFileName() : "Unknown Source") +
|
||||
":" + element.getLineNumber() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean b) {
|
||||
if (b) {
|
||||
if (browser != null) {
|
||||
updateTheme();
|
||||
}
|
||||
}
|
||||
super.setVisible(b);
|
||||
}
|
||||
|
||||
public Component getBrowserComponent() {
|
||||
return browserComponent;
|
||||
}
|
||||
|
||||
private void setupMessageHandlers(WindowOperationHandler handler) {
|
||||
if (client != null) {
|
||||
msgRouter = CefMessageRouter.create();
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onQuery(CefBrowser browser,
|
||||
CefFrame frame,
|
||||
long queryId,
|
||||
String request,
|
||||
boolean persistent,
|
||||
CefQueryCallback callback) {
|
||||
if (request.startsWith("system:")) {
|
||||
String[] parts = request.split(":");
|
||||
String operation = parts.length >= 2 ? parts[1] : null;
|
||||
String targetWindow = parts.length > 2 ? parts[2] : null;
|
||||
handler.handleOperation(
|
||||
new WindowOperation(operation, targetWindow, callback) // [!code ++]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (request.startsWith("java-response:")) {
|
||||
String[] parts = request.split(":");
|
||||
String requestId = parts[1];
|
||||
String responseData = parts.length > 2 ? parts[2] : "";
|
||||
Consumer<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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 不需要 props,URL 已经是文件路径(已做过替换)
|
||||
connection = DriverManager.getConnection(url);
|
||||
break;
|
||||
case "oracle":
|
||||
connection = DriverManager.getConnection(url, props);
|
||||
break;
|
||||
case "h2":
|
||||
// H2 使用默认用户 sa / 空密码(如果需要可调整)
|
||||
connection = DriverManager.getConnection(url, "sa", "");
|
||||
break;
|
||||
default:
|
||||
throw new SQLException("不支持的数据库类型: " + driver);
|
||||
}
|
||||
|
||||
connections.put(connectionId, connection);
|
||||
connectionInfo.put(connectionId, new DatabaseInfo(driver, url, host, port, database, username));
|
||||
return connectionId;
|
||||
}
|
||||
|
||||
public static void disconnect(String connectionId) throws SQLException {
|
||||
Connection connection = connections.get(connectionId);
|
||||
if (connection != null && !connection.isClosed()) {
|
||||
connection.close();
|
||||
}
|
||||
connections.remove(connectionId);
|
||||
connectionInfo.remove(connectionId);
|
||||
}
|
||||
|
||||
public static Connection getConnection(String connectionId) {
|
||||
return connections.get(connectionId);
|
||||
}
|
||||
|
||||
public static DatabaseInfo getConnectionInfo(String connectionId) {
|
||||
return connectionInfo.get(connectionId);
|
||||
}
|
||||
|
||||
private static String buildConnectionUrl(String driver, String host, String port, String database) {
|
||||
String drv = driver == null ? "" : driver.toLowerCase();
|
||||
switch (drv) {
|
||||
case "mysql":
|
||||
return "jdbc:mysql://" + host + ":" + port + "/" + database;
|
||||
case "postgresql":
|
||||
return "jdbc:postgresql://" + host + ":" + port + "/" + database;
|
||||
case "sqlite":
|
||||
// 对于 SQLite,database 可能是绝对路径或相对文件名,先把反斜杠替成正斜杠
|
||||
if (database == null || database.isEmpty()) {
|
||||
return "jdbc:sqlite::memory:";
|
||||
}
|
||||
String normalized = database.replace("\\", "/");
|
||||
// 如果看起来像相对文件名(不含冒号也不以 / 开头),则当作相对于用户目录的路径
|
||||
if (!normalized.contains(":") && !normalized.startsWith("/")) {
|
||||
String userHome = System.getProperty("user.home").replace("\\", "/");
|
||||
normalized = userHome + "/" + normalized;
|
||||
}
|
||||
return "jdbc:sqlite:" + normalized;
|
||||
case "oracle":
|
||||
return "jdbc:oracle:thin:@" + host + ":" + port + ":" + database;
|
||||
case "h2":
|
||||
// H2 文件路径同样做反斜杠处理
|
||||
if (database == null || database.isEmpty()) {
|
||||
String userHome = System.getProperty("user.home").replace("\\", "/");
|
||||
return "jdbc:h2:file:" + userHome + "/.axis_innovators_box/databases/h2db";
|
||||
} else {
|
||||
String norm = database.replace("\\", "/");
|
||||
// 如果传入仅是名字(无斜杠或冒号),则存到用户目录下
|
||||
if (!norm.contains("/") && !norm.contains(":")) {
|
||||
String userHome = System.getProperty("user.home").replace("\\", "/");
|
||||
norm = userHome + "/.axis_innovators_box/databases/" + norm;
|
||||
}
|
||||
return "jdbc:h2:file:" + norm;
|
||||
}
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的数据库类型: " + driver);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 在服务器上创建数据库(MySQL / PostgreSQL / Oracle(示例))
|
||||
* @param driver mysql | postgresql | oracle
|
||||
* @param host 数据库主机
|
||||
* @param port 端口
|
||||
* @param dbName 要创建的数据库名(或 schema 名)
|
||||
* @param adminUser 管理员用户名(用于创建数据库)
|
||||
* @param adminPassword 管理员密码
|
||||
* @return 如果创建成功返回一个简短消息,否则抛出 SQLException
|
||||
* @throws SQLException
|
||||
*/
|
||||
public static String createDatabaseOnServer(String driver, String host, String port,
|
||||
String dbName, String adminUser, String adminPassword) throws SQLException {
|
||||
if (driver == null) throw new SQLException("driver 不能为空");
|
||||
String drv = driver.toLowerCase().trim();
|
||||
|
||||
// 简单校验 dbName(避免注入)——只允许字母数字下划线
|
||||
if (dbName == null || !dbName.matches("[A-Za-z0-9_]+")) {
|
||||
throw new SQLException("不合法的数据库名: " + dbName);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (drv) {
|
||||
case "mysql":
|
||||
// 加载驱动(如果尚未加载)
|
||||
try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) {
|
||||
throw new SQLException("MySQL 驱动未找到,请加入 mysql-connector-java 到 classpath", e);
|
||||
}
|
||||
// 连接到服务器的默认库(不指定数据库)以执行 CREATE DATABASE
|
||||
String mysqlUrl = "jdbc:mysql://" + host + ":" + port + "/?useSSL=false&serverTimezone=UTC";
|
||||
try (Connection conn = DriverManager.getConnection(mysqlUrl, adminUser, adminPassword);
|
||||
Statement st = conn.createStatement()) {
|
||||
String sql = "CREATE DATABASE `" + dbName + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci";
|
||||
st.executeUpdate(sql);
|
||||
}
|
||||
return "MySQL 数据库创建成功: " + dbName;
|
||||
|
||||
case "postgresql":
|
||||
case "postgres":
|
||||
try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e) {
|
||||
throw new SQLException("Postgres 驱动未找到,请加入 postgresql 到 classpath", e);
|
||||
}
|
||||
// 连接到默认 postgres 数据库以创建新数据库
|
||||
String pgUrl = "jdbc:postgresql://" + host + ":" + port + "/postgres";
|
||||
try (Connection conn = DriverManager.getConnection(pgUrl, adminUser, adminPassword);
|
||||
Statement st = conn.createStatement()) {
|
||||
String sql = "CREATE DATABASE " + dbName + " WITH ENCODING 'UTF8'";
|
||||
st.executeUpdate(sql);
|
||||
}
|
||||
return "PostgreSQL 数据库创建成功: " + dbName;
|
||||
|
||||
case "oracle":
|
||||
// Oracle 数据库“创建数据库”通常由 DBA 完成(复杂),这里示例创建用户/模式(更常见)
|
||||
try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException e) {
|
||||
throw new SQLException("Oracle 驱动未找到,请把 ojdbc.jar 加入 classpath", e);
|
||||
}
|
||||
// 需使用具有足够权限的账户(通常为 sys or system),并且 URL 需要正确(SID / ServiceName)
|
||||
// 下面示例假设通过 SID 连接: jdbc:oracle:thin:@host:port:SID
|
||||
String oracleUrl = "jdbc:oracle:thin:@" + host + ":" + port + ":" + "ORCL"; // 把 ORCL 换成实际 SID
|
||||
try (Connection conn = DriverManager.getConnection(oracleUrl, adminUser, adminPassword);
|
||||
Statement st = conn.createStatement()) {
|
||||
// 创建 user(schema)示例
|
||||
String pwd = adminPassword; // 实际应使用独立密码,不推荐用 adminPassword
|
||||
String createUser = "CREATE USER " + dbName + " IDENTIFIED BY \"" + pwd + "\"";
|
||||
String grant = "GRANT CONNECT, RESOURCE TO " + dbName;
|
||||
st.executeUpdate(createUser);
|
||||
st.executeUpdate(grant);
|
||||
} catch (SQLException ex) {
|
||||
// Oracle 操作更容易失败,给出提示
|
||||
throw new SQLException("Oracle: 无法创建用户/模式,请检查权限和 URL(通常需由 DBA 操作): " + ex.getMessage(), ex);
|
||||
}
|
||||
return "Oracle 用户/模式创建成功(注意:真正的 DB 实例通常由 DBA 管理): " + dbName;
|
||||
|
||||
default:
|
||||
throw new SQLException("不支持的数据库类型: " + driver);
|
||||
}
|
||||
} catch (SQLException se) {
|
||||
// 透传 SQLException,调用方会拿到 message 并反馈给前端
|
||||
throw se;
|
||||
} catch (Exception e) {
|
||||
throw new SQLException("创建数据库时发生异常: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
public static String createLocalDatabase(String driver, String dbName) throws SQLException {
|
||||
switch (driver.toLowerCase()) {
|
||||
case "sqlite":
|
||||
// 创建目录并构造规范化路径(确保路径使用正斜杠)
|
||||
String dbFileName = dbName.endsWith(".db") ? dbName : (dbName + ".db");
|
||||
java.nio.file.Path dbDir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases");
|
||||
try {
|
||||
java.nio.file.Files.createDirectories(dbDir);
|
||||
} catch (Exception e) {
|
||||
throw new SQLException("无法创建数据库目录: " + e.getMessage(), e);
|
||||
}
|
||||
String dbPath = dbDir.resolve(dbFileName).toAbsolutePath().toString().replace("\\", "/");
|
||||
|
||||
// 显式加载 sqlite 驱动(避免 No suitable driver)
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new SQLException("未找到 sqlite 驱动,请确认 sqlite-jdbc 已加入 classpath", e);
|
||||
}
|
||||
|
||||
// 直接使用 connect 构建连接(connect 中会通过 buildConnectionUrl 处理 path)
|
||||
String connectionId = connect("sqlite", "", "", dbPath, "", "");
|
||||
|
||||
// 创建示例表
|
||||
createSampleTables(connectionId);
|
||||
|
||||
return connectionId;
|
||||
|
||||
case "h2":
|
||||
java.nio.file.Path h2Dir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases");
|
||||
try {
|
||||
java.nio.file.Files.createDirectories(h2Dir);
|
||||
} catch (Exception e) {
|
||||
throw new SQLException("无法创建数据库目录: " + e.getMessage(), e);
|
||||
}
|
||||
String h2Path = h2Dir.resolve(dbName).toAbsolutePath().toString().replace("\\", "/");
|
||||
String h2Url = "jdbc:h2:file:" + h2Path;
|
||||
|
||||
try {
|
||||
Class.forName("org.h2.Driver");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new SQLException("未找到 H2 驱动,请确认 h2.jar 已加入 classpath", e);
|
||||
}
|
||||
|
||||
Connection h2Conn = DriverManager.getConnection(h2Url, "sa", "");
|
||||
String h2ConnectionId = "conn_" + System.currentTimeMillis();
|
||||
connections.put(h2ConnectionId, h2Conn);
|
||||
connectionInfo.put(h2ConnectionId, new DatabaseInfo("h2", h2Url, "localhost", "", dbName, "sa"));
|
||||
|
||||
createSampleTables(h2ConnectionId);
|
||||
|
||||
return h2ConnectionId;
|
||||
|
||||
default:
|
||||
throw new SQLException("不支持创建本地数据库类型: " + driver);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSampleTables(String connectionId) throws SQLException {
|
||||
Connection conn = getConnection(connectionId);
|
||||
DatabaseInfo info = getConnectionInfo(connectionId);
|
||||
|
||||
if ("sqlite".equals(info.driver) || "h2".equals(info.driver)) {
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
// 创建用户表
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS users (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"username VARCHAR(50) NOT NULL UNIQUE, " +
|
||||
"email VARCHAR(100) NOT NULL, " +
|
||||
"password VARCHAR(100) NOT NULL, " +
|
||||
"status VARCHAR(20) DEFAULT 'active', " +
|
||||
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
|
||||
")");
|
||||
|
||||
// 创建产品表
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS products (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"name VARCHAR(100) NOT NULL, " +
|
||||
"description TEXT, " +
|
||||
"price DECIMAL(10,2) NOT NULL, " +
|
||||
"stock INTEGER DEFAULT 0, " +
|
||||
"category VARCHAR(50), " +
|
||||
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
|
||||
")");
|
||||
|
||||
// 创建订单表
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS orders (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"user_id INTEGER, " +
|
||||
"product_id INTEGER, " +
|
||||
"quantity INTEGER NOT NULL, " +
|
||||
"total_price DECIMAL(10,2) NOT NULL, " +
|
||||
"status VARCHAR(20) DEFAULT 'pending', " +
|
||||
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
||||
"FOREIGN KEY (user_id) REFERENCES users(id), " +
|
||||
"FOREIGN KEY (product_id) REFERENCES products(id)" +
|
||||
")");
|
||||
|
||||
// 插入示例数据
|
||||
insertSampleData(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void insertSampleData(Connection conn) throws SQLException {
|
||||
// 检查是否已有数据
|
||||
try (Statement checkStmt = conn.createStatement();
|
||||
ResultSet rs = checkStmt.executeQuery("SELECT COUNT(*) FROM users")) {
|
||||
if (rs.next() && rs.getInt(1) == 0) {
|
||||
// 插入用户数据
|
||||
try (PreparedStatement pstmt = conn.prepareStatement(
|
||||
"INSERT INTO users (username, email, password) VALUES (?, ?, ?)")) {
|
||||
String[][] users = {
|
||||
{"admin", "admin@example.com", "password123"},
|
||||
{"user1", "user1@example.com", "password123"},
|
||||
{"user2", "user2@example.com", "password123"}
|
||||
};
|
||||
|
||||
for (String[] user : users) {
|
||||
pstmt.setString(1, user[0]);
|
||||
pstmt.setString(2, user[1]);
|
||||
pstmt.setString(3, user[2]);
|
||||
pstmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// 插入产品数据
|
||||
try (PreparedStatement pstmt = conn.prepareStatement(
|
||||
"INSERT INTO products (name, description, price, stock, category) VALUES (?, ?, ?, ?, ?)")) {
|
||||
Object[][] products = {
|
||||
{"笔记本电脑", "高性能笔记本电脑", 5999.99, 50, "电子"},
|
||||
{"智能手机", "最新款智能手机", 3999.99, 100, "电子"},
|
||||
{"办公椅", "舒适办公椅", 299.99, 30, "家居"},
|
||||
{"咖啡机", "全自动咖啡机", 899.99, 20, "家电"}
|
||||
};
|
||||
|
||||
for (Object[] product : products) {
|
||||
pstmt.setString(1, (String) product[0]);
|
||||
pstmt.setString(2, (String) product[1]);
|
||||
pstmt.setDouble(3, (Double) product[2]);
|
||||
pstmt.setInt(4, (Integer) product[3]);
|
||||
pstmt.setString(5, (String) product[4]);
|
||||
pstmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.chuangzhou.vivid2D.events;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class GlobalEventBus {
|
||||
/**
|
||||
* 全局事件总线
|
||||
*/
|
||||
public static final EventBus EVENT_BUS = new EventBus();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public class EventPanel extends JPanel {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
public class ModelAIPanel {
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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("键盘管理器已清理");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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("操作历史已清除");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() : "无");
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.chuangzhou.vivid2D.render.model;
|
||||
|
||||
/**
|
||||
* 模型事件
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public interface ModelEvent {
|
||||
void trigger(String eventName, Object eventBus);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user