chore(cache): update jcef cache log files
- Updated Extension Scripts log file entries - Modified shared_proto_db metadata log with new entries - Adjusted Site Characteristics Database log content - Refreshed Session Storage log data
This commit is contained in:
@@ -603,8 +603,10 @@ public class BrowserWindowJDialog extends JDialog {
|
||||
injectFontInfoToPage(browser, fontInfo);
|
||||
|
||||
// 2. 注入主题信息
|
||||
boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
|
||||
injectThemeInfoToPage(browser, isDarkTheme);
|
||||
if (AxisInnovatorsBox.getMain() != null) {
|
||||
boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
|
||||
injectThemeInfoToPage(browser, isDarkTheme);
|
||||
}
|
||||
|
||||
//// 3. 刷新浏览器
|
||||
//SwingUtilities.invokeLater(() -> {
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
package com.axis.innovators.box.events;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 事件注解,事件订阅注解,用于标记事件监听方法
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface SubscribeEvent {
|
||||
/**
|
||||
* 事件处理的优先级。值越大,优先级越高。
|
||||
* @return 优先级
|
||||
*/
|
||||
int priority() default 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/main/java/com/chuangzhou/vivid2D/block/BlocklyPanel.java
Normal file
97
src/main/java/com/chuangzhou/vivid2D/block/BlocklyPanel.java
Normal file
@@ -0,0 +1,97 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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 渲染器代码
|
||||
}
|
||||
}
|
||||
19
src/main/java/com/chuangzhou/vivid2D/events/Event.java
Normal file
19
src/main/java/com/chuangzhou/vivid2D/events/Event.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.chuangzhou.vivid2D.events;
|
||||
|
||||
/**
|
||||
* 事件基础接口
|
||||
* 所有事件都应实现此接口
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public interface Event {
|
||||
/**
|
||||
* @return 事件是否已被取消
|
||||
*/
|
||||
boolean isCancelled();
|
||||
|
||||
/**
|
||||
* 设置事件的取消状态
|
||||
* @param cancelled true 表示取消事件,后续的订阅者将不会收到此事件
|
||||
*/
|
||||
void setCancelled(boolean cancelled);
|
||||
}
|
||||
192
src/main/java/com/chuangzhou/vivid2D/events/EventBus.java
Normal file
192
src/main/java/com/chuangzhou/vivid2D/events/EventBus.java
Normal file
@@ -0,0 +1,192 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.chuangzhou.vivid2D.events;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class GlobalEventBus {
|
||||
/**
|
||||
* 全局事件总线
|
||||
*/
|
||||
public static final EventBus EVENT_BUS = new EventBus();
|
||||
}
|
||||
32
src/main/java/com/chuangzhou/vivid2D/events/PostResult.java
Normal file
32
src/main/java/com/chuangzhou/vivid2D/events/PostResult.java
Normal file
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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) {
|
||||
// 不支持取消
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,8 @@ public class KeyframeDetailsDialog extends JDialog {
|
||||
this.paramIdMap = new HashMap<>();
|
||||
this.paramIdMap.put("position", "位置");
|
||||
this.paramIdMap.put("rotate", "旋转");
|
||||
this.paramIdMap.put("secondaryVertex", "二级顶点变形器(顶点位置)");
|
||||
//this.paramIdMap.put("secondaryVertex", "二级顶点变形器(顶点位置)");
|
||||
this.paramIdMap.put("meshVertices", "变形器(顶点位置)");
|
||||
this.paramIdMap.put("scale", "缩放");
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ 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;
|
||||
@@ -58,6 +59,7 @@ public class ModelRenderPanel extends JPanel {
|
||||
private volatile long lastClickTime = 0;
|
||||
private static final int DOUBLE_CLICK_INTERVAL = 300;
|
||||
private final ToolManagement toolManagement;
|
||||
private VolatileImage vImage = null;
|
||||
|
||||
/**
|
||||
* 获取摄像机实例
|
||||
@@ -511,28 +513,67 @@ public class ModelRenderPanel extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
Graphics2D g2d = (Graphics2D) g.create();
|
||||
try {
|
||||
BufferedImage imgToDraw = glContextManager.getCurrentFrame()
|
||||
!= null ? glContextManager.getCurrentFrame() : glContextManager.getLastFrame();
|
||||
int panelW = getWidth();
|
||||
int panelH = getHeight();
|
||||
if (imgToDraw != null) {
|
||||
g2d.drawImage(imgToDraw, 0, 0, panelW, panelH, null);
|
||||
} else {
|
||||
g2d.setColor(Color.DARK_GRAY);
|
||||
g2d.fillRect(0, 0, panelW, panelH);
|
||||
// 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.
|
||||
}
|
||||
if (getModel() == null) {
|
||||
g2d.setColor(new Color(255, 255, 0, 200));
|
||||
g2d.drawString("模型未加载", 10, 20);
|
||||
// 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();
|
||||
}
|
||||
} finally {
|
||||
g2d.dispose();
|
||||
|
||||
// --- 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -498,7 +498,7 @@ public class ParametersPanel extends JPanel {
|
||||
return nearest;
|
||||
} else {
|
||||
// 如果不相等,则不认为是“选中”的关键帧
|
||||
return null;
|
||||
return -114514f;
|
||||
}
|
||||
}
|
||||
// -------------------------------------------------------------
|
||||
@@ -515,7 +515,7 @@ public class ParametersPanel extends JPanel {
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return -114514f;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ 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.GL;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.*;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -13,41 +12,45 @@ 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.nio.ByteOrder;
|
||||
import java.nio.IntBuffer;
|
||||
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 BufferedImage currentFrame;
|
||||
private volatile boolean contextInitialized = false;
|
||||
private final CompletableFuture<Void> contextReady = new CompletableFuture<>();
|
||||
private String modelPath;
|
||||
private final AtomicReference<Model2D> modelRef = new AtomicReference<>();
|
||||
|
||||
private BufferedImage lastFrame = null;
|
||||
private ByteBuffer pixelBuffer = null;
|
||||
private int[] pixelInts = null;
|
||||
private int[] argbInts = null;
|
||||
public volatile float displayScale = 1.0f; // 当前可视缩放(用于检测阈值/角点等)
|
||||
public volatile float targetScale = 1.0f; // 目标缩放(鼠标滚轮/程序改变时设置)
|
||||
// --- 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;
|
||||
|
||||
// 任务队列,用于在 GL 上下文线程执行代码
|
||||
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; // 0..1, 越大收敛越快(建议 0.12-0.25)
|
||||
private static final float ZOOM_SMOOTHING = 0.18f;
|
||||
private RepaintCallback repaintCallback;
|
||||
|
||||
private final CompletableFuture<Model2D> modelReady = new CompletableFuture<>();
|
||||
@@ -76,11 +79,7 @@ public class GLContextManager {
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建离屏 OpenGL 上下文
|
||||
*/
|
||||
private void createOffscreenContext() throws Exception {
|
||||
// 设置窗口提示
|
||||
GLFW.glfwDefaultWindowHints();
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
@@ -89,54 +88,59 @@ public class GLContextManager {
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GL11.GL_TRUE);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, 4);
|
||||
|
||||
// 创建离屏窗口(像素尺寸以当前 width/height 为准)
|
||||
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
|
||||
RenderSystem.beginInitialization();
|
||||
RenderSystem.initRenderThread();
|
||||
|
||||
// 使用 RenderSystem 设置视口
|
||||
RenderSystem.viewport(0, 0, width, height);
|
||||
|
||||
// 分配像素读取缓冲
|
||||
int pixelCount = Math.max(1, width * height);
|
||||
pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4);
|
||||
pixelBuffer.order(ByteOrder.nativeOrder());
|
||||
pixelInts = new int[pixelCount];
|
||||
argbInts = new int[pixelCount];
|
||||
initializeFrameResources();
|
||||
|
||||
// 初始化 ModelRender
|
||||
ModelRender.initialize();
|
||||
|
||||
RenderSystem.finishInitialization();
|
||||
|
||||
// 在正确的上下文中加载模型(可能会耗时)
|
||||
loadModelInContext();
|
||||
|
||||
// 标记上下文已初始化并完成通知(只 complete 一次)
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 OpenGL 上下文中加载模型
|
||||
*/
|
||||
private void loadModelInContext() {
|
||||
try {
|
||||
if (modelPath != null) {
|
||||
@@ -154,15 +158,10 @@ public class GLContextManager {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("模型加载失败: {}", e.getMessage(), e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动渲染线程
|
||||
*/
|
||||
public void startRendering() {
|
||||
// 初始化 GLFW
|
||||
if (!GLFW.glfwInit()) {
|
||||
throw new RuntimeException("无法初始化 GLFW");
|
||||
}
|
||||
@@ -171,13 +170,8 @@ public class GLContextManager {
|
||||
if (modelRef.get() != null && !modelReady.isDone()) {
|
||||
modelReady.complete(modelRef.get());
|
||||
}
|
||||
|
||||
createOffscreenContext();
|
||||
|
||||
// 等待上下文就绪后再开始渲染循环(contextReady 由 createOffscreenContext 完成)
|
||||
contextReady.get();
|
||||
|
||||
// 确保当前线程一直持有该 GL 上下文(避免在每个任务/帧中重复 makeCurrent)
|
||||
GLFW.glfwMakeContextCurrent(windowId);
|
||||
|
||||
final long targetNs = 1_000_000_000L / 60L; // 60 FPS
|
||||
@@ -200,153 +194,95 @@ public class GLContextManager {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
renderThread.setDaemon(true);
|
||||
renderThread.setName("GL-Render-Thread");
|
||||
renderThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染单帧并读取到 BufferedImage
|
||||
*/
|
||||
private void renderFrame() {
|
||||
if (!contextInitialized || windowId == 0) return;
|
||||
// 确保在当前上下文中
|
||||
GLFW.glfwMakeContextCurrent(windowId);
|
||||
Model2D currentModel = modelRef.get();
|
||||
if (currentModel != null) {
|
||||
try {
|
||||
Color panelBackground = UIManager.getColor("Panel.background");
|
||||
Color darkerBackground = panelBackground.darker();
|
||||
float r = darkerBackground.getRed() / 255.0f;
|
||||
float g = darkerBackground.getGreen() / 255.0f;
|
||||
float b = darkerBackground.getBlue() / 255.0f;
|
||||
float a = darkerBackground.getAlpha() / 255.0f;
|
||||
RenderSystem.setClearColor(r, g, b, a);
|
||||
RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT);
|
||||
// 渲染模型
|
||||
ModelRender.render(1.0f / 60f, currentModel);
|
||||
// 读取像素数据到 BufferedImage
|
||||
readPixelsToImage();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("渲染错误", e);
|
||||
renderErrorFrame(e.getMessage());
|
||||
}
|
||||
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 {
|
||||
// 没有模型时显示默认背景
|
||||
renderDefaultBackground();
|
||||
RenderSystem.setClearColor(0.1f, 0.1f, 0.15f, 1f);
|
||||
RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
// 在 Swing EDT 中更新显示
|
||||
readPixelsToImage();
|
||||
|
||||
if (repaintCallback != null) {
|
||||
repaintCallback.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染默认背景
|
||||
*/
|
||||
private void renderDefaultBackground() {
|
||||
RenderSystem.setClearColor(0.1f, 0.1f, 0.15f, 1f);
|
||||
RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT);
|
||||
readPixelsToImage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染错误帧
|
||||
*/
|
||||
private void renderErrorFrame(String errorMessage) {
|
||||
GL11.glClearColor(0.3f, 0.1f, 0.1f, 1f);
|
||||
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
|
||||
readPixelsToImage();
|
||||
BufferedImage errorImage = new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = errorImage.createGraphics();
|
||||
g2d.setColor(Color.DARK_GRAY);
|
||||
g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight());
|
||||
g2d.setColor(Color.RED);
|
||||
g2d.drawString("渲染错误: " + errorMessage, 10, 20);
|
||||
g2d.dispose();
|
||||
currentFrame = errorImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 OpenGL 像素数据到 BufferedImage
|
||||
* Reads pixels asynchronously using PBOs into the back buffer, then swaps it to the front.
|
||||
*/
|
||||
private void readPixelsToImage() {
|
||||
try {
|
||||
final int w = Math.max(1, this.width);
|
||||
final int h = Math.max(1, this.height);
|
||||
final int pixelCount = w * h;
|
||||
final int w = Math.max(1, this.width);
|
||||
final int h = Math.max(1, this.height);
|
||||
|
||||
// 确保缓冲区大小匹配(可能在 resize 后需要重建)
|
||||
if (pixelBuffer == null || pixelInts == null || pixelInts.length != pixelCount) {
|
||||
if (pixelBuffer != null) {
|
||||
try {
|
||||
MemoryUtil.memFree(pixelBuffer);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4);
|
||||
pixelBuffer.order(ByteOrder.nativeOrder());
|
||||
pixelInts = new int[pixelCount];
|
||||
argbInts = new int[pixelCount];
|
||||
}
|
||||
GL15.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, pboIds[pboIndex]);
|
||||
|
||||
pixelBuffer.clear();
|
||||
// 从 GPU 读取 RGBA 字节到本地缓冲 - 使用 RenderSystem
|
||||
RenderSystem.readPixels(0, 0, w, h, RenderSystem.GL_RGBA, RenderSystem.GL_UNSIGNED_BYTE, pixelBuffer);
|
||||
RenderSystem.readPixels(0, 0, w, h, GL13.GL_BGRA, GL13.GL_UNSIGNED_INT_8_8_8_8_REV, 0);
|
||||
|
||||
// 以 int 批量读取(依赖本机字节序),然后转换为带 alpha 的 ARGB 并垂直翻转
|
||||
IntBuffer ib = pixelBuffer.asIntBuffer();
|
||||
ib.get(pixelInts, 0, pixelCount);
|
||||
GL15.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, pboIds[nextPboIndex]);
|
||||
ByteBuffer byteBuffer = GL15.glMapBuffer(GL21.GL_PIXEL_PACK_BUFFER, GL15.GL_READ_ONLY);
|
||||
|
||||
// 转换并翻转(RGBA -> ARGB)
|
||||
for (int y = 0; y < h; y++) {
|
||||
int srcRow = (h - y - 1) * w;
|
||||
int dstRow = y * w;
|
||||
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 rgba = pixelInts[srcRow + x];
|
||||
|
||||
// 提取字节(考虑 native order,按 RGBA 存放)
|
||||
int r = (rgba >> 0) & 0xFF;
|
||||
int g = (rgba >> 8) & 0xFF;
|
||||
int b = (rgba >> 16) & 0xFF;
|
||||
int a = (rgba >> 24) & 0xFF;
|
||||
|
||||
// 组合为 ARGB (BufferedImage 使用 ARGB)
|
||||
argbInts[dstRow + x] = (a << 24) | (r << 16) | (g << 8) | b;
|
||||
int pixel1 = backBufferPixelArray[row1 + x];
|
||||
backBufferPixelArray[row1 + x] = backBufferPixelArray[row2 + x];
|
||||
backBufferPixelArray[row2 + x] = pixel1;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用一次 setRGB 写入 BufferedImage(比逐像素 setRGB 快)
|
||||
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
||||
image.setRGB(0, 0, w, h, argbInts, 0, w);
|
||||
GL15.glUnmapBuffer(GL21.GL_PIXEL_PACK_BUFFER);
|
||||
}
|
||||
|
||||
currentFrame = image;
|
||||
lastFrame = image;
|
||||
} catch (Exception e) {
|
||||
logger.error("读取像素数据错误", e);
|
||||
// 创建错误图像(保持原逻辑)
|
||||
BufferedImage errorImage = new BufferedImage(Math.max(1, this.width), Math.max(1, this.height), BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = errorImage.createGraphics();
|
||||
g2d.setColor(Color.BLACK);
|
||||
g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight());
|
||||
g2d.setColor(Color.RED);
|
||||
g2d.drawString("像素读取失败", 10, 20);
|
||||
g2d.dispose();
|
||||
currentFrame = errorImage;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 GL 上下文任务队列
|
||||
*/
|
||||
private void processGLTasks() {
|
||||
Runnable task;
|
||||
while ((task = glTaskQueue.poll()) != null) {
|
||||
try {
|
||||
// 在渲染线程中执行,渲染线程已将上下文设为 current
|
||||
task.run();
|
||||
} catch (Exception e) {
|
||||
logger.error("执行 GL 任务时出错", e);
|
||||
@@ -354,12 +290,6 @@ public class GLContextManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新设置面板大小
|
||||
* <p>
|
||||
* 说明:当 Swing 面板被放大时,需要同时调整离屏 GLFW 窗口像素大小、GL 视口以及重分配像素读取缓冲,
|
||||
* 否则将把较小分辨率的图像拉伸到更大面板上导致模糊。
|
||||
*/
|
||||
public void resize(int newWidth, int newHeight) {
|
||||
executeInGLContext(() -> {
|
||||
if (contextInitialized && windowId != 0) {
|
||||
@@ -369,18 +299,10 @@ public class GLContextManager {
|
||||
GLFW.glfwSetWindowSize(windowId, this.width, this.height);
|
||||
RenderSystem.viewport(0, 0, this.width, this.height);
|
||||
ModelRender.setViewport(this.width, this.height);
|
||||
try {
|
||||
if (pixelBuffer != null) {
|
||||
MemoryUtil.memFree(pixelBuffer);
|
||||
pixelBuffer = null;
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
int pixelCount = Math.max(1, this.width * this.height);
|
||||
pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4);
|
||||
pixelBuffer.order(ByteOrder.nativeOrder());
|
||||
pixelInts = new int[pixelCount];
|
||||
argbInts = new int[pixelCount];
|
||||
currentFrame = null;
|
||||
|
||||
GL15.glDeleteBuffers(pboIds);
|
||||
initializeFrameResources();
|
||||
|
||||
} else {
|
||||
this.width = Math.max(1, newWidth);
|
||||
this.height = Math.max(1, newHeight);
|
||||
@@ -388,35 +310,21 @@ public class GLContextManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待渲染上下文准备就绪
|
||||
*/
|
||||
public CompletableFuture<Void> waitForContext() {
|
||||
return contextReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查渲染上下文是否已初始化
|
||||
* @return true 表示已初始化,false 表示未初始化
|
||||
*/
|
||||
public boolean isContextInitialized() {
|
||||
return contextInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否正在运行
|
||||
*/
|
||||
public boolean isRunning() {
|
||||
return running && contextInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
public void dispose() {
|
||||
running = false;
|
||||
cameraDragging = false;
|
||||
// 停止任务执行器
|
||||
taskExecutor.shutdown();
|
||||
|
||||
if (renderThread != null) {
|
||||
@@ -430,61 +338,39 @@ public class GLContextManager {
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
// 清理 ModelRender
|
||||
try {
|
||||
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("清理 ModelRender 时出错: {}", e.getMessage());
|
||||
logger.error("Error during GL resource cleanup", e);
|
||||
}
|
||||
|
||||
if (windowId != 0) {
|
||||
try {
|
||||
GLFW.glfwDestroyWindow(windowId);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
GLFW.glfwDestroyWindow(windowId);
|
||||
windowId = 0;
|
||||
}
|
||||
|
||||
// 释放像素缓冲
|
||||
try {
|
||||
if (pixelBuffer != null) {
|
||||
MemoryUtil.memFree(pixelBuffer);
|
||||
pixelBuffer = null;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.warn("释放 pixelBuffer 时出错: {}", t.getMessage());
|
||||
}
|
||||
|
||||
// 终止 GLFW(注意:如果应用中还有其他 GLFW 窗口,这里会影响它们)
|
||||
try {
|
||||
GLFW.glfwTerminate();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
GLFW.glfwTerminate();
|
||||
logger.info("OpenGL 资源已清理");
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 GL 上下文线程上异步执行任务
|
||||
*
|
||||
* @param task 要在 GL 上下文线程中执行的任务
|
||||
* @return CompletableFuture 用于获取任务执行结果
|
||||
*/
|
||||
public CompletableFuture<Void> executeInGLContext(Runnable task) {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
|
||||
if (!running) {
|
||||
future.completeExceptionally(new IllegalStateException("渲染线程已停止"));
|
||||
return future;
|
||||
}
|
||||
|
||||
// 等待上下文就绪后再提交任务
|
||||
contextReady.thenRun(() -> {
|
||||
try {
|
||||
// 使用 put 保证任务不会被丢弃,如果队列已满会阻塞调用者直到可入队
|
||||
glTaskQueue.put(() -> {
|
||||
try {
|
||||
task.run();
|
||||
@@ -497,30 +383,20 @@ public class GLContextManager {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 GL 上下文线程上异步执行任务并返回结果
|
||||
*
|
||||
* @param task 要在 GL 上下文线程中执行的有返回值的任务
|
||||
* @return CompletableFuture 用于获取任务执行结果
|
||||
*/
|
||||
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 {
|
||||
T result = task.call();
|
||||
future.complete(result);
|
||||
future.complete(task.call());
|
||||
} catch (Exception e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
@@ -532,43 +408,27 @@ public class GLContextManager {
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
future.completeExceptionally(new IllegalStateException("任务提交被中断", e));
|
||||
} catch (Exception e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步在 GL 上下文线程上执行任务(会阻塞当前线程直到任务完成)
|
||||
*
|
||||
* @param task 要在 GL 上下文线程中执行的任务
|
||||
* @throws Exception 如果任务执行出错
|
||||
*/
|
||||
public void executeInGLContextSync(Runnable task) throws Exception {
|
||||
public void executeInGLContextSync(Runnable task) {
|
||||
if (!running) {
|
||||
throw new IllegalStateException("渲染线程已停止");
|
||||
}
|
||||
|
||||
CompletableFuture<Void> future = executeInGLContext(task);
|
||||
future.get(10, TimeUnit.SECONDS); // 设置超时时间
|
||||
try {
|
||||
executeInGLContext(task).get(10, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("执行同步GL任务时出错", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步在 GL 上下文线程上执行任务并返回结果(会阻塞当前线程直到任务完成)
|
||||
*
|
||||
* @param task 要在 GL 上下文线程中执行的有返回值的任务
|
||||
* @return 任务执行结果
|
||||
* @throws Exception 如果任务执行出错或超时
|
||||
*/
|
||||
public <T> T executeInGLContextSync(Callable<T> task) throws Exception {
|
||||
if (!running) {
|
||||
throw new IllegalStateException("渲染线程已停止");
|
||||
}
|
||||
|
||||
CompletableFuture<T> future = executeInGLContext(task);
|
||||
return future.get(10, TimeUnit.SECONDS); // 设置超时时间
|
||||
return executeInGLContext(task).get(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void setDisplayScale(float scale) {
|
||||
@@ -587,11 +447,6 @@ public class GLContextManager {
|
||||
return targetScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态加载新的模型,在 GL 线程上执行文件 I/O 和模型初始化。
|
||||
* * @param newModelPath 新的模型文件路径。
|
||||
* @return 包含加载完成的模型对象的 CompletableFuture,可用于获取加载结果或处理错误。
|
||||
*/
|
||||
public CompletableFuture<Model2D> loadModel(String newModelPath) {
|
||||
return executeInGLContext(() -> {
|
||||
Model2D model;
|
||||
@@ -634,7 +489,6 @@ public class GLContextManager {
|
||||
if (repaintCallback != null) {
|
||||
SwingUtilities.invokeLater(repaintCallback::repaint);
|
||||
}
|
||||
return newModel;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -643,19 +497,11 @@ public class GLContextManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前帧
|
||||
* @return 当前帧
|
||||
* 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 currentFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上一帧
|
||||
* @return 上一帧
|
||||
*/
|
||||
public BufferedImage getLastFrame() {
|
||||
return lastFrame;
|
||||
return frontBuffer;
|
||||
}
|
||||
|
||||
public boolean isCameraDragging() {
|
||||
@@ -670,17 +516,11 @@ public class GLContextManager {
|
||||
return modelPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 GLContextManager 获取当前模型引用
|
||||
*/
|
||||
public Model2D getModel() {
|
||||
return modelRef.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待模型加载完成(若已经完成会立即返回已完成的 CompletableFuture)
|
||||
*/
|
||||
public CompletableFuture<Model2D> waitForModel() {
|
||||
return modelReady;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,13 +147,13 @@ public class ParametersManagement {
|
||||
return;
|
||||
}
|
||||
boolean isKeyframe = currentAnimParam.getKeyframes().contains(currentKeyframe);
|
||||
Integer newId = null;
|
||||
if (paramId.equals("secondaryVertex") && value instanceof Map) {
|
||||
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 Integer) {
|
||||
newId = (Integer) idObj;
|
||||
if (idObj instanceof String) {
|
||||
newId = (String) idObj;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < oldValues.size(); i++) {
|
||||
@@ -171,13 +171,13 @@ public class ParametersManagement {
|
||||
AnimationParameter recordAnimParam = newAnimationParameters.get(j);
|
||||
boolean animParamMatches = recordAnimParam != null && recordAnimParam.equals(currentAnimParam);
|
||||
boolean idMatches = true;
|
||||
if (paramIdMatches && paramId.equals("secondaryVertex")) {
|
||||
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");
|
||||
Integer oldId = (oldIdObj instanceof Integer) ? (Integer) oldIdObj : null;
|
||||
String oldId = (oldIdObj instanceof String) ? (String) oldIdObj : null;
|
||||
idMatches = Objects.equals(newId, oldId);
|
||||
} else {
|
||||
idMatches = false;
|
||||
|
||||
@@ -2,10 +2,12 @@ package com.chuangzhou.vivid2D.render.awt.tools;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.CameraManagement;
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Vertex;
|
||||
import org.joml.Vector2f;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -26,7 +28,7 @@ public class SelectionTool extends Tool {
|
||||
// 选择工具专用字段
|
||||
private volatile Mesh2D hoveredMesh = null;
|
||||
private final Set<Mesh2D> selectedMeshes = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
private volatile List<Call> callQueue = new LinkedList<>();
|
||||
private final List<Call> callQueue = new LinkedList<>();
|
||||
private volatile Mesh2D lastSelectedMesh = null;
|
||||
private volatile ModelPart draggedPart = null;
|
||||
private volatile float dragStartX, dragStartY;
|
||||
@@ -127,15 +129,15 @@ public class SelectionTool extends Tool {
|
||||
clearSelectedMeshes();
|
||||
}
|
||||
|
||||
public void addCall(Call call){
|
||||
public void addCall(Call call) {
|
||||
callQueue.add(call);
|
||||
}
|
||||
|
||||
public void removeCall(Call call){
|
||||
public void removeCall(Call call) {
|
||||
callQueue.remove(call);
|
||||
}
|
||||
|
||||
private void runCall(List<Mesh2D> meshes){
|
||||
private void runCall(List<Mesh2D> meshes) {
|
||||
for (Call call : callQueue) {
|
||||
call.call(meshes);
|
||||
}
|
||||
@@ -408,12 +410,10 @@ public class SelectionTool extends Tool {
|
||||
|
||||
@Override
|
||||
public void onMouseClicked(MouseEvent e, float modelX, float modelY) {
|
||||
// 选择工具的点击逻辑已在 onMousePressed 中处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) {
|
||||
// 选择工具的双击逻辑 - 进入液化模式
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
// 检测双击的网格
|
||||
@@ -449,6 +449,7 @@ public class SelectionTool extends Tool {
|
||||
float newY = startPos.y + deltaY;
|
||||
part.setPosition(newX, newY);
|
||||
renderPanel.getParametersManagement().broadcast(part, "position", List.of(newX, newY));
|
||||
updateMeshVertices();
|
||||
}
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
@@ -482,6 +483,7 @@ public class SelectionTool extends Tool {
|
||||
part.rotate(deltaAngle);
|
||||
}
|
||||
renderPanel.getParametersManagement().broadcast(part, "rotate", part.getRotation());
|
||||
updateMeshVertices();
|
||||
}
|
||||
rotationStartAngle = currentAngle;
|
||||
}
|
||||
@@ -503,6 +505,7 @@ public class SelectionTool extends Tool {
|
||||
float newPivotX = currentPivot.x + deltaX;
|
||||
float newPivotY = currentPivot.y + deltaY;
|
||||
renderPanel.getParametersManagement().broadcast(selectedPart, "pivot", List.of(newPivotX, newPivotY));
|
||||
updateMeshVertices();
|
||||
if (selectedPart.setPivot(newPivotX, newPivotY)) {
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
@@ -616,6 +619,7 @@ public class SelectionTool extends Tool {
|
||||
// 广播同步参数
|
||||
renderPanel.getParametersManagement().broadcast(part, "scale", List.of(newScaleX, newScaleY));
|
||||
renderPanel.getParametersManagement().broadcast(part, "position", List.of(newPosX, newPosY));
|
||||
updateMeshVertices();
|
||||
}
|
||||
|
||||
dragStartX = modelX;
|
||||
@@ -1243,6 +1247,47 @@ public class SelectionTool extends Tool {
|
||||
logger.info("选择工具请求进入液化模式: {}", targetMesh.getName());
|
||||
}
|
||||
|
||||
public void updateMeshVertices() {
|
||||
for (Mesh2D mesh : selectedMeshes) {
|
||||
updateMeshVertices(mesh);
|
||||
}
|
||||
updateMeshVertices(hoveredMesh);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void updateMeshVertices(Mesh2D mesh) {
|
||||
if (mesh == null){
|
||||
return;
|
||||
}
|
||||
ParametersManagement.Parameter param = renderPanel.getParametersManagement().getValue(mesh.getModelPart(), "meshVertices");
|
||||
if (param == null || param.value().isEmpty() || param.keyframe().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<Object> paramValue = param.value();
|
||||
for (Object keyframeDataObject : paramValue) {
|
||||
if (!(keyframeDataObject instanceof Map)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> keyframeData = (Map<String, Object>) keyframeDataObject;
|
||||
Object idObject = keyframeData.get("id");
|
||||
if (!(idObject instanceof String vertexIdInMap)) {
|
||||
continue;
|
||||
}
|
||||
for (Vertex vertex : mesh.getActiveVertexList()) {
|
||||
if (vertex == null || vertex.getName() == null) {
|
||||
continue;
|
||||
}
|
||||
if (vertexIdInMap.equals(vertex.getName())) {
|
||||
Map<String, Object> vertexUpdatePayload = Map.of(
|
||||
"id", vertex.getName(),
|
||||
"Vertex", new float[]{vertex.position.x, vertex.position.y}
|
||||
);
|
||||
renderPanel.getParametersManagement().broadcast(mesh.getModelPart(), "meshVertices", vertexUpdatePayload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取鼠标悬停的网格
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,9 @@ 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);
|
||||
@@ -25,11 +27,9 @@ public class VertexDeformationTool extends Tool {
|
||||
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; // 存储拖动开始时的顶点快照
|
||||
private List<Vertex> dragBaseState = null;
|
||||
|
||||
public VertexDeformationTool(ModelRenderPanel renderPanel) {
|
||||
super(renderPanel, "顶点变形工具", "直接对网格顶点进行精细变形操作");
|
||||
@@ -181,6 +181,13 @@ public class VertexDeformationTool extends Tool {
|
||||
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
Map<String, Object> parameters = Map.of("id", primaryVertex.getName(),
|
||||
"Vertex", new float[]{modelX, modelY});
|
||||
renderPanel.getParametersManagement().broadcast(
|
||||
targetMesh.getModelPart(),
|
||||
"meshVertices",
|
||||
parameters
|
||||
);
|
||||
primaryVertex.position.set(modelX, modelY);
|
||||
} catch (Throwable t) {
|
||||
logger.error("onMouseDragged (控制点模式) 处理失败", t);
|
||||
@@ -191,6 +198,9 @@ public class VertexDeformationTool extends Tool {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [已修正] onMouseReleased 现在会在固化变形后,向 ParametersManagement 广播消息。
|
||||
*/
|
||||
@Override
|
||||
public void onMouseReleased(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive) return;
|
||||
@@ -211,7 +221,7 @@ public class VertexDeformationTool extends Tool {
|
||||
if (targetMesh.getModelPart() != null) {
|
||||
targetMesh.getModelPart().updateMeshVertices();
|
||||
}
|
||||
} catch (Throwable t) { logger.error("onMouseReleased 保存基准失败", t); }
|
||||
} catch (Throwable t) { logger.error("onMouseReleased 保存基准或广播消息失败", t); }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,8 +229,6 @@ public class VertexDeformationTool extends Tool {
|
||||
renderPanel.repaint();
|
||||
}
|
||||
|
||||
// onMouseMoved, onMouseClicked, onMouseDoubleClicked, onKeyPressed, 等方法保持不变...
|
||||
// ... (此处省略所有其他未修改的方法,请保留您文件中的原样)
|
||||
@Override
|
||||
public void onMouseMoved(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive || targetMesh == null) return;
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.*;
|
||||
import com.chuangzhou.vivid2D.render.model.*;
|
||||
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.model.util.VertexTag;
|
||||
import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Vector2f;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* [MODIFIED] 关键帧插值器
|
||||
* 已更新为直接操作 Mesh2D 的一级顶点 (Vertex),通过 "deformationVertex" 参数
|
||||
* 来控制带有 VertexTag.DEFORMATION 标签的顶点。
|
||||
* [已修复] 关键帧插值器
|
||||
* 1. 优先处理 "meshVertices" 参数,对整个网格状态进行插值。
|
||||
* 2. 如果 "meshVertices" 不存在,则回退到处理独立的 "deformationVertex" 参数。
|
||||
* 3. 增加了在应用 "deformationVertex" 前的重置逻辑,防止顶点卡住。
|
||||
* 4. 修正了顶点 "删除" (取消变形) 的逻辑。
|
||||
*/
|
||||
public class FrameInterpolator {
|
||||
private FrameInterpolator() {}
|
||||
@@ -37,11 +38,21 @@ public class FrameInterpolator {
|
||||
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 {
|
||||
Object[] arr = (Object[]) o;
|
||||
if (arr.length > 0) out[0] = toFloat(arr[0]);
|
||||
if (arr.length > 1) out[1] = toFloat(arr[1]);
|
||||
// 处理 float[] 的情况
|
||||
if (o instanceof float[]) {
|
||||
float[] arr = (float[]) o;
|
||||
if (arr.length > 0) out[0] = arr[0];
|
||||
if (arr.length > 1) out[1] = arr[1];
|
||||
} else {
|
||||
Object[] arr = (Object[]) o;
|
||||
if (arr.length > 0) out[0] = toFloat(arr[0]);
|
||||
if (arr.length > 1) out[1] = toFloat(arr[1]);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} else if (o instanceof Number || o instanceof String) {
|
||||
float v = toFloat(o);
|
||||
@@ -57,7 +68,8 @@ public class FrameInterpolator {
|
||||
}
|
||||
|
||||
private static float normalizeAnimAngleUnits(float a) {
|
||||
if (Math.abs(a) > Math.PI * 2.1f) { // 增加一点容差
|
||||
// 假设大于 2*PI 的值是以角度为单位的
|
||||
if (Math.abs(a) > Math.PI * 2.1f) {
|
||||
return (float) Math.toRadians(a);
|
||||
}
|
||||
return a;
|
||||
@@ -79,6 +91,36 @@ public class FrameInterpolator {
|
||||
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;
|
||||
@@ -152,9 +194,10 @@ public class FrameInterpolator {
|
||||
if (prevIndex == nextIndex) {
|
||||
target = toFloat(values.get(prevIndex));
|
||||
} else {
|
||||
float p = toFloat(values.get(prevIndex));
|
||||
float q = toFloat(values.get(nextIndex));
|
||||
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) {
|
||||
@@ -164,180 +207,134 @@ public class FrameInterpolator {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
out[0] = normalizeAnimAngleUnits(target);
|
||||
out[0] = target;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- [NEW] Deformation Vertex 插值 ----
|
||||
|
||||
// [MODIFIED] 辅助结构,现在使用顶点索引(idx)替代ID
|
||||
private static class DeformationVertexTarget {
|
||||
int idx;
|
||||
float x;
|
||||
float y;
|
||||
boolean deleted = false; // pos == null 表示应取消变形标记
|
||||
}
|
||||
|
||||
// [MODIFIED] 解析新的变形顶点数据格式 { "idx": int, "pos": [x, y] }
|
||||
private static DeformationVertexTarget parseDeformationVertexValue(Object v) {
|
||||
if (v instanceof Map) {
|
||||
Map<?,?> m = (Map<?,?>) v;
|
||||
Object idxObj = m.get("idx");
|
||||
Object posObj = m.get("pos");
|
||||
if (idxObj instanceof Number) {
|
||||
int idx = ((Number) idxObj).intValue();
|
||||
DeformationVertexTarget target = new DeformationVertexTarget();
|
||||
target.idx = idx;
|
||||
if (posObj == null) {
|
||||
target.deleted = true;
|
||||
} else {
|
||||
float[] p = readVec2(posObj);
|
||||
target.x = p[0];
|
||||
target.y = p[1];
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// [MODIFIED] 计算所有变形顶点的目标状态
|
||||
private static List<DeformationVertexTarget> computeAllDeformationVertexTargets(ParametersManagement.Parameter fullParam, String paramId, float current, AnimationParameter animParam) {
|
||||
List<DeformationVertexTarget> results = new ArrayList<>();
|
||||
List<Integer> allIndicesForParam = findIndicesForParam(fullParam, paramId, animParam);
|
||||
if (allIndicesForParam.isEmpty()) return results;
|
||||
|
||||
Map<Integer, List<Integer>> vertexIdxToParamIndices = new HashMap<>();
|
||||
for (int paramIdx : allIndicesForParam) {
|
||||
DeformationVertexTarget parsed = parseDeformationVertexValue(fullParam.value().get(paramIdx));
|
||||
if (parsed != null) {
|
||||
vertexIdxToParamIndices.computeIfAbsent(parsed.idx, k -> new ArrayList<>()).add(paramIdx);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<Integer, List<Integer>> entry : vertexIdxToParamIndices.entrySet()) {
|
||||
int vertexIdx = entry.getKey();
|
||||
List<Integer> paramIndices = entry.getValue();
|
||||
|
||||
int[] surrounding = findSurroundingKeyframesForIndices(fullParam.keyframe(), paramIndices, current);
|
||||
int prevParamIdx = surrounding[0], nextParamIdx = surrounding[1];
|
||||
|
||||
if (prevParamIdx != -1 && nextParamIdx != -1) {
|
||||
DeformationVertexTarget prevTarget = parseDeformationVertexValue(fullParam.value().get(prevParamIdx));
|
||||
DeformationVertexTarget nextTarget = parseDeformationVertexValue(fullParam.value().get(nextParamIdx));
|
||||
|
||||
if (prevTarget == null || nextTarget == null) continue;
|
||||
|
||||
if (prevParamIdx == nextParamIdx) {
|
||||
results.add(prevTarget);
|
||||
} else {
|
||||
// 如果任意一侧被删除,则不进行插值,而是取前一个关键帧的状态
|
||||
if (prevTarget.deleted || nextTarget.deleted) {
|
||||
results.add(prevTarget);
|
||||
} else {
|
||||
float t = computeT(fullParam.keyframe().get(prevParamIdx), fullParam.keyframe().get(nextParamIdx), current);
|
||||
DeformationVertexTarget interpolated = new DeformationVertexTarget();
|
||||
interpolated.idx = vertexIdx;
|
||||
interpolated.x = prevTarget.x + t * (nextTarget.x - prevTarget.x);
|
||||
interpolated.y = prevTarget.y + t * (nextTarget.y - prevTarget.y);
|
||||
results.add(interpolated);
|
||||
/**
|
||||
* [新增] 计算所有变形顶点的目标状态。
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else if (prevParamIdx != -1) {
|
||||
DeformationVertexTarget target = parseDeformationVertexValue(fullParam.value().get(prevParamIdx));
|
||||
if (target != null) results.add(target);
|
||||
} else if (nextParamIdx != -1) {
|
||||
DeformationVertexTarget target = parseDeformationVertexValue(fullParam.value().get(nextParamIdx));
|
||||
if (target != null) results.add(target);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
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) return;
|
||||
if (pm == null || parts == null || parts.isEmpty() || currentAnimationParameter == null || pm.getParametersPanel().getSelectParameter() == null) return;
|
||||
|
||||
float current = toFloat(currentAnimationParameter.getValue());
|
||||
|
||||
for (ModelPart part : parts) {
|
||||
if (!pm.getParametersPanel().getSelectParameter().equals(currentAnimationParameter)) continue;
|
||||
if (!Objects.equals(pm.getParametersPanel().getSelectParameter().getId(), currentAnimationParameter.getId())) continue;
|
||||
|
||||
ParametersManagement.Parameter fullParam = pm.getModelPartParameters(part);
|
||||
if (fullParam == null) continue;
|
||||
|
||||
float[] targetPivot = null, targetScale = null, targetPosition = null;
|
||||
Float targetRotation = null;
|
||||
// [MODIFIED] 使用新的类型和名称
|
||||
List<DeformationVertexTarget> dvTargets = 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];
|
||||
|
||||
// [MODIFIED] 计算可变形顶点的目标
|
||||
dvTargets = computeAllDeformationVertexTargets(fullParam, "deformationVertex", current, currentAnimationParameter);
|
||||
|
||||
if (targetPivot == null && targetScale == null && targetRotation == null && targetPosition == null && (dvTargets == null || dvTargets.isEmpty())) {
|
||||
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) {
|
||||
try {
|
||||
part.getClass().getMethod("setRotation", float.class).invoke(part, targetRotation);
|
||||
} catch (NoSuchMethodException e) {
|
||||
part.rotate(normalizeAngle(targetRotation - part.getRotation()));
|
||||
}
|
||||
}
|
||||
|
||||
// [MODIFIED] 应用变形顶点更改
|
||||
if (dvTargets != null && !dvTargets.isEmpty()) {
|
||||
for (Mesh2D mesh : part.getMeshes()) {
|
||||
if (mesh == null) continue;
|
||||
boolean meshModified = false;
|
||||
for (DeformationVertexTarget target : dvTargets) {
|
||||
if (target.idx >= 0 && target.idx < mesh.getVertexCount()) {
|
||||
Vertex vertex = mesh.getVertexInstance(target.idx);
|
||||
if (target.deleted) {
|
||||
// "删除"操作意味着取消其变形资格
|
||||
if (vertex.getTag() == VertexTag.DEFORMATION) {
|
||||
vertex.setTag(VertexTag.DEFORMATION);
|
||||
meshModified = true;
|
||||
}
|
||||
} else {
|
||||
// 更新位置并确保其为可变形顶点
|
||||
vertex.position.set(target.x, target.y);
|
||||
if (vertex.getTag() != VertexTag.DEFORMATION) {
|
||||
vertex.setTag(VertexTag.DEFORMATION);
|
||||
}
|
||||
meshModified = true;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (meshModified) {
|
||||
// 如果顶点数据被修改,需要标记mesh为dirty
|
||||
mesh.markDirty();
|
||||
}
|
||||
part.updateMeshVertices();
|
||||
}
|
||||
}
|
||||
|
||||
// 统一刷新
|
||||
part.updateMeshVertices();
|
||||
for (Mesh2D mesh : part.getMeshes()) {
|
||||
if (mesh != null) mesh.updateBounds();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("在对部件 '{}' 应用插值时发生异常", part.getName(), e);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.chuangzhou.vivid2D.render.model;
|
||||
|
||||
import com.chuangzhou.vivid2D.events.GlobalEventBus;
|
||||
import com.chuangzhou.vivid2D.events.render.Mesh2DRender;
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.MultiSelectionBoxRenderer;
|
||||
import com.chuangzhou.vivid2D.render.TextRenderer;
|
||||
@@ -65,8 +67,6 @@ public class Mesh2D {
|
||||
// [NEW] ==================== 变形引擎字段 ====================
|
||||
private final List<Vertex> deformationControlVertices = new ArrayList<>();
|
||||
private final List<Vertex> deformationControlCage = new ArrayList<>();
|
||||
private final Map<Vertex, BarycentricWeightInfo> deformationMap = new HashMap<>();
|
||||
private boolean deformationMappingsDirty = true;
|
||||
|
||||
// ==================== 多选支持 ====================
|
||||
private final List<Mesh2D> multiSelectedParts = new ArrayList<>();
|
||||
@@ -114,96 +114,6 @@ public class Mesh2D {
|
||||
|
||||
// ==================== 变形引擎方法 ====================
|
||||
|
||||
/**
|
||||
* [新增] 核心方法:重新计算所有 DEFAULT 顶点与 DEFORMATION 控制点之间的重心坐标映射。
|
||||
* 这是一个消耗较大的操作,只应在控制点结构发生变化时调用。
|
||||
*/
|
||||
private void recalculateDeformationMappings() {
|
||||
deformationMap.clear();
|
||||
if (deformationControlVertices.isEmpty() || deformationControlVertices.size() < 3) {
|
||||
// 没有足够的控制点形成“骨架”,无法进行映射
|
||||
deformationMappingsDirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 步骤 1: 对所有 DEFORMATION 控制点进行三角剖分,形成控制网格
|
||||
// 我们使用 originalPosition 进行三角剖分,因为这是最稳定的拓扑结构
|
||||
List<Triangle> controlTriangles = Delaunay.triangulate(deformationControlVertices);
|
||||
|
||||
if (controlTriangles.isEmpty()) {
|
||||
deformationMappingsDirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 步骤 2: 为每一个 DEFAULT 顶点找到它所在的控制三角形,并计算重心坐标
|
||||
for (Vertex vertexToMap : activeVertexList) {
|
||||
if (vertexToMap.getTag() == VertexTag.DEFORMATION) {
|
||||
continue; // 控制点不需要被映射
|
||||
}
|
||||
|
||||
for (Triangle controlTriangle : controlTriangles) {
|
||||
// 使用顶点的 originalPosition 来确定其在哪个控制三角形内
|
||||
if (isPointInTriangle(vertexToMap.originalPosition,
|
||||
controlTriangle.p1.originalPosition,
|
||||
controlTriangle.p2.originalPosition,
|
||||
controlTriangle.p3.originalPosition)) {
|
||||
|
||||
Vector2f barycentricCoords = barycentric(vertexToMap.originalPosition,
|
||||
controlTriangle.p1.originalPosition,
|
||||
controlTriangle.p2.originalPosition,
|
||||
controlTriangle.p3.originalPosition);
|
||||
float w = barycentricCoords.x;
|
||||
float v = barycentricCoords.y;
|
||||
float u = 1.0f - w - v;
|
||||
|
||||
// 保存这个“基因”
|
||||
deformationMap.put(vertexToMap, new BarycentricWeightInfo(
|
||||
controlTriangle.p1, controlTriangle.p2, controlTriangle.p3, u, w, v // u,w,v -> p1,p2,p3
|
||||
));
|
||||
break; // 找到后即可处理下一个顶点
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deformationMappingsDirty = false;
|
||||
logger.info("Deformation mappings recalculated. Mapped {} vertices.", deformationMap.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* [新增] 核心方法:根据 DEFORMATION 控制点的当前位置,更新所有受其影响的 DEFAULT 顶点的位置。
|
||||
* 这是一个快速的操作,可以在每帧或每次控制点移动后调用。
|
||||
*/
|
||||
public void applyDeformation() {
|
||||
if (deformationMappingsDirty) {
|
||||
recalculateDeformationMappings();
|
||||
}
|
||||
|
||||
if (deformationMap.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 遍历所有已建立映射关系的 DEFAULT 顶点
|
||||
for (Map.Entry<Vertex, BarycentricWeightInfo> entry : deformationMap.entrySet()) {
|
||||
Vertex vertexToUpdate = entry.getKey();
|
||||
BarycentricWeightInfo weights = entry.getValue();
|
||||
|
||||
// 获取其控制点的【当前】世界坐标
|
||||
Vector2f c1_pos = weights.controlVertex1.position;
|
||||
Vector2f c2_pos = weights.controlVertex2.position;
|
||||
Vector2f c3_pos = weights.controlVertex3.position;
|
||||
|
||||
// 根据“基因”(重心坐标)和控制点的当前位置,计算出该顶点的新位置
|
||||
float newX = c1_pos.x * weights.weight1 + c2_pos.x * weights.weight2 + c3_pos.x * weights.weight3;
|
||||
float newY = c1_pos.y * weights.weight1 + c2_pos.y * weights.weight2 + c3_pos.y * weights.weight3;
|
||||
|
||||
vertexToUpdate.position.set(newX, newY);
|
||||
activeVertexList.set(vertexToUpdate.index, vertexToUpdate);
|
||||
}
|
||||
|
||||
// 因为顶点位置已变,标记网格需要重新上传到GPU
|
||||
markDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定的 (x, y) 坐标处,直接创建一个新的控制点。
|
||||
*
|
||||
@@ -232,31 +142,25 @@ public class Mesh2D {
|
||||
}
|
||||
|
||||
// --- [核心修正] 步骤 2: 在正确的、统一的坐标系下计算所有属性 ---
|
||||
|
||||
// [关键] 使用【当前世界位置】(position) 来计算重心坐标,确保与 findTriangleContainingPoint 的逻辑一致
|
||||
Vector2f barycentricCoords = barycentric(worldPoint,
|
||||
containingTriangle.v1.position,
|
||||
containingTriangle.v2.position,
|
||||
containingTriangle.v3.position);
|
||||
|
||||
// 计算重心坐标权重 u, v, w
|
||||
float w = barycentricCoords.x;
|
||||
float v = barycentricCoords.y;
|
||||
float u = 1.0f - w - v;
|
||||
|
||||
// 检查重心坐标是否有效,防止因浮点误差导致的崩溃
|
||||
if (u < -1e-6f || v < -1e-6f || w < -1e-6f) {
|
||||
logger.warn("计算出的重心坐标无效,添加顶点失败。 u={}, v={}, w={}", u, v, w);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 使用正确的权重插值 UV
|
||||
Vector2f newUv = new Vector2f(0, 0);
|
||||
newUv.add(new Vector2f(containingTriangle.v1.uv).mul(u));
|
||||
newUv.add(new Vector2f(containingTriangle.v2.uv).mul(w));
|
||||
newUv.add(new Vector2f(containingTriangle.v3.uv).mul(v));
|
||||
|
||||
// [关键] 同时,我们也必须使用同样的重心坐标来插值【局部原始位置】,以获得新顶点的正确局部坐标
|
||||
Vector2f localPoint = new Vector2f(0, 0);
|
||||
localPoint.add(new Vector2f(containingTriangle.v1.originalPosition).mul(u));
|
||||
localPoint.add(new Vector2f(containingTriangle.v2.originalPosition).mul(w));
|
||||
@@ -266,13 +170,15 @@ public class Mesh2D {
|
||||
Vertex newVertex = new Vertex(worldPoint, newUv, VertexTag.DEFORMATION);
|
||||
newVertex.originalPosition.set(localPoint);
|
||||
newVertex.setName(String.valueOf(UUID.randomUUID()));
|
||||
// --- [后续所有逻辑都保持不变] ---
|
||||
|
||||
// 添加到 active 列表
|
||||
activeVertexList.add(newVertex);
|
||||
final int newVertexIndex = activeVertexList.size() - 1;
|
||||
|
||||
// 重建索引(把所在三角形拆成三个小三角形)
|
||||
List<Integer> newIndices = new ArrayList<>();
|
||||
for (int i = 0; i < this.activeVertexList.getIndices().length; i += 3) {
|
||||
int i1 = this.activeVertexList.getIndices()[i], i2 = this.activeVertexList.getIndices()[i+1], i3 = this.activeVertexList.getIndices()[i+2];
|
||||
int i1 = this.activeVertexList.getIndices()[i], i2 = this.activeVertexList.getIndices()[i + 1], i3 = this.activeVertexList.getIndices()[i + 2];
|
||||
if (i1 == containingTriangle.i1 && i2 == containingTriangle.i2 && i3 == containingTriangle.i3) {
|
||||
newIndices.add(i1); newIndices.add(i2); newIndices.add(newVertexIndex);
|
||||
newIndices.add(i2); newIndices.add(i3); newIndices.add(newVertexIndex);
|
||||
@@ -283,29 +189,108 @@ public class Mesh2D {
|
||||
}
|
||||
this.activeVertexList.setIndices(newIndices.stream().mapToInt(Integer::intValue).toArray());
|
||||
|
||||
// --- 动态分配三角形区域(改进) ---
|
||||
// 规则:三角形分配到离质心最近的 DEFORMATION 顶点;
|
||||
// 但如果该控制顶点是当前三角形的 apex(APEX),且质心位于 AB 底边的“另一侧”,则禁止该 apex 成为 owner。
|
||||
// 使用基于 AB 直线的“同侧测试”(叉乘符号)来判断质心是否与 apex 在同一侧,避免受屏幕坐标系方向影响。
|
||||
|
||||
int[] indicesArr = activeVertexList.getIndices();
|
||||
List<Vertex> vertices = activeVertexList.getVertices();
|
||||
List<Vertex> deformationVertices = this.deformationControlVertices; // 已包含新的控制点
|
||||
|
||||
// 初始化每个 deformation 顶点的控制三角形列表
|
||||
for (Vertex dv : deformationVertices) {
|
||||
dv.setControlledTriangles(new ArrayList<>());
|
||||
}
|
||||
|
||||
final float EPS = 1e-6f;
|
||||
int triCount = indicesArr.length / 3;
|
||||
for (int t = 0; t < triCount; t++) {
|
||||
int idx0 = indicesArr[t * 3];
|
||||
int idx1 = indicesArr[t * 3 + 1];
|
||||
int idx2 = indicesArr[t * 3 + 2];
|
||||
|
||||
Vector2f p0 = vertices.get(idx0).originalPosition;
|
||||
Vector2f p1 = vertices.get(idx1).originalPosition;
|
||||
Vector2f p2 = vertices.get(idx2).originalPosition;
|
||||
|
||||
// 质心(使用原始位置)
|
||||
Vector2f centroid = new Vector2f(
|
||||
(p0.x + p1.x + p2.x) / 3.0f,
|
||||
(p0.y + p1.y + p2.y) / 3.0f
|
||||
);
|
||||
|
||||
// 找出 apex(y 最大的顶点)及 base 两点(用索引表示)
|
||||
int apexIndexLocal = idx0;
|
||||
Vector2f apexP = p0;
|
||||
int baseIdxA = idx1, baseIdxB = idx2;
|
||||
Vector2f baseA = p1, baseB = p2;
|
||||
|
||||
if (p1.y > apexP.y) { apexIndexLocal = idx1; apexP = p1; baseIdxA = idx0; baseA = p0; baseIdxB = idx2; baseB = p2; }
|
||||
if (p2.y > apexP.y) { apexIndexLocal = idx2; apexP = p2; baseIdxA = idx0; baseA = p0; baseIdxB = idx1; baseB = p1; }
|
||||
|
||||
// 计算 AB 向量和叉积符号函数(用于同侧测试)
|
||||
float abx = baseB.x - baseA.x;
|
||||
float aby = baseB.y - baseA.y;
|
||||
|
||||
// cross = (B-A) x (P-A) = abx*(py - baseA.y) - aby*(px - baseA.x)
|
||||
float crossApex = abx * (apexP.y - baseA.y) - aby * (apexP.x - baseA.x);
|
||||
float crossCentroid = abx * (centroid.y - baseA.y) - aby * (centroid.x - baseA.x);
|
||||
|
||||
boolean apexForbidden;
|
||||
if (Math.abs(crossApex) < EPS) {
|
||||
// apex 在 AB 线上,保守处理为不禁止(因为没有明确“对侧”)
|
||||
apexForbidden = false;
|
||||
} else {
|
||||
// 若叉积符号不同则表示质心和 apex 在 AB 的两侧 -> 禁止该 apex 控制此三角形
|
||||
apexForbidden = (crossApex * crossCentroid) < 0.0f;
|
||||
}
|
||||
|
||||
// 在 deformationVertices 中选择最近的顶点(平方距离比较),若最近是 apex 且被禁止则取下一个最近
|
||||
Vertex chosen = null;
|
||||
float bestDist = Float.MAX_VALUE;
|
||||
Vertex second = null;
|
||||
float secondDist = Float.MAX_VALUE;
|
||||
|
||||
for (Vertex dv : deformationVertices) {
|
||||
Vector2f dvPos = dv.originalPosition;
|
||||
float dx = dvPos.x - centroid.x;
|
||||
float dy = dvPos.y - centroid.y;
|
||||
float dist2 = dx * dx + dy * dy;
|
||||
|
||||
if (dist2 < bestDist) {
|
||||
second = chosen; secondDist = bestDist;
|
||||
chosen = dv; bestDist = dist2;
|
||||
} else if (dist2 < secondDist) {
|
||||
second = dv; secondDist = dist2;
|
||||
}
|
||||
}
|
||||
|
||||
Vertex owner = chosen;
|
||||
// 如果最近的是三角形的 apex 且禁止,则尝试用 second;如果 second 为 null 或仍不合适则保留 chosen(降级处理)
|
||||
if (owner != null && owner.getIndex() == apexIndexLocal && apexForbidden) {
|
||||
if (second != null && second.getIndex() != apexIndexLocal) {
|
||||
owner = second;
|
||||
}
|
||||
}
|
||||
|
||||
// 最终将该三角形 t 分配给 owner
|
||||
if (owner != null) {
|
||||
List<Integer> list = owner.getControlledTriangles();
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
owner.setControlledTriangles(list);
|
||||
}
|
||||
list.add(t); // 使用三角形全局索引 t
|
||||
}
|
||||
}
|
||||
List<Vertex> allPoints = new ArrayList<>(this.deformationControlVertices);
|
||||
allPoints.add(newVertex);
|
||||
setDeformationControlVertices(allPoints);
|
||||
applyDeformation();
|
||||
// 返回新创建的控制顶点
|
||||
return newVertex;
|
||||
}
|
||||
|
||||
private static class BarycentricWeightInfo {
|
||||
// 构成包含此顶点的“控制三角形”的三个控制点
|
||||
Vertex controlVertex1, controlVertex2, controlVertex3;
|
||||
// 此顶点相对于这三个控制点的重心坐标 (u, v, w)
|
||||
float weight1, weight2, weight3; // Corresponds to w, v, u in barycentric calculation
|
||||
|
||||
BarycentricWeightInfo(Vertex c1, Vertex c2, Vertex c3, float w1, float w2, float w3) {
|
||||
this.controlVertex1 = c1;
|
||||
this.controlVertex2 = c2;
|
||||
this.controlVertex3 = c3;
|
||||
this.weight1 = w1; // Weight for controlVertex1
|
||||
this.weight2 = w2; // Weight for controlVertex2
|
||||
this.weight3 = w3; // Weight for controlVertex3
|
||||
}
|
||||
}
|
||||
|
||||
// [辅助内部类] 用于方便地传递找到的三角形信息
|
||||
private static class TriangleInfo {
|
||||
int i1, i2, i3;
|
||||
@@ -375,8 +360,6 @@ public class Mesh2D {
|
||||
if (controlVertices != null && !controlVertices.isEmpty()) {
|
||||
this.deformationControlVertices.addAll(controlVertices);
|
||||
}
|
||||
|
||||
applyDeformation();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1299,11 +1282,12 @@ public class Mesh2D {
|
||||
* 获取索引缓冲区数据
|
||||
*/
|
||||
public IntBuffer getIndexBuffer(IntBuffer buffer) {
|
||||
if (buffer == null || buffer.capacity() < activeVertexList.getIndices().length) {
|
||||
throw new IllegalArgumentException("Buffer is null or too small");
|
||||
int idxCount = (activeVertexList != null && activeVertexList.getIndices() != null) ? activeVertexList.getIndices().length : 0;
|
||||
if (buffer == null || buffer.capacity() < idxCount) {
|
||||
throw new IllegalArgumentException("Buffer is null or too小/capacity不足");
|
||||
}
|
||||
buffer.clear();
|
||||
buffer.put(activeVertexList.getIndices());
|
||||
if (idxCount > 0) buffer.put(activeVertexList.getIndices());
|
||||
buffer.flip();
|
||||
return buffer;
|
||||
}
|
||||
@@ -1330,6 +1314,87 @@ public class Mesh2D {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public boolean moveControlVertex(Vertex control, Vector2f targetPos) {
|
||||
if (control == null) return false;
|
||||
if (activeVertexList == null) return false;
|
||||
|
||||
int[] indicesArr = activeVertexList.getIndices();
|
||||
List<Vertex> vertices = activeVertexList.getVertices();
|
||||
List<Integer> controlled = control.getControlledTriangles();
|
||||
if (controlled == null || controlled.isEmpty()) {
|
||||
// 没有控制的三角形,直接移动
|
||||
control.position.set(targetPos);
|
||||
control.originalPosition.set(targetPos);
|
||||
return true;
|
||||
}
|
||||
|
||||
final float EPS = 1e-6f;
|
||||
// 先模拟检查所有被该控制点控制的三角形,任何一个三角形违反规则都拒绝移动
|
||||
for (Integer t : controlled) {
|
||||
if (t == null || t < 0) continue;
|
||||
int triIdx = t * 3;
|
||||
if (triIdx + 2 >= indicesArr.length) continue;
|
||||
int idx0 = indicesArr[triIdx];
|
||||
int idx1 = indicesArr[triIdx + 1];
|
||||
int idx2 = indicesArr[triIdx + 2];
|
||||
|
||||
// 找到这一三角形的三个点(使用 originalPosition 进行判断)
|
||||
Vertex va = vertices.get(idx0);
|
||||
Vertex vb = vertices.get(idx1);
|
||||
Vertex vc = vertices.get(idx2);
|
||||
|
||||
// 确定当前三角形哪个是 apex(y 最大)
|
||||
int apexIndexLocal = idx0;
|
||||
Vector2f apexP = va.originalPosition;
|
||||
Vector2f baseA = vb.originalPosition, baseB = vc.originalPosition;
|
||||
int baseIdxA = idx1, baseIdxB = idx2;
|
||||
|
||||
if (vb.originalPosition.y > apexP.y) {
|
||||
apexIndexLocal = idx1; apexP = vb.originalPosition;
|
||||
baseA = va.originalPosition; baseIdxA = idx0;
|
||||
baseB = vc.originalPosition; baseIdxB = idx2;
|
||||
}
|
||||
if (vc.originalPosition.y > apexP.y) {
|
||||
apexIndexLocal = idx2; apexP = vc.originalPosition;
|
||||
baseA = va.originalPosition; baseIdxA = idx0;
|
||||
baseB = vb.originalPosition; baseIdxB = idx1;
|
||||
}
|
||||
|
||||
// 只有当 control 是这个三角形的 apex 时才应用 "不允许移动到 AB 以下" 的约束
|
||||
if (control.getIndex() != apexIndexLocal) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 模拟将 apex 移动到 targetPos 后的质心
|
||||
Vector2f centroidAfter = new Vector2f(
|
||||
(targetPos.x + baseA.x + baseB.x) / 3.0f,
|
||||
(targetPos.y + baseA.y + baseB.y) / 3.0f
|
||||
);
|
||||
|
||||
float baseMinY = Math.min(baseA.y, baseB.y);
|
||||
// 如果质心低于底边的最小 y,则违反规则(即移动使得质心进入 AB 以下区域)
|
||||
if (centroidAfter.y < baseMinY - EPS) {
|
||||
// 被约束,拒绝整个移动
|
||||
return false;
|
||||
}
|
||||
|
||||
// 进一步增强:使用 AB 的同侧检测(可选冗余检查)
|
||||
float abx = baseB.x - baseA.x;
|
||||
float aby = baseB.y - baseA.y;
|
||||
float crossApex = abx * (apexP.y - baseA.y) - aby * (apexP.x - baseA.x);
|
||||
float crossCentroidAfter = abx * (centroidAfter.y - baseA.y) - aby * (centroidAfter.x - baseA.x);
|
||||
if (Math.abs(crossApex) >= EPS && (crossApex * crossCentroidAfter) < 0.0f) {
|
||||
// centroidAfter 与 apex 在 AB 两侧,视为违规
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有受控三角形都通过约束检查,执行移动并更新 originalPosition(或只更新 position,取决于你的设计)
|
||||
control.position.set(targetPos);
|
||||
control.originalPosition.set(targetPos);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== 状态管理 ====================
|
||||
/**
|
||||
* 标记数据已修改
|
||||
@@ -1418,6 +1483,10 @@ public class Mesh2D {
|
||||
* 绘制网格(会在第一次绘制时自动上传到 GPU)
|
||||
*/
|
||||
public void draw(int shaderProgram, Matrix3f modelMatrix) {
|
||||
if (GlobalEventBus.EVENT_BUS.post(new Mesh2DRender.Start(this, shaderProgram, modelMatrix)).isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!visible) return;
|
||||
if (activeVertexList.getIndices() == null || activeVertexList.getIndices().length == 0) return;
|
||||
if (dirty) {
|
||||
@@ -1547,6 +1616,7 @@ public class Mesh2D {
|
||||
|
||||
RenderSystem.popState();
|
||||
}
|
||||
GlobalEventBus.EVENT_BUS.post(new Mesh2DRender.End(this, shaderProgram, modelMatrix));
|
||||
}
|
||||
|
||||
public void setSolidShader(Matrix3f modelMatrix) {
|
||||
@@ -2040,11 +2110,18 @@ public class Mesh2D {
|
||||
if (removedIndex == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 备份旧索引和顶点快照(在移除顶点前获取)
|
||||
int[] oldIndices = activeVertexList.getIndices() != null ? activeVertexList.getIndices().clone() : new int[0];
|
||||
List<Vertex> oldVerticesSnapshot = activeVertexList.getVertices(); // 位置等信息用它来计算
|
||||
Vector2f removedPos = oldVerticesSnapshot.get(removedIndex).originalPosition;
|
||||
|
||||
// 收集与被移除顶点相连的邻居(无序集合去重)
|
||||
Set<Integer> neighborIndices = new LinkedHashSet<>();
|
||||
for (int i = 0; i < activeVertexList.getIndices().length; i += 3) {
|
||||
int i1 = activeVertexList.getIndices()[i];
|
||||
int i2 = activeVertexList.getIndices()[i + 1];
|
||||
int i3 = activeVertexList.getIndices()[i + 2];
|
||||
for (int i = 0; i < oldIndices.length; i += 3) {
|
||||
int i1 = oldIndices[i];
|
||||
int i2 = oldIndices[i + 1];
|
||||
int i3 = oldIndices[i + 2];
|
||||
|
||||
if (i1 == removedIndex) {
|
||||
neighborIndices.add(i2);
|
||||
@@ -2057,16 +2134,35 @@ public class Mesh2D {
|
||||
neighborIndices.add(i2);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果邻居少于3,不存在需要填补的多边形,直接移除并重映射索引即可
|
||||
List<Integer> sortedNeighbors = new ArrayList<>(neighborIndices);
|
||||
// 若邻居>=3,按角度排序以重建有序环(相对于被移除顶点)
|
||||
if (sortedNeighbors.size() >= 3) {
|
||||
sortedNeighbors.sort((a, b) -> {
|
||||
Vector2f pa = oldVerticesSnapshot.get(a).originalPosition;
|
||||
Vector2f pb = oldVerticesSnapshot.get(b).originalPosition;
|
||||
double angA = Math.atan2(pa.y - removedPos.y, pa.x - removedPos.x);
|
||||
double angB = Math.atan2(pb.y - removedPos.y, pb.x - removedPos.x);
|
||||
return Double.compare(angA, angB);
|
||||
});
|
||||
}
|
||||
|
||||
// 执行真正的顶点移除(会调整 activeVertexList.vertices)
|
||||
activeVertexList.remove(removedIndex);
|
||||
|
||||
// 重建索引:先把原索引中不包含 removedIndex 的三角形添加进来,并对大于 removedIndex 的索引 -1
|
||||
List<Integer> newIndicesList = new ArrayList<>();
|
||||
for (int i = 0; i < activeVertexList.getIndices().length; i += 3) {
|
||||
int i1 = activeVertexList.getIndices()[i];
|
||||
int i2 = activeVertexList.getIndices()[i + 1];
|
||||
int i3 = activeVertexList.getIndices()[i + 2];
|
||||
for (int i = 0; i < oldIndices.length; i += 3) {
|
||||
int i1 = oldIndices[i];
|
||||
int i2 = oldIndices[i + 1];
|
||||
int i3 = oldIndices[i + 2];
|
||||
|
||||
// 跳过包含已删除顶点的三角形
|
||||
if (i1 == removedIndex || i2 == removedIndex || i3 == removedIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i1 > removedIndex) i1--;
|
||||
if (i2 > removedIndex) i2--;
|
||||
if (i3 > removedIndex) i3--;
|
||||
@@ -2075,18 +2171,60 @@ public class Mesh2D {
|
||||
newIndicesList.add(i2);
|
||||
newIndicesList.add(i3);
|
||||
}
|
||||
|
||||
// 填补孔洞:使用按角度排序的邻居形成扇形三角化(若邻居>=3)
|
||||
if (sortedNeighbors.size() >= 3) {
|
||||
List<Integer> remappedNeighbors = new ArrayList<>();
|
||||
for (int neighborIdx : sortedNeighbors) {
|
||||
remappedNeighbors.add(neighborIdx > removedIndex ? neighborIdx - 1 : neighborIdx);
|
||||
// remap neighbor indices 到移除后索引空间
|
||||
List<Integer> remapped = new ArrayList<>();
|
||||
for (int n : sortedNeighbors) {
|
||||
int rn = (n > removedIndex) ? (n - 1) : n;
|
||||
// 避免重复
|
||||
if (remapped.isEmpty() || remapped.get(remapped.size() - 1) != rn) {
|
||||
remapped.add(rn);
|
||||
}
|
||||
}
|
||||
int pivotIndex = remappedNeighbors.get(0);
|
||||
for (int i = 1; i < remappedNeighbors.size() - 1; i++) {
|
||||
newIndicesList.add(pivotIndex);
|
||||
newIndicesList.add(remappedNeighbors.get(i));
|
||||
newIndicesList.add(remappedNeighbors.get(i + 1));
|
||||
// 去重首尾相同的项(若存在)
|
||||
if (remapped.size() >= 2 && remapped.get(0).equals(remapped.get(remapped.size() - 1))) {
|
||||
remapped.remove(remapped.size() - 1);
|
||||
}
|
||||
|
||||
// 如果仍然不足3个顶点则跳过填充
|
||||
if (remapped.size() >= 3) {
|
||||
// 检查顺序方向(计算多边形的有向面积),确保为逆时针,如为顺时针则反转(以获得一致的三角形朝向)
|
||||
double area = 0.0;
|
||||
List<Vertex> currentVertices = activeVertexList.getVertices();
|
||||
for (int i = 0; i < remapped.size(); i++) {
|
||||
Vector2f p1 = currentVertices.get(remapped.get(i)).originalPosition;
|
||||
Vector2f p2 = currentVertices.get(remapped.get((i + 1) % remapped.size())).originalPosition;
|
||||
area += (p1.x * p2.y - p2.x * p1.y);
|
||||
}
|
||||
if (area < 0) { // 顺时针 -> 反转为逆时针
|
||||
Collections.reverse(remapped);
|
||||
}
|
||||
|
||||
// 选择扇形三角化的枢轴顶点为 remapped.get(0)
|
||||
int pivot = remapped.get(0);
|
||||
for (int i = 1; i < remapped.size() - 1; i++) {
|
||||
int a = pivot;
|
||||
int b = remapped.get(i);
|
||||
int c = remapped.get(i + 1);
|
||||
// 防止生成退化三角形(共线)
|
||||
Vector2f pa = activeVertexList.get(a).originalPosition;
|
||||
Vector2f pb = activeVertexList.get(b).originalPosition;
|
||||
Vector2f pc = activeVertexList.get(c).originalPosition;
|
||||
float cross = (pb.x - pa.x) * (pc.y - pa.y) - (pb.y - pa.y) * (pc.x - pa.x);
|
||||
if (Math.abs(cross) < 1e-8f) {
|
||||
// 跳过退化三角形
|
||||
continue;
|
||||
}
|
||||
newIndicesList.add(a);
|
||||
newIndicesList.add(b);
|
||||
newIndicesList.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 最后设置新索引并标记脏
|
||||
activeVertexList.setIndices(newIndicesList.stream().mapToInt(Integer::intValue).toArray());
|
||||
markDirty();
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class ModelPart {
|
||||
private boolean boundsDirty;
|
||||
private boolean pivotInitialized;
|
||||
|
||||
private final List<ModelEvent> events = new LinkedList<>();
|
||||
private final List<ModelEvent> events = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
private boolean inMultiSelectionOperation = false;
|
||||
private boolean startLiquefy =false;
|
||||
private final Map<String, AnimationParameter> parameters = new LinkedHashMap<>();
|
||||
@@ -98,7 +98,6 @@ public class ModelPart {
|
||||
this.boundsDirty = true;
|
||||
|
||||
updateLocalTransform();
|
||||
// 初始时 worldTransform = localTransform(无父节点时)
|
||||
recomputeWorldTransformRecursive();
|
||||
}
|
||||
|
||||
@@ -112,7 +111,8 @@ public class ModelPart {
|
||||
|
||||
private void triggerEvent(String eventName) {
|
||||
for (ModelEvent event : events) {
|
||||
event.trigger(eventName,this);
|
||||
if (event != null)
|
||||
event.trigger(eventName,this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1966,7 +1966,7 @@ public class ModelPart {
|
||||
* 获取所有网格
|
||||
*/
|
||||
public List<Mesh2D> getMeshes() {
|
||||
return new ArrayList<>(meshes);
|
||||
return meshes;
|
||||
}
|
||||
|
||||
// ==================== 参数管理 ====================
|
||||
|
||||
@@ -38,19 +38,6 @@ public class LightSource {
|
||||
this.isAmbient = true;
|
||||
}
|
||||
|
||||
// 带辉光参数
|
||||
//public LightSource(Vector2f pos, Color color, float intensity,
|
||||
// boolean isGlow, Vector2f glowDirection, float glowIntensity, float glowRadius, float glowAmount) {
|
||||
// this.position = pos;
|
||||
// this.color = colorToVector3f(color);
|
||||
// this.intensity = intensity;
|
||||
// this.isGlow = isGlow;
|
||||
// this.glowDirection = glowDirection != null ? glowDirection : new Vector2f(0f, 0f);
|
||||
// this.glowIntensity = glowIntensity;
|
||||
// this.glowRadius = glowRadius;
|
||||
// this.glowAmount = glowAmount;
|
||||
//}
|
||||
|
||||
public static Vector3f colorToVector3f(Color color) {
|
||||
if (color == null) return new Vector3f(1, 1, 1);
|
||||
return new Vector3f(
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -10,7 +12,7 @@ import java.util.Objects;
|
||||
* @author Gemini
|
||||
*/
|
||||
public class Vertex {
|
||||
|
||||
private List<Integer> controlledTriangles = new ArrayList<>();
|
||||
public Vector2f position; // 当前顶点位置 (x, y)
|
||||
public Vector2f uv; // UV坐标 (u, v)
|
||||
public Vector2f originalPosition; // 原始顶点位置 (用于变形)
|
||||
@@ -141,7 +143,13 @@ public class Vertex {
|
||||
* 创建此顶点的深拷贝
|
||||
*/
|
||||
public Vertex copy() {
|
||||
return new Vertex(this.position, this.uv, this.originalPosition);
|
||||
Vertex copy = new Vertex(this.position, this.uv, this.originalPosition);
|
||||
copy.setTag(this.tag);
|
||||
copy.setSelected(this.selected);
|
||||
copy.setName(this.name);
|
||||
copy.setControlledTriangles(this.controlledTriangles);
|
||||
copy.setIndex(this.index);
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -183,4 +191,12 @@ public class Vertex {
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<Integer> getControlledTriangles() {
|
||||
return controlledTriangles;
|
||||
}
|
||||
|
||||
public void setControlledTriangles(List<Integer> controlledTriangles) {
|
||||
this.controlledTriangles = (controlledTriangles != null) ? controlledTriangles : new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@@ -1503,11 +1503,24 @@ public final class RenderSystem {
|
||||
}
|
||||
}
|
||||
|
||||
public static void readPixels(int x, int y, int width, int height, int format, int type, int pixels) {
|
||||
if (!isOnRenderThread()) {
|
||||
recordRenderCall(() -> _readPixels(x, y, width, height, format, type, pixels));
|
||||
} else {
|
||||
_readPixels(x, y, width, height, format, type, pixels);
|
||||
}
|
||||
}
|
||||
|
||||
private static void _readPixels(int x, int y, int width, int height, int format, int type, java.nio.ByteBuffer pixels) {
|
||||
assertOnRenderThread();
|
||||
GL11.glReadPixels(x, y, width, height, format, type, pixels);
|
||||
}
|
||||
|
||||
private static void _readPixels(int x, int y, int width, int height, int format, int type, int pixels) {
|
||||
assertOnRenderThread();
|
||||
GL11.glReadPixels(x, y, width, height, format, type, pixels);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查特定扩展是否支持
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,7 @@ import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
* @version 1.0
|
||||
*/
|
||||
public class Tesselator {
|
||||
private static final int DEFAULT_BUFFER_SIZE = 2097152; // 2MB
|
||||
private static final int DEFAULT_BUFFER_SIZE = 2097152;
|
||||
private static final Tesselator INSTANCE = new Tesselator();
|
||||
|
||||
private final BufferBuilder builder;
|
||||
|
||||
Reference in New Issue
Block a user