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:
tzdwindows 7
2025-11-16 22:43:44 +08:00
parent 27f8ab11cf
commit 8de6baf653
261 changed files with 1816 additions and 174213 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

@@ -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", "缩放");
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}
}
}
}
/**
* 获取鼠标悬停的网格
*/

View File

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

View File

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

View File

@@ -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 顶点;
// 但如果该控制顶点是当前三角形的 apexAPEX且质心位于 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
);
// 找出 apexy 最大的顶点)及 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);
// 确定当前三角形哪个是 apexy 最大)
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();
}

View File

@@ -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;
}
// ==================== 参数管理 ====================

View File

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

View File

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

View File

@@ -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);
}
/**
* 检查特定扩展是否支持
*/

View File

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