feat(render): 实现图层管理和渲染优化功能- 新增 LayerCellRenderer 类,用于渲染模型图层列表,支持可见性切换和缩略图显示- 添加 LayerOperationManager 类,提供图层的增删改查和视觉顺序调整功能

- 实现 LayerReorderTransferHandler 类,支持通过拖拽方式重新排列图层顺序- 优化 Mesh2D 类,引入 renderVertices 渲染缓存机制,提升渲染性能
- 完善二级顶点系统,增强网格变形算法,修复顶点移动和平移相关问题
- 改进三角分配变形算法,增加 pinned 控制点支持和整体位移校正
- 更新 GLContextManager任务队列处理逻辑,增加超时和中断处理机制- 修正模型包装器文档注释格式,提高代码可读性
This commit is contained in:
tzdwindows 7
2025-11-01 18:33:59 +08:00
parent e06c59c8d1
commit 5c66838b3e
19 changed files with 2811 additions and 1814 deletions

View File

@@ -14,7 +14,7 @@ import java.util.List;
/**
* Anime2VividModelWrapper - 对之前 Anime2Segmenter 的封装提供更便捷的API
*
* <p>
* 用法示例:
* Anime2VividModelWrapper wrapper = Anime2VividModelWrapper.load(Paths.get("/path/to/modelDir"));
* Map<String, Anime2VividModelWrapper.ResultFiles> out = wrapper.segmentAndSave(

View File

@@ -14,6 +14,7 @@ import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement;
import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram;
import org.joml.Matrix3f;
import org.joml.Vector2f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
@@ -674,6 +675,309 @@ public final class ModelRender {
RenderSystem.checkGLError("render_end");
}
// ================== 缩略图渲染方法 ==================
/**
* 渲染模型缩略图(图层式渲染,不受摄像机控制)
*
* <p>该方法提供类似PS图层预览的缩略图渲染功能</p>
* <ul>
* <li>固定位置和大小,不受摄像机影响</li>
* <li>自动缩放确保模型完全可见</li>
* <li>禁用复杂效果以提高性能</li>
* <li>独立的渲染状态管理</li>
* </ul>
*
* @param model 要渲染的模型
* @param x 缩略图左上角X坐标屏幕坐标
* @param y 缩略图左上角Y坐标屏幕坐标
* @param width 缩略图宽度
* @param height 缩略图高度
*/
public static void renderThumbnail(Model2D model, float x, float y, float width, float height) {
if (!initialized) throw new IllegalStateException("ModelRender not initialized");
if (model == null) return;
RenderSystem.assertOnRenderThread();
RenderSystem.checkGLError("renderThumbnail_start");
// 保存原始状态以便恢复
boolean originalRenderColliders = renderColliders;
boolean originalRenderLightPositions = renderLightPositions;
int originalViewportWidth = viewportWidth;
int originalViewportHeight = viewportHeight;
try {
// 设置缩略图专用状态
renderColliders = false;
renderLightPositions = false;
// 设置缩略图视口(屏幕坐标)
RenderSystem.viewport((int)x, (int)y, (int)width, (int)height);
// 清除缩略图区域
RenderSystem.clear(GL11.GL_COLOR_BUFFER_BIT | (enableDepthTest ? GL11.GL_DEPTH_BUFFER_BIT : 0));
RenderSystem.checkGLError("thumbnail_after_clear");
// 简化版的模型更新(跳过物理系统)
model.update(0.016f); // 使用固定时间步长
// 计算模型边界和缩放比例
ThumbnailBounds bounds = calculateThumbnailBounds(model, width, height);
// 设置缩略图专用的正交投影(固定位置,不受摄像机影响)
Matrix3f proj = buildThumbnailProjection(width, height);
Matrix3f view = new Matrix3f().identity();
// 使用默认着色器
defaultProgram.use();
RenderSystem.checkGLError("thumbnail_after_use_program");
// 设置基础变换矩阵
setUniformMatrix3(defaultProgram, "uProjectionMatrix", proj);
setUniformMatrix3(defaultProgram, "uViewMatrix", view);
setUniformFloatInternal(defaultProgram, "uCameraZ", 0f); // 固定Z位置
RenderSystem.checkGLError("thumbnail_after_set_matrices");
// 简化光源:只使用环境光
setupThumbnailLighting(defaultProgram, model);
RenderSystem.checkGLError("thumbnail_after_setup_lighting");
// 应用缩放和平移确保模型完全可见
Matrix3f thumbnailTransform = new Matrix3f(
bounds.scale, 0, bounds.offsetX,
0, bounds.scale, bounds.offsetY,
0, 0, 1
);
// 递归渲染所有根部件(应用缩略图专用变换)
for (ModelPart p : model.getParts()) {
if (p.getParent() != null) continue;
renderPartForThumbnail(p, thumbnailTransform);
}
RenderSystem.checkGLError("thumbnail_after_render_parts");
} finally {
// 恢复原始状态
renderColliders = originalRenderColliders;
renderLightPositions = originalRenderLightPositions;
RenderSystem.viewport(0, 0, originalViewportWidth, originalViewportHeight);
}
RenderSystem.checkGLError("renderThumbnail_end");
}
/**
* 缩略图边界计算结果
*/
private static class ThumbnailBounds {
public float minX, maxX, minY, maxY;
public float scale;
public float offsetX, offsetY;
}
/**
* 计算模型的边界和合适的缩放比例
*/
private static ThumbnailBounds calculateThumbnailBounds(Model2D model, float thumbWidth, float thumbHeight) {
ThumbnailBounds bounds = new ThumbnailBounds();
// 初始化为极值
bounds.minX = Float.MAX_VALUE;
bounds.maxX = Float.MIN_VALUE;
bounds.minY = Float.MAX_VALUE;
bounds.maxY = Float.MIN_VALUE;
// 计算模型的世界坐标边界(递归遍历所有部件)
calculateModelBounds(model, bounds, new Matrix3f().identity());
// 如果模型没有有效边界,使用默认值
if (bounds.minX > bounds.maxX) {
bounds.minX = -50f;
bounds.maxX = 50f;
bounds.minY = -50f;
bounds.maxY = 50f;
}
// 计算模型宽度和高度
float modelWidth = bounds.maxX - bounds.minX;
float modelHeight = bounds.maxY - bounds.minY;
// 计算中心点
float centerX = (bounds.minX + bounds.maxX) * 0.5f;
float centerY = (bounds.minY + bounds.maxY) * 0.5f;
// 计算缩放比例(考虑边距)
float margin = 0.1f; // 10%边距
float scaleX = (thumbWidth * (1 - margin)) / modelWidth;
float scaleY = (thumbHeight * (1 - margin)) / modelHeight;
bounds.scale = Math.min(scaleX, scaleY);
// 计算偏移量(将模型中心对齐到缩略图中心)
bounds.offsetX = -centerX;
bounds.offsetY = -centerY;
return bounds;
}
/**
* 递归计算模型的边界
*/
private static void calculateModelBounds(Model2D model, ThumbnailBounds bounds, Matrix3f parentTransform) {
for (ModelPart part : model.getParts()) {
if (part.getParent() != null) continue; // 只处理根部件
// 计算部件的世界变换
part.updateWorldTransform(parentTransform, false);
Matrix3f worldTransform = part.getWorldTransform();
// 计算部件的边界
calculatePartBounds(part, bounds, worldTransform);
// 递归处理子部件
for (ModelPart child : part.getChildren()) {
calculateModelBoundsForPart(child, bounds, worldTransform);
}
}
}
/**
* 递归计算部件及其子部件的边界
*/
private static void calculateModelBoundsForPart(ModelPart part, ThumbnailBounds bounds, Matrix3f parentTransform) {
part.updateWorldTransform(parentTransform, false);
Matrix3f worldTransform = part.getWorldTransform();
calculatePartBounds(part, bounds, worldTransform);
for (ModelPart child : part.getChildren()) {
calculateModelBoundsForPart(child, bounds, worldTransform);
}
}
/**
* 计算单个部件的边界
*/
private static void calculatePartBounds(ModelPart part, ThumbnailBounds bounds, Matrix3f worldTransform) {
for (Mesh2D mesh : part.getMeshes()) {
if (!mesh.isVisible()) continue;
// 获取网格的顶点数据
float[] vertices = mesh.getVertices(); // 假设有这个方法获取原始顶点
if (vertices == null) continue;
// 变换顶点并更新边界
for (int i = 0; i < vertices.length; i += 3) { // 假设顶点格式x, y, z
float x = vertices[i];
float y = vertices[i + 1];
// 应用世界变换
Vector3f transformed = new Vector3f(x, y, 1.0f);
worldTransform.transform(transformed);
// 更新边界
bounds.minX = Math.min(bounds.minX, transformed.x);
bounds.maxX = Math.max(bounds.maxX, transformed.x);
bounds.minY = Math.min(bounds.minY, transformed.y);
bounds.maxY = Math.max(bounds.maxY, transformed.y);
}
}
}
/**
* 构建缩略图专用的正交投影矩阵
*/
private static Matrix3f buildThumbnailProjection(float width, float height) {
Matrix3f m = new Matrix3f();
// 标准正交投影,不受摄像机影响
m.set(
2.0f / width, 0.0f, -1.0f,
0.0f, -2.0f / height, 1.0f,
0.0f, 0.0f, 1.0f
);
return m;
}
/**
* 缩略图专用的部件渲染
*/
public static void renderPartForThumbnail(ModelPart part, Matrix3f parentTransform) {
part.updateWorldTransform(parentTransform, false);
Matrix3f world = part.getWorldTransform();
setPartUniforms(defaultProgram, part);
setUniformMatrix3(defaultProgram, "uModelMatrix", world);
for (Mesh2D mesh : part.getMeshes()) {
renderMeshForThumbnail(mesh, world);
}
for (ModelPart child : part.getChildren()) {
renderPartForThumbnail(child, world);
}
}
/**
* 缩略图专用的网格渲染
*/
private static void renderMeshForThumbnail(Mesh2D mesh, Matrix3f modelMatrix) {
if (!mesh.isVisible()) return;
Matrix3f matToUse = mesh.isBakedToWorld() ? new Matrix3f().identity() : new Matrix3f(modelMatrix);
if (mesh.getTexture() != null) {
mesh.getTexture().bind(0);
setUniformIntInternal(defaultProgram, "uTexture", 0);
} else {
RenderSystem.bindTexture(defaultTextureId);
setUniformIntInternal(defaultProgram, "uTexture", 0);
}
setUniformMatrix3(defaultProgram, "uModelMatrix", matToUse);
mesh.draw(defaultProgram.programId, matToUse);
RenderSystem.checkGLError("renderMeshForThumbnail");
}
/**
* 设置缩略图专用的简化光照
*/
private static void setupThumbnailLighting(ShaderProgram sp, Model2D model) {
List<LightSource> lights = model.getLights();
int ambientLightCount = 0;
// 查找环境光
for (int i = 0; i < lights.size() && ambientLightCount < 1; i++) {
LightSource light = lights.get(i);
if (light.isEnabled() && light.isAmbient()) {
setUniformVec2Internal(sp, "uLightsPos[0]", new Vector2f(0f, 0f));
setUniformVec3Internal(sp, "uLightsColor[0]", light.getColor());
setUniformFloatInternal(sp, "uLightsIntensity[0]", light.getIntensity());
setUniformIntInternal(sp, "uLightsIsAmbient[0]", 1);
setUniformIntInternal(sp, "uLightsIsGlow[0]", 0);
ambientLightCount++;
}
}
// 如果没有环境光,创建一个默认的环境光
if (ambientLightCount == 0) {
setUniformVec2Internal(sp, "uLightsPos[0]", new Vector2f(0f, 0f));
setUniformVec3Internal(sp, "uLightsColor[0]", new Vector3f(0.8f, 0.8f, 0.8f));
setUniformFloatInternal(sp, "uLightsIntensity[0]", 1.0f);
setUniformIntInternal(sp, "uLightsIsAmbient[0]", 1);
setUniformIntInternal(sp, "uLightsIsGlow[0]", 0);
ambientLightCount = 1;
}
setUniformIntInternal(sp, "uLightCount", ambientLightCount);
// 禁用所有其他光源槽位
for (int i = ambientLightCount; i < MAX_LIGHTS; i++) {
setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", 0f);
setUniformIntInternal(sp, "uLightsIsAmbient[" + i + "]", 0);
}
}
/**
* 设置所有非默认着色器的顶点坐标相关uniform

View File

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

View File

@@ -493,14 +493,21 @@ public class GLContextManager {
contextReady.thenRun(() -> {
try {
glTaskQueue.put(() -> {
boolean offered = glTaskQueue.offer(() -> {
try {
T result = task.call();
future.complete(result);
} catch (Exception e) {
future.completeExceptionally(e);
}
});
}, 5, TimeUnit.SECONDS);
if (!offered) {
future.completeExceptionally(new TimeoutException("任务队列已满无法在5秒内添加任务"));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
future.completeExceptionally(new IllegalStateException("任务提交被中断", e));
} catch (Exception e) {
future.completeExceptionally(e);
}

View File

@@ -0,0 +1,70 @@
package com.chuangzhou.vivid2D.render.awt.manager;
import com.chuangzhou.vivid2D.render.model.Model2D;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import org.joml.Vector2f;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class LayerOperationManager {
private final Model2D model;
public LayerOperationManager(Model2D model) {
this.model = model;
}
public void addLayer(String name) {
model.createPart(name);
model.markNeedsUpdate();
}
public void removeLayer(ModelPart part) {
if (part == null) return;
List<ModelPart> parts = model.getParts();
if (parts != null) parts.remove(part);
Map<String, ModelPart> partMap = model.getPartMap();
if (partMap != null) partMap.remove(part.getName());
model.markNeedsUpdate();
}
public void moveLayer(List<ModelPart> visualOrder) {
List<ModelPart> newModelParts = new ArrayList<>(visualOrder.size());
for (int i = visualOrder.size() - 1; i >= 0; i--) {
newModelParts.add(visualOrder.get(i));
}
replaceModelPartsList(newModelParts);
model.markNeedsUpdate();
}
public void setLayerOpacity(ModelPart part, float opacity) {
part.setOpacity(opacity);
model.markNeedsUpdate();
}
public void setLayerVisibility(ModelPart part, boolean visible) {
part.setVisible(visible);
model.markNeedsUpdate();
}
private void replaceModelPartsList(List<ModelPart> newParts) {
if (model == null) return;
try {
java.lang.reflect.Field partsField = model.getClass().getDeclaredField("parts");
partsField.setAccessible(true);
Object old = partsField.get(model);
if (old instanceof List) {
((List) old).clear();
((List) old).addAll(newParts);
} else {
partsField.set(model, newParts);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,244 @@
package com.chuangzhou.vivid2D.render.awt.manager;
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
import com.chuangzhou.vivid2D.render.model.util.Texture;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ThumbnailManager {
private static final int THUMBNAIL_WIDTH = 48;
private static final int THUMBNAIL_HEIGHT = 48;
private final Map<ModelPart, BufferedImage> thumbnailCache = new HashMap<>();
private ModelRenderPanel renderPanel;
public ThumbnailManager(ModelRenderPanel renderPanel) {
this.renderPanel = renderPanel;
}
public BufferedImage getThumbnail(ModelPart part) {
return thumbnailCache.get(part);
}
public void generateThumbnail(ModelPart part) {
if (renderPanel == null) return;
try {
BufferedImage thumbnail = renderPanel.getGlContextManager()
.executeInGLContext(() -> renderPartThumbnail(part))
.get();
if (thumbnail != null) {
thumbnailCache.put(part, thumbnail);
}
} catch (Exception e) {
thumbnailCache.put(part, createDefaultThumbnail());
}
}
public void removeThumbnail(ModelPart part) {
thumbnailCache.remove(part);
}
public void clearCache() {
thumbnailCache.clear();
}
/**
* 渲染单个部件的缩略图
*/
private BufferedImage renderPartThumbnail(ModelPart part) {
if (renderPanel == null) return createDefaultThumbnail();
try {
return createThumbnailForPart(part);
} catch (Exception e) {
e.printStackTrace();
return createDefaultThumbnail();
}
}
private BufferedImage createThumbnailForPart(ModelPart part) {
BufferedImage thumbnail = new BufferedImage(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = thumbnail.createGraphics();
// 设置抗锯齿
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
// 绘制背景
g2d.setColor(new Color(40, 40, 40));
g2d.fillRect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
try {
// 尝试获取部件的纹理
Texture texture = null;
List<Mesh2D> meshes = part.getMeshes();
if (meshes != null && !meshes.isEmpty()) {
for (Mesh2D mesh : meshes) {
texture = mesh.getTexture();
if (texture != null) break;
}
}
if (texture != null && !texture.isDisposed()) {
// 获取纹理的 BufferedImage
BufferedImage textureImage = textureToBufferedImage(texture);
if (textureImage != null) {
// 计算缩放比例以保持宽高比
int imgWidth = textureImage.getWidth();
int imgHeight = textureImage.getHeight();
if (imgWidth > 0 && imgHeight > 0) {
float scale = Math.min(
(float)(THUMBNAIL_WIDTH - 8) / imgWidth,
(float)(THUMBNAIL_HEIGHT - 8) / imgHeight
);
int scaledWidth = (int)(imgWidth * scale);
int scaledHeight = (int)(imgHeight * scale);
int x = (THUMBNAIL_WIDTH - scaledWidth) / 2;
int y = (THUMBNAIL_HEIGHT - scaledHeight) / 2;
// 绘制纹理图片
g2d.drawImage(textureImage, x, y, scaledWidth, scaledHeight, null);
// 绘制边框
g2d.setColor(Color.WHITE);
g2d.drawRect(x, y, scaledWidth - 1, scaledHeight - 1);
}
}
}
} catch (Exception e) {
System.err.println("生成缩略图失败: " + part.getName() + " - " + e.getMessage());
}
// 如果部件不可见,绘制红色斜线覆盖
if (!part.isVisible()) {
g2d.setColor(new Color(255, 0, 0, 128)); // 半透明红色
g2d.setStroke(new BasicStroke(3));
g2d.drawLine(2, 2, THUMBNAIL_WIDTH - 2, THUMBNAIL_HEIGHT - 2);
g2d.drawLine(THUMBNAIL_WIDTH - 2, 2, 2, THUMBNAIL_HEIGHT - 2);
}
g2d.dispose();
return thumbnail;
}
/**
* 将Texture转换为BufferedImage
*/
private BufferedImage textureToBufferedImage(Texture texture) {
try {
// 确保纹理有像素数据缓存
texture.ensurePixelDataCached();
if (!texture.hasPixelData()) {
System.err.println("纹理没有像素数据: " + texture.getName());
return null;
}
byte[] pixelData = texture.getPixelData();
if (pixelData == null || pixelData.length == 0) {
return null;
}
int width = texture.getWidth();
int height = texture.getHeight();
Texture.TextureFormat format = texture.getFormat();
int components = format.getComponents();
// 创建BufferedImage
BufferedImage image;
switch (components) {
case 1: // 单通道
image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
break;
case 3: // RGB
image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
break;
case 4: // RGBA
image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
break;
default:
System.err.println("不支持的纹理格式组件数量: " + components);
return null;
}
// 将像素数据复制到BufferedImage同时翻转垂直方向
if (components == 4) {
// RGBA格式 - 垂直翻转
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int srcY = height - 1 - y; // 翻转Y坐标
for (int x = 0; x < width; x++) {
int srcIndex = (srcY * width + x) * 4;
int dstIndex = y * width + x;
int r = pixelData[srcIndex] & 0xFF;
int g = pixelData[srcIndex + 1] & 0xFF;
int b = pixelData[srcIndex + 2] & 0xFF;
int a = pixelData[srcIndex + 3] & 0xFF;
pixels[dstIndex] = (a << 24) | (r << 16) | (g << 8) | b;
}
}
image.setRGB(0, 0, width, height, pixels, 0, width);
} else if (components == 3) {
// RGB格式 - 垂直翻转
for (int y = 0; y < height; y++) {
int srcY = height - 1 - y; // 翻转Y坐标
for (int x = 0; x < width; x++) {
int srcIndex = (srcY * width + x) * 3;
int r = pixelData[srcIndex] & 0xFF;
int g = pixelData[srcIndex + 1] & 0xFF;
int b = pixelData[srcIndex + 2] & 0xFF;
int rgb = (r << 16) | (g << 8) | b;
image.setRGB(x, y, rgb);
}
}
} else if (components == 1) {
// 单通道格式 - 垂直翻转
for (int y = 0; y < height; y++) {
int srcY = height - 1 - y; // 翻转Y坐标
for (int x = 0; x < width; x++) {
int srcIndex = srcY * width + x;
int gray = pixelData[srcIndex] & 0xFF;
int rgb = (gray << 16) | (gray << 8) | gray;
image.setRGB(x, y, rgb);
}
}
}
return image;
} catch (Exception e) {
System.err.println("转换纹理到BufferedImage失败: " + texture.getName() + " - " + e.getMessage());
e.printStackTrace();
return null;
}
}
private BufferedImage createDefaultThumbnail() {
BufferedImage thumbnail = new BufferedImage(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = thumbnail.createGraphics();
g2d.setColor(new Color(60, 60, 60));
g2d.fillRect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
g2d.setColor(Color.GRAY);
g2d.drawRect(2, 2, THUMBNAIL_WIDTH - 5, THUMBNAIL_HEIGHT - 5);
g2d.setColor(Color.WHITE);
g2d.drawString("?", THUMBNAIL_WIDTH/2 - 4, THUMBNAIL_HEIGHT/2 + 4);
g2d.dispose();
return thumbnail;
}
}

View File

@@ -5,6 +5,7 @@ import com.chuangzhou.vivid2D.render.model.Model2D;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex;
import org.joml.Vector2f;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -89,6 +90,7 @@ public class SelectionTool extends Tool {
dragStartY = modelY;
// 获取边界框和中心点
// 确保使用世界坐标下的边界框
BoundingBox bounds = targetMeshForHandle.getBounds();
renderPanel.getCameraManagement().getRotationCenter().set(
(bounds.getMinX() + bounds.getMaxX()) / 2.0f,
@@ -112,6 +114,7 @@ public class SelectionTool extends Tool {
dragStartY = modelY;
// 记录初始中心点位置
// 确保使用世界坐标下的边界框
BoundingBox bounds = targetMeshForHandle.getBounds();
renderPanel.getCameraManagement().getRotationCenter().set(
(bounds.getMinX() + bounds.getMaxX()) / 2.0f,
@@ -127,6 +130,7 @@ public class SelectionTool extends Tool {
dragStartX = modelX;
dragStartY = modelY;
// 确保使用世界坐标下的边界框
BoundingBox bounds = targetMeshForHandle.getBounds();
resizeStartWidth = bounds.getWidth();
resizeStartHeight = bounds.getHeight();
@@ -242,7 +246,6 @@ public class SelectionTool extends Tool {
handleResizeDrag(modelX, modelY);
break;
}
} catch (Exception ex) {
logger.error("选择工具处理鼠标拖拽时出错", ex);
}
@@ -447,7 +450,7 @@ public class SelectionTool extends Tool {
part.setScale(currentScale.x * relScaleX, currentScale.y * relScaleY);
// 同步缩放该部件下的所有网格的二级顶点
syncSecondaryVerticesScaleForPart(part, relScaleX, relScaleY);
//syncSecondaryVerticesScaleForPart(part, relScaleX, relScaleY);
}
// 更新拖拽起始位置和初始尺寸
@@ -467,10 +470,32 @@ public class SelectionTool extends Tool {
if (meshes == null) return;
for (Mesh2D mesh : meshes) {
if (mesh != null && mesh.getSecondaryVertexCount() > 0) {
mesh.moveSecondaryVertices(deltaX, deltaY);
if (mesh != null && mesh.isVisible() && mesh.getSecondaryVertexCount() > 0) {
List<SecondaryVertex> secondaryVertices = mesh.getSecondaryVertices();
if (secondaryVertices != null) {
// 遍历所有顶点,逐个调用 moveSecondaryVertex
for (SecondaryVertex vertex : secondaryVertices) {
// 【修正 1避免双重平移和状态冲突】
// 仅对未锁定/未固定的顶点执行局部坐标平移。
// 锁定的顶点不应被工具的同步逻辑移动,它们应该随 ModelPart 的世界变换移动。
if (!vertex.isLocked() && !vertex.isPinned()) {
// 计算顶点的新局部坐标 (position + delta)
float newX = vertex.getPosition().x + deltaX;
float newY = vertex.getPosition().y + deltaY;
// 使用 moveSecondaryVertex 方法
mesh.moveSecondaryVertex(vertex, newX, newY);
// 注意mesh.moveSecondaryVertex 内部会触发形变计算和 markDirty
}
}
}
}
}
part.setPosition(part.getPosition());
// 递归处理子部件
for (ModelPart child : part.getChildren()) {
@@ -526,10 +551,10 @@ public class SelectionTool extends Tool {
return ModelRenderPanel.DragMode.NONE;
}
// 统一使用世界坐标边界框
BoundingBox bounds;
Vector2f center;
// 多选状态下使用多选边界框
if (targetMesh.isInMultiSelection()) {
bounds = targetMesh.getMultiSelectionBounds();
center = bounds.getCenter();
@@ -566,6 +591,7 @@ public class SelectionTool extends Tool {
float expandedMaxX = maxX + borderThickness;
float expandedMaxY = maxY + borderThickness;
// 如果不在扩展边界内直接返回NONE
if (result == ModelRenderPanel.DragMode.NONE) {
if (modelX < expandedMinX || modelX > expandedMaxX ||
modelY < expandedMinY || modelY > expandedMaxY) {
@@ -600,9 +626,13 @@ public class SelectionTool extends Tool {
}
}
logger.debug("手柄检测: 位置({}, {}), 边界[{}, {}, {}, {}], 结果: {}",
modelX, modelY, minX, minY, maxX, maxY, result);
return result;
}
// 辅助方法:检查点是否在中心点、旋转手柄、角点区域内
private boolean isPointInCenterHandle(float x, float y, float centerX, float centerY, float handleSize) {
return Math.abs(x - centerX) <= handleSize && Math.abs(y - centerY) <= handleSize;
@@ -732,14 +762,11 @@ public class SelectionTool extends Tool {
*/
public void setSelectedMesh(Mesh2D mesh) {
renderPanel.getGlContextManager().executeInGLContext(() -> {
// 清除之前选中的所有网格
for (Mesh2D selectedMesh : selectedMeshes) {
selectedMesh.setSelected(false);
selectedMesh.clearMultiSelection();
}
selectedMeshes.clear();
// 设置新的选中网格
if (mesh != null) {
mesh.setSelected(true);
selectedMeshes.add(mesh);
@@ -760,7 +787,14 @@ public class SelectionTool extends Tool {
mesh.setSelected(true);
selectedMeshes.add(mesh);
lastSelectedMesh = mesh;
ModelPart part = findPartByMesh(mesh);
if (part != null) {
part.updateMeshVertices();
}
updateMultiSelectionInMeshes();
for (ModelPart selectedPart : getSelectedParts()) {
selectedPart.updateMeshVertices();
}
}
});
}
@@ -787,13 +821,34 @@ public class SelectionTool extends Tool {
*/
public void clearSelectedMeshes() {
renderPanel.getGlContextManager().executeInGLContext(() -> {
// 记录所有受影响的 ModelPart以便在清除选中状态后更新它们的网格顶点
// Use a Set to collect unique ModelParts
Set<ModelPart> affectedParts = new HashSet<>();
// 1. 清除网格的选中状态并收集父 ModelPart
for (Mesh2D mesh : selectedMeshes) {
mesh.setSelected(false);
mesh.setSuspension(false);
mesh.clearMultiSelection();
// 查找并记录父 ModelPart
ModelPart part = findPartByMesh(mesh);
if (part != null) {
affectedParts.add(part);
}
}
// 2. 清除选择集
selectedMeshes.clear();
lastSelectedMesh = null;
// 3. 强制更新所有受影响 ModelPart 的网格顶点。
// 这将确保网格的渲染顶点renderVertices从 ModelPart 的世界变换中重新同步,
// 从而修复多选结束后位置重置的错误。
for (ModelPart part : affectedParts) {
// 关键的修复:强制 ModelPart 重新同步其网格顶点,恢复正确的世界位置
part.updateMeshVertices();
}
});
}
@@ -805,14 +860,19 @@ public class SelectionTool extends Tool {
Model2D model = renderPanel.getModel();
if (model == null) return;
// 清除之前的选择
// 1. 清除之前的选择
for (Mesh2D mesh : selectedMeshes) {
mesh.setSelected(false);
mesh.clearMultiSelection();
// 在清除前获取并更新 ModelPart 也是一个好习惯,确保状态一致性
ModelPart part = findPartByMesh(mesh);
if (part != null) {
part.updateMeshVertices();
}
}
selectedMeshes.clear();
// 获取所有网格并选中
// 2. 获取所有网格并选中
List<Mesh2D> allMeshes = getAllMeshesFromModel(model);
for (Mesh2D mesh : allMeshes) {
if (mesh.isVisible()) {
@@ -821,12 +881,16 @@ public class SelectionTool extends Tool {
}
}
// 设置最后选中的网格
// 3. 设置最后选中的网格
if (!selectedMeshes.isEmpty()) {
lastSelectedMesh = selectedMeshes.iterator().next();
}
updateMultiSelectionInMeshes();
for (ModelPart selectedPart : getSelectedParts()) {
selectedPart.updateMeshVertices();
}
});
}

View File

@@ -6,6 +6,7 @@ import com.chuangzhou.vivid2D.render.model.ModelPart;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex;
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
import org.joml.Vector2f;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -28,7 +29,9 @@ public class VertexDeformationTool extends Tool {
private static final float VERTEX_TOLERANCE = 8.0f;
private ModelRenderPanel.DragMode currentDragMode = ModelRenderPanel.DragMode.NONE;
private float dragStartX, dragStartY;
private float savedCameraRotation = Float.NaN;
private Vector2f savedCameraScale = new Vector2f(1,1);
private boolean cameraStateSaved = false;
public VertexDeformationTool(ModelRenderPanel renderPanel) {
super(renderPanel, "顶点变形工具", "通过二级顶点对网格进行精细变形操作");
}
@@ -47,17 +50,28 @@ public class VertexDeformationTool extends Tool {
targetMesh = findFirstVisibleMesh();
}
// 记录并重置相机(如果可用)到默认状态:旋转 = 0缩放 = 1
try {
if (renderPanel.getCameraManagement() != null) {
// 备份
savedCameraRotation = targetMesh.getModelPart().getRotation();
savedCameraScale = targetMesh.getModelPart().getScale();
cameraStateSaved = true;
// 设置为默认
targetMesh.getModelPart().setRotation(0f);
targetMesh.getModelPart().setScale(1f);
}
} catch (Throwable t) {
// 若没有这些方法或发生异常则记录但不阻塞工具激活
logger.debug("无法备份/设置相机状态: {}", t.getMessage());
}
if (targetMesh != null) {
// 显示二级顶点
associatedRanderTools.setAlgorithmEnabled("showSecondaryVertices", true);
targetMesh.setShowSecondaryVertices(true);
targetMesh.setRenderVertices(true);
// 如果没有二级顶点,创建默认的四个角点
//if (targetMesh.getSecondaryVertexCount() == 0) {
// createDefaultSecondaryVertices();
//}
logger.info("激活顶点变形工具: {}", targetMesh.getName());
} else {
logger.warn("没有找到可用的网格用于顶点变形");
@@ -69,10 +83,26 @@ public class VertexDeformationTool extends Tool {
if (!isActive) return;
isActive = false;
// 恢复相机之前的旋转/缩放状态(如果已保存)
try {
if (cameraStateSaved && renderPanel.getCameraManagement() != null) {
targetMesh.getModelPart().setRotation(savedCameraRotation);
targetMesh.getModelPart().setScale(savedCameraScale);
}
} catch (Throwable t) {
logger.debug("无法恢复相机状态: {}", t.getMessage());
} finally {
cameraStateSaved = false;
savedCameraRotation = Float.NaN;
savedCameraScale = new Vector2f(1,1);
}
if (targetMesh != null) {
associatedRanderTools.setAlgorithmEnabled("showSecondaryVertices", false);
targetMesh.setShowSecondaryVertices(false);
targetMesh.setRenderVertices(false);
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
}
targetMesh = null;
selectedVertex = null;
@@ -124,8 +154,6 @@ public class VertexDeformationTool extends Tool {
if (!isActive || selectedVertex == null) return;
if (currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX) {
float deltaX = modelX - dragStartX;
float deltaY = modelY - dragStartY;
// 移动顶点到新位置
selectedVertex.setPosition(modelX, modelY);
@@ -135,8 +163,7 @@ public class VertexDeformationTool extends Tool {
dragStartY = modelY;
// 标记网格为脏状态,需要重新计算边界等
targetMesh.markDirty();
targetMesh.updateBounds();
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
// 强制重绘
renderPanel.repaint();
@@ -247,8 +274,7 @@ public class VertexDeformationTool extends Tool {
logger.info("创建二级顶点: ID={}, 位置({}, {}), UV({}, {})",
newVertex.getId(), x, y, u, v);
// 标记网格为脏状态
targetMesh.markDirty();
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
renderPanel.repaint();
} else {
logger.warn("创建二级顶点失败");
@@ -272,7 +298,7 @@ public class VertexDeformationTool extends Tool {
logger.info("删除二级顶点: ID={}", vertex.getId());
// 标记网格为脏状态
targetMesh.markDirty();
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
renderPanel.repaint();
} else {
logger.warn("删除二级顶点失败: ID={}", vertex.getId());

View File

@@ -0,0 +1,267 @@
package com.chuangzhou.vivid2D.render.awt.util;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
import com.chuangzhou.vivid2D.render.model.util.Texture;
import org.joml.Vector2f;
import org.lwjgl.system.MemoryUtil;
import java.awt.image.BufferedImage;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
public class MeshTextureUtil {
public static Mesh2D createQuadForImage(BufferedImage img, String meshName) {
float w = img.getWidth();
float h = img.getHeight();
try {
Mesh2D o = Mesh2D.createQuad(meshName, w, h);
return subdivideMeshForLiquify(o, 3);
} catch (Exception ignored) {
}
try {
return createSubdividedQuad(meshName, w, h, 3);
} catch (Exception ex) {
ex.printStackTrace();
}
throw new RuntimeException("无法创建 Mesh2D");
}
private static Mesh2D createSubdividedQuad(String name, float width, float height, int subdivisionLevel) {
int segments = (int) Math.pow(2, subdivisionLevel);
int vertexCount = (segments + 1) * (segments + 1);
int triangleCount = segments * segments * 2;
float[] vertices = new float[vertexCount * 2];
float[] uvs = new float[vertexCount * 2];
int[] indices = new int[triangleCount * 3];
float halfW = width / 2f;
float halfH = height / 2f;
int vertexIndex = 0;
for (int y = 0; y <= segments; y++) {
for (int x = 0; x <= segments; x++) {
float xPos = -halfW + (x * width) / segments;
float yPos = -halfH + (y * height) / segments;
vertices[vertexIndex * 2] = xPos;
vertices[vertexIndex * 2 + 1] = yPos;
uvs[vertexIndex * 2] = (float) x / segments;
uvs[vertexIndex * 2 + 1] = 1f - (float) y / segments;
vertexIndex++;
}
}
int index = 0;
for (int y = 0; y < segments; y++) {
for (int x = 0; x < segments; x++) {
int topLeft = y * (segments + 1) + x;
int topRight = topLeft + 1;
int bottomLeft = (y + 1) * (segments + 1) + x;
int bottomRight = bottomLeft + 1;
indices[index++] = topLeft;
indices[index++] = topRight;
indices[index++] = bottomLeft;
indices[index++] = topRight;
indices[index++] = bottomRight;
indices[index++] = bottomLeft;
}
}
try {
Constructor<?> cons = null;
for (Constructor<?> c : Mesh2D.class.getDeclaredConstructors()) {
Class<?>[] params = c.getParameterTypes();
if (params.length >= 4 && params[0] == String.class) {
cons = c;
break;
}
}
if (cons != null) {
cons.setAccessible(true);
Object meshObj = cons.newInstance(name, vertices, uvs, indices);
if (meshObj instanceof Mesh2D mesh) {
mesh.setPivot(0, 0);
if (mesh.getOriginalPivot() != null) {
mesh.setOriginalPivot(new Vector2f(0, 0));
}
return mesh;
}
}
} catch (Exception e) {
e.printStackTrace();
}
throw new RuntimeException("无法创建细分网格");
}
private static Mesh2D subdivideMeshForLiquify(Mesh2D originalMesh, int subdivisionLevel) {
if (subdivisionLevel <= 0) return originalMesh;
try {
float[] origVertices = originalMesh.getVertices();
float[] origUVs = originalMesh.getUVs();
int[] origIndices = originalMesh.getIndices();
List<Vector2f> newVertices = new ArrayList<>();
List<Vector2f> newUVs = new ArrayList<>();
List<Integer> newIndices = new ArrayList<>();
for (int i = 0; i < origVertices.length / 2; i++) {
newVertices.add(new Vector2f(origVertices[i * 2], origVertices[i * 2 + 1]));
newUVs.add(new Vector2f(origUVs[i * 2], origUVs[i * 2 + 1]));
}
for (int i = 0; i < origIndices.length; i += 3) {
int i1 = origIndices[i];
int i2 = origIndices[i + 1];
int i3 = origIndices[i + 2];
Vector2f v1 = newVertices.get(i1);
Vector2f v2 = newVertices.get(i2);
Vector2f v3 = newVertices.get(i3);
Vector2f uv1 = newUVs.get(i1);
Vector2f uv2 = newUVs.get(i2);
Vector2f uv3 = newUVs.get(i3);
Vector2f mid12 = new Vector2f(v1).add(v2).mul(0.5f);
Vector2f mid23 = new Vector2f(v2).add(v3).mul(0.5f);
Vector2f mid31 = new Vector2f(v3).add(v1).mul(0.5f);
Vector2f uvMid12 = new Vector2f(uv1).add(uv2).mul(0.5f);
Vector2f uvMid23 = new Vector2f(uv2).add(uv3).mul(0.5f);
Vector2f uvMid31 = new Vector2f(uv3).add(uv1).mul(0.5f);
int mid12Idx = newVertices.size();
newVertices.add(mid12);
newUVs.add(uvMid12);
int mid23Idx = newVertices.size();
newVertices.add(mid23);
newUVs.add(uvMid23);
int mid31Idx = newVertices.size();
newVertices.add(mid31);
newUVs.add(uvMid31);
newIndices.add(i1); newIndices.add(mid12Idx); newIndices.add(mid31Idx);
newIndices.add(i2); newIndices.add(mid23Idx); newIndices.add(mid12Idx);
newIndices.add(i3); newIndices.add(mid31Idx); newIndices.add(mid23Idx);
newIndices.add(mid12Idx); newIndices.add(mid23Idx); newIndices.add(mid31Idx);
}
float[] finalVertices = new float[newVertices.size() * 2];
float[] finalUVs = new float[newUVs.size() * 2];
int[] finalIndices = new int[newIndices.size()];
for (int i = 0; i < newVertices.size(); i++) {
finalVertices[i * 2] = newVertices.get(i).x;
finalVertices[i * 2 + 1] = newVertices.get(i).y;
finalUVs[i * 2] = newUVs.get(i).x;
finalUVs[i * 2 + 1] = newUVs.get(i).y;
}
for (int i = 0; i < newIndices.size(); i++) {
finalIndices[i] = newIndices.get(i);
}
Mesh2D subdividedMesh = originalMesh.copy();
subdividedMesh.setMeshData(finalVertices, finalUVs, finalIndices);
if (subdivisionLevel > 1) {
return subdivideMeshForLiquify(subdividedMesh, subdivisionLevel - 1);
}
return subdividedMesh;
} catch (Exception e) {
e.printStackTrace();
return originalMesh;
}
}
public static Texture tryCreateTextureFromImageMemory(BufferedImage img, String texName) {
try {
int w = img.getWidth();
int h = img.getHeight();
ByteBuffer buf = imageToRGBAByteBuffer(img);
Constructor<?> suit = null;
for (Constructor<?> c : Texture.class.getDeclaredConstructors()) {
Class<?>[] ps = c.getParameterTypes();
if (ps.length >= 4 && ps[0] == String.class) {
suit = c;
break;
}
}
if (suit != null) {
suit.setAccessible(true);
Object texObj = null;
Class<?>[] ps = suit.getParameterTypes();
if (ps.length >= 5 && ps[3].getSimpleName().toLowerCase().contains("format")) {
Object formatEnum = null;
try {
Class<?> formatCls = null;
for (Class<?> inner : Texture.class.getDeclaredClasses()) {
if (inner.getSimpleName().toLowerCase().contains("format")) {
formatCls = inner;
break;
}
}
if (formatCls != null) {
for (Field f : formatCls.getFields()) {
if (f.getName().toUpperCase().contains("RGBA")) {
formatEnum = f.get(null);
break;
}
}
}
} catch (Throwable ignored) {
}
if (formatEnum != null) {
try {
texObj = suit.newInstance(texName, w, h, formatEnum, buf);
} catch (Throwable ignored) {
}
}
}
if (texObj == null) {
try {
texObj = suit.newInstance(texName, w, h, buf);
} catch (Throwable ignored) {
}
}
if (texObj instanceof Texture) return (Texture) texObj;
}
} catch (Throwable t) {
t.printStackTrace();
}
return null;
}
private static ByteBuffer imageToRGBAByteBuffer(BufferedImage img) {
final int w = img.getWidth();
final int h = img.getHeight();
final int[] pixels = new int[w * h];
img.getRGB(0, 0, w, h, pixels, 0, w);
ByteBuffer buffer = MemoryUtil.memAlloc(w * h * 4).order(ByteOrder.nativeOrder());
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int argb = pixels[y * w + x];
int a = (argb >> 24) & 0xFF;
int r = (argb >> 16) & 0xFF;
int g = (argb >> 8) & 0xFF;
int b = (argb) & 0xFF;
buffer.put((byte) r);
buffer.put((byte) g);
buffer.put((byte) b);
buffer.put((byte) a);
}
}
buffer.flip();
return buffer;
}
}

View File

@@ -0,0 +1,187 @@
package com.chuangzhou.vivid2D.render.awt.util;
import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel;
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
import com.chuangzhou.vivid2D.render.awt.util.PsdParser;
import com.chuangzhou.vivid2D.render.model.Model2D;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
import com.chuangzhou.vivid2D.render.model.util.Texture;
import javax.swing.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class PSDImporter {
private final Model2D model;
private final ModelRenderPanel renderPanel;
private final ModelLayerPanel layerPanel;
public PSDImporter(Model2D model, ModelRenderPanel renderPanel, ModelLayerPanel layerPanel) {
this.model = model;
this.renderPanel = renderPanel;
this.layerPanel = layerPanel;
}
public void importPSDFile(File psdFile) {
try {
PsdParser.PSDImportResult result = PsdParser.parsePSDFile(psdFile);
if (result != null && !result.layers.isEmpty()) {
int choice = JOptionPane.showConfirmDialog(null,
String.format("PSD文件包含 %d 个图层,是否全部导入?", result.layers.size()),
"导入PSD图层", JOptionPane.YES_NO_OPTION);
if (choice == JOptionPane.YES_OPTION) {
importPSDLayers(result);
}
}
} catch (Exception ex) {
JOptionPane.showMessageDialog(null,
"解析PSD文件失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
private void importPSDLayers(PsdParser.PSDImportResult result) {
if (renderPanel != null) {
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
List<ModelPart> createdParts = createPartsFromPSDLayers(result.layers);
SwingUtilities.invokeLater(() -> notifyImportComplete(createdParts));
} catch (Exception e) {
SwingUtilities.invokeLater(() ->
showError("导入PSD图层失败: " + e.getMessage()));
}
});
} else {
List<ModelPart> createdParts = createPartsFromPSDLayers(result.layers);
notifyImportComplete(createdParts);
}
}
private List<ModelPart> createPartsFromPSDLayers(List<PsdParser.PSDLayerInfo> layers) {
List<ModelPart> createdParts = new ArrayList<>();
for (PsdParser.PSDLayerInfo layerInfo : layers) {
ModelPart part = createPartFromPSDLayer(layerInfo);
if (part != null) {
createdParts.add(part);
}
}
return createdParts;
}
private ModelPart createPartFromPSDLayer(PsdParser.PSDLayerInfo layerInfo) {
try {
System.out.println("正在创建PSD图层: " + layerInfo.name + " [" +
layerInfo.width + "x" + layerInfo.height + "]" + "[x=" + layerInfo.x + ",y=" + layerInfo.y + "]");
// 确保部件名唯一,避免覆盖已有部件导致"合并成一个图层"的问题
String uniqueName = ensureUniquePartName(layerInfo.name);
// 创建部件
ModelPart part = model.createPart(uniqueName);
if (part == null) {
System.err.println("创建部件失败: " + uniqueName);
return null;
}
// 如果 model 有 partMap更新映射防止老实现以 name 为 key 覆盖或冲突)
try {
Map<String, ModelPart> partMap = layerPanel.getModelPartMap();
if (partMap != null) {
partMap.put(uniqueName, part);
}
} catch (Exception ignored) {
}
part.setVisible(layerInfo.visible);
// 设置不透明度(优先使用公开方法)
try {
part.setOpacity(layerInfo.opacity);
} catch (Throwable t) {
// 如果没有公开方法,尝试通过反射备用(保持兼容)
try {
Field f = part.getClass().getDeclaredField("opacity");
f.setAccessible(true);
f.setFloat(part, layerInfo.opacity);
} catch (Throwable ignored) {
System.err.println("设置不透明度失败: " + uniqueName);
}
}
part.setPosition(layerInfo.x, layerInfo.y);
// 创建网格(使用唯一 mesh 名避免工厂复用同一实例)
long uniq = System.nanoTime();
Mesh2D mesh = MeshTextureUtil.createQuadForImage(layerInfo.image, uniqueName + "_mesh_" + uniq);
// 把 mesh 加入 part注意部分实现可能复制或包装 mesh
part.addMesh(mesh);
// 创建纹理(使用唯一名称,防止按 name 在内部被复用或覆盖)
String texName = uniqueName + "_tex_" + uniq;
Texture texture = layerPanel.createTextureFromBufferedImage(layerInfo.image, texName);
try {
List<Mesh2D> partMeshes = part.getMeshes();
Mesh2D actualMesh = null;
if (partMeshes != null && !partMeshes.isEmpty()) {
actualMesh = partMeshes.get(partMeshes.size() - 1);
}
if (actualMesh != null) {
actualMesh.setTexture(texture);
} else {
mesh.setTexture(texture);
}
model.addTexture(texture);
model.markNeedsUpdate();
} catch (Throwable e) {
System.err.println("在绑定纹理到 mesh 时出错: " + uniqueName + " - " + e.getMessage());
e.printStackTrace();
}
SwingUtilities.invokeLater(() -> {
try {
layerPanel.reloadFromModel();
} catch (Throwable ignored) {
}
try {
if (renderPanel != null) renderPanel.repaint();
} catch (Throwable ignored) {
}
});
return part;
} catch (Exception e) {
System.err.println("创建PSD图层部件失败: " + layerInfo.name + " - " + e.getMessage());
e.printStackTrace();
return null;
}
}
private String ensureUniquePartName(String baseName) {
if (model == null) return baseName;
Map<String, ModelPart> partMap = layerPanel.getModelPartMap();
if (partMap == null) return baseName;
String name = baseName;
int idx = 1;
while (partMap.containsKey(name)) {
name = baseName + "_" + idx++;
}
return name;
}
private void notifyImportComplete(List<ModelPart> createdParts) {
if (model != null) {
model.markNeedsUpdate();
}
// 通知监听器导入完成
}
private void showError(String message) {
JOptionPane.showMessageDialog(null, message, "错误", JOptionPane.ERROR_MESSAGE);
}
}

View File

@@ -0,0 +1,120 @@
package com.chuangzhou.vivid2D.render.awt.util.renderer;
import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel;
import com.chuangzhou.vivid2D.render.awt.manager.ThumbnailManager;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
public class LayerCellRenderer extends JPanel implements ListCellRenderer<ModelPart> {
private static final int THUMBNAIL_WIDTH = 48;
private static final int THUMBNAIL_HEIGHT = 48;
private final JCheckBox visibleBox = new JCheckBox();
private final JLabel nameLabel = new JLabel();
private final JLabel opacityLabel = new JLabel();
private final JLabel thumbnailLabel = new JLabel();
private final ModelLayerPanel layerPanel;
private final ThumbnailManager thumbnailManager;
public LayerCellRenderer(ModelLayerPanel layerPanel, ThumbnailManager thumbnailManager) {
this.layerPanel = layerPanel;
this.thumbnailManager = thumbnailManager;
initComponents();
}
private void initComponents() {
setLayout(new BorderLayout(6, 6));
// 左侧:缩略图
thumbnailLabel.setPreferredSize(new Dimension(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT));
thumbnailLabel.setOpaque(true);
thumbnailLabel.setBackground(new Color(60, 60, 60));
thumbnailLabel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
// 中间:可见性复选框和名称
JPanel centerPanel = new JPanel(new BorderLayout(4, 0));
centerPanel.setOpaque(false);
JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
leftPanel.setOpaque(false);
visibleBox.setOpaque(false);
leftPanel.add(visibleBox);
leftPanel.add(nameLabel);
centerPanel.add(leftPanel, BorderLayout.CENTER);
centerPanel.add(opacityLabel, BorderLayout.EAST);
add(thumbnailLabel, BorderLayout.WEST);
add(centerPanel, BorderLayout.CENTER);
}
public void attachMouseListener(JList<ModelPart> layerList, javax.swing.ListModel<ModelPart> listModel) {
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
int idx = layerList.locationToIndex(e.getPoint());
if (idx >= 0) {
ModelPart part = listModel.getElementAt(idx);
Rectangle cbBounds = visibleBox.getBounds();
// 调整点击区域检测,考虑缩略图的存在
cbBounds.x += thumbnailLabel.getWidth() + 6; // 缩略图宽度 + 间距
if (cbBounds.contains(e.getPoint())) {
boolean newVis = !part.isVisible();
part.setVisible(newVis);
if (layerPanel.getModel() != null) {
layerPanel.getModel().markNeedsUpdate();
}
layerPanel.reloadFromModel();
layerPanel.refreshCurrentThumbnail();
} else {
layerList.setSelectedIndex(idx);
}
}
}
});
}
@Override
public Component getListCellRendererComponent(JList<? extends ModelPart> list, ModelPart value,
int index, boolean isSelected, boolean cellHasFocus) {
nameLabel.setText(value.getName());
opacityLabel.setText(((int) (value.getOpacity() * 100)) + "%");
visibleBox.setSelected(value.isVisible());
// 设置缩略图
BufferedImage thumbnail = thumbnailManager.getThumbnail(value);
if (thumbnail != null) {
thumbnailLabel.setIcon(new ImageIcon(thumbnail));
} else {
thumbnailLabel.setIcon(null);
// 如果没有缩略图,生成一个
SwingUtilities.invokeLater(() -> {
thumbnailManager.generateThumbnail(value);
list.repaint();
});
}
if (isSelected) {
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
nameLabel.setForeground(list.getSelectionForeground());
opacityLabel.setForeground(list.getSelectionForeground());
thumbnailLabel.setBorder(BorderFactory.createLineBorder(list.getSelectionForeground(), 2));
} else {
setBackground(list.getBackground());
setForeground(list.getForeground());
nameLabel.setForeground(list.getForeground());
opacityLabel.setForeground(list.getForeground());
thumbnailLabel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
}
setOpaque(true);
setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
return this;
}
}

View File

@@ -0,0 +1,68 @@
package com.chuangzhou.vivid2D.render.awt.util.renderer;
import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel;
import javax.swing.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
public class LayerReorderTransferHandler extends TransferHandler {
private final ModelLayerPanel layerPanel;
public LayerReorderTransferHandler(ModelLayerPanel layerPanel) {
this.layerPanel = layerPanel;
}
@Override
protected Transferable createTransferable(JComponent c) {
if (!(c instanceof JList)) return null;
JList<?> list = (JList<?>) c;
int src = list.getSelectedIndex();
if (src < 0) return null;
return new StringSelection(Integer.toString(src));
}
@Override
public int getSourceActions(JComponent c) {
return MOVE;
}
@Override
public boolean canImport(TransferSupport support) {
return support.isDrop() && support.isDataFlavorSupported(DataFlavor.stringFlavor);
}
@Override
public boolean importData(TransferSupport support) {
if (!canImport(support)) return false;
try {
if (!(support.getComponent() instanceof JList)) return false;
JList.DropLocation dl = (JList.DropLocation) support.getDropLocation();
int dropIndex = dl.getIndex();
String s = (String) support.getTransferable().getTransferData(DataFlavor.stringFlavor);
int srcIdx = Integer.parseInt(s);
if (srcIdx == dropIndex || srcIdx + 1 == dropIndex) return false;
layerPanel.performVisualReorder(srcIdx, dropIndex);
layerPanel.endDragOperation();
return true;
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
@Override
protected void exportDone(JComponent source, Transferable data, int action) {
if (action == TransferHandler.NONE) {
layerPanel.endDragOperation();
}
super.exportDone(source, data, action);
}
}

View File

@@ -209,8 +209,12 @@ public class Model2D {
return partMap.get(name);
}
public Map<String, ModelPart> getPartMap() {
return partMap;
}
public List<ModelPart> getParts() {
return Collections.unmodifiableList(parts);
return parts;
}
// ==================== 参数管理 ====================

View File

@@ -1,10 +1,7 @@
package com.chuangzhou.vivid2D.render.model;
import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal;
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
import com.chuangzhou.vivid2D.render.model.util.Deformer;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
import com.chuangzhou.vivid2D.render.model.util.PuppetPin;
import com.chuangzhou.vivid2D.render.model.util.*;
import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils;
import org.joml.Matrix3f;
import org.joml.Vector2f;
@@ -1595,6 +1592,7 @@ public class ModelPart {
mesh.setPivot(worldPivot.x, worldPivot.y);
}
updateMeshVertices();
triggerEvent("position");
}
@@ -1626,56 +1624,72 @@ public class ModelPart {
*/
private void updateMeshVertices(Mesh2D mesh) {
if (mesh == null) return;
// 获取原始顶点数据(局部坐标)
float[] originalVertices = mesh.getOriginalVertices();
if (originalVertices == null || originalVertices.length == 0) {
logger.warn("网格 {} 没有原始顶点数据,无法更新变换", mesh.getName());
return;
}
// 确保世界变换是最新的
// 确保 worldTransform 是最新的
if (transformDirty) {
updateLocalTransform();
recomputeWorldTransformRecursive();
}
int vertexCount = originalVertices.length / 2;
// 应用当前世界变换到每个顶点 - 添加边界检查
for (int i = 0; i < vertexCount; i++) {
if (i * 2 + 1 >= originalVertices.length) {
logger.warn("顶点索引 {} 超出原始顶点数组范围", i);
continue;
}
Vector2f localPoint = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]);
Vector2f worldPoint = Matrix3fUtils.transformPoint(worldTransform, localPoint);
// 检查目标索引是否有效
if (i < mesh.getVertexCount()) {
mesh.setVertex(i, worldPoint.x, worldPoint.y);
} else {
logger.warn("顶点索引 {} 超出网格顶点范围 (总顶点数: {})", i, mesh.getVertexCount());
}
}
// 同步 mesh 的原始局部 pivot -> 当前世界 pivot
// 1) 让 mesh 自己把局部顶点一次性转换成渲染缓存(世界坐标)
mesh.syncRenderVerticesFromLocal(this.worldTransform);
// 2) 同步 pivot不改原始局部数据
try {
Vector2f origPivot = mesh.getOriginalPivot();
Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, origPivot);
mesh.setPivot(worldPivot.x, worldPivot.y);
if (origPivot != null) {
Vector2f worldPivot = Matrix3fUtils.transformPoint(this.worldTransform, origPivot);
mesh.setPivot(worldPivot.x, worldPivot.y);
}
} catch (Exception e) {
logger.warn("更新网格pivot时出错: {}", e.getMessage());
}
updatePuppetPinsPosition(mesh);
// 标记网格需要更新
// 3) 更新木偶控制点显示位置(仅 display/world pos
//updatePuppetPinsPosition(mesh);
// 4) 更新二级顶点的 worldPosition 缓存(仅 display/world pos不修改局部变形数据
updateSecondaryVerticesWorldPosition(mesh);
// 5) 标记 mesh 需要重新渲染(渲染器应使用 mesh.getVerticesForUpload() 来上传 VBO
mesh.markDirty();
mesh.setBakedToWorld(true);
}
/**
* 更新二级顶点的原始局部位置与当前局部位置(当 ModelPart 的 worldTransform 改变时调用)
* 保证 SecondaryVertex 在变换后仍然用局部坐标表示(用于变形计算),同时更新 worldPosition 缓存用于显示。
*/
private void updateSecondaryVerticesWorldPosition(Mesh2D mesh) {
if (mesh == null) return;
List<SecondaryVertex> secondaryVertices = mesh.getSecondaryVertices();
if (secondaryVertices == null || secondaryVertices.isEmpty()) return;
if (transformDirty) {
updateLocalTransform();
recomputeWorldTransformRecursive();
}
boolean hasMirror = hasMirrorTransform(this.worldTransform);
for (SecondaryVertex vertex : secondaryVertices) {
Vector2f localPos = vertex.getPosition();
Vector2f adjustedPos = localPos;
if (hasMirror) {
adjustedPos = new Vector2f(-localPos.x, localPos.y);
}
Vector2f worldPos = Matrix3fUtils.transformPoint(this.worldTransform, adjustedPos);
vertex.setWorldPosition(worldPos);
vertex.setRenderPosition(worldPos.x, worldPos.y);
}
logger.debug("更新了 {} 个二级顶点的位置(处理镜像:{}",
secondaryVertices.size(), hasMirror);
}
/**
* 检查变换矩阵是否包含镜像(负缩放)
*/
private boolean hasMirrorTransform(Matrix3f transform) {
// 检查X轴缩放因子的符号
float scaleX = (float)Math.sqrt(transform.m00 * transform.m00 + transform.m10 * transform.m10);
// 通过行列式检查镜像
float determinant = transform.m00 * transform.m11 - transform.m01 * transform.m10;
return determinant < 0;
}
/**
* 更新木偶控制点的位置
*/
@@ -1733,12 +1747,8 @@ public class ModelPart {
Vector2f movedWorldPivot = new Vector2f(oldWorldPivot.x + dx, oldWorldPivot.y + dy);
// 将位移后的世界 pivot 逆变换回新的局部坐标系(即新的 originalPivot
Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, movedWorldPivot);
mesh.setOriginalPivot(newLocalOriginalPivot);
mesh.setPivot(movedWorldPivot.x, movedWorldPivot.y);
// ==================== 新增:同步更新木偶控制点的原始位置 ====================
updatePuppetPinsOriginalPosition(mesh, oldWorldTransform, dx, dy);
}
// 更新网格顶点位置
@@ -1805,7 +1815,7 @@ public class ModelPart {
return;
}
// 原有单选择辑
// 原有单选择辑 - 修复:确保网格顶点被更新
Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform);
this.rotation = radians;
markTransformDirty();
@@ -1818,8 +1828,9 @@ public class ModelPart {
mesh.setOriginalPivot(newLocalOriginalPivot);
mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot));
// ==================== 新增:同步更新木偶控制点的原始位置 ====================
// 更新木偶控制点和二级顶点
updatePuppetPinsOriginalPositionForTransform(mesh, oldWorldTransform);
updateSecondaryVerticesWorldPosition(mesh);
}
updateMeshVertices();
@@ -1831,9 +1842,16 @@ public class ModelPart {
*/
public void rotate(float deltaRadians) {
this.rotation += deltaRadians;
Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform);
markTransformDirty();
updateLocalTransform();
recomputeWorldTransformRecursive();
for (Mesh2D mesh : meshes) {
Vector2f oldWorldPivot = Matrix3fUtils.transformPoint(oldWorldTransform, mesh.getOriginalPivot());
Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot);
mesh.setOriginalPivot(newLocalOriginalPivot);
mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot));
}
updateMeshVertices();
triggerEvent("rotation");
}
@@ -1851,7 +1869,7 @@ public class ModelPart {
return;
}
// 原有单选择辑
// 原有单选择辑 - 修复:确保网格顶点被更新
Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform);
this.scaleX = sx;
this.scaleY = sy;
@@ -1866,8 +1884,9 @@ public class ModelPart {
mesh.setOriginalPivot(newLocalOriginalPivot);
mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot));
// ==================== 新增:同步更新木偶控制点的原始位置 ====================
// 更新木偶控制点和二级顶点
updatePuppetPinsOriginalPositionForTransform(mesh, oldWorldTransform);
updateSecondaryVerticesWorldPosition(mesh);
}
updateMeshVertices();
@@ -1918,6 +1937,7 @@ public class ModelPart {
mesh.setOriginalPivot(newLocalOriginalPivot);
// 同时更新 mesh 的当前 pivot 到新的世界坐标
mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot));
updateSecondaryVerticesWorldPosition(mesh);
}
updateMeshVertices();
@@ -1968,29 +1988,33 @@ public class ModelPart {
public void addMesh(Mesh2D mesh) {
if (mesh == null) return;
// 确保拷贝保留原始的纹理引用copy() 已处理)
//mesh.setTexture(mesh.getTexture());
mesh.setModelPart(this);
// 确保本节点的 worldTransform 是最新的
recomputeWorldTransformRecursive();
// 保存拷贝的原始(局部顶点供后续重算 world 顶点使用
float[] originalVertices = mesh.getVertices().clone();
mesh.setOriginalVertices(originalVertices);
// 把 originalPivot 保存在 mesh 中setMeshData 已经初始化 originalPivot
// 将每个顶点从本地空间变换到世界空间(烘焙到 world
// 1. 保存局部顶点到 originalVertices
float[] localVertices = mesh.getVertices().clone();
mesh.setOriginalVertices(localVertices);
// 2. 确保 renderVertices 数组已初始化
// (您需要 Mesh2D.java 中有这个方法)
// mesh.ensureRenderVerticesInitialized();
// 3. 计算世界坐标并写入 *renderVertices*,而不是
int vc = mesh.getVertexCount();
for (int i = 0; i < vc; i++) {
Vector2f local = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]);
Vector2f local = new Vector2f(localVertices[i * 2], localVertices[i * 2 + 1]);
Vector2f worldPt = Matrix3fUtils.transformPoint(this.worldTransform, local);
mesh.setVertex(i, worldPt.x, worldPt.y);
// 错误mesh.setVertex(i, worldPt.x, worldPt.y);
// 正确:
mesh.setRenderVertex(i, worldPt.x, worldPt.y); // 假设 setRenderVertex 存在
}
// 同步 originalPivot -> world pivot如果 originalPivot 有意义)
// 4. 同步 pivot
try {
Vector2f origPivot = mesh.getOriginalPivot();
Vector2f worldPivot = Matrix3fUtils.transformPoint(this.worldTransform, origPivot);
mesh.setPivot(worldPivot.x, worldPivot.y);
mesh.setPivot(worldPivot.x, worldPivot.y); // 现在这个会成功因为步骤1的修复
} catch (Exception ignored) {
}
@@ -2193,19 +2217,27 @@ public class ModelPart {
* 获取世界空间中的包围盒
*/
public BoundingBox getWorldBounds() {
if (boundsDirty) {
updateBounds();
BoundingBox worldBounds = new BoundingBox();
for (Mesh2D mesh : meshes) {
// 确保网格的世界边界是最新的
BoundingBox meshWorldBounds = mesh.getWorldBounds();
if (meshWorldBounds != null && meshWorldBounds.isValid()) {
worldBounds.expand(meshWorldBounds);
}
}
BoundingBox worldBounds = new BoundingBox();
for (Mesh2D mesh : meshes) {
BoundingBox meshBounds = mesh.getBounds();
if (meshBounds != null) {
// 变换到世界空间
Vector2f min = localToWorld(new Vector2f(meshBounds.getMinX(), meshBounds.getMinY()));
Vector2f max = localToWorld(new Vector2f(meshBounds.getMaxX(), meshBounds.getMaxY()));
worldBounds.expand(min.x, min.y);
worldBounds.expand(max.x, max.y);
// 如果没有有效边界,使用局部边界作为备选
if (!worldBounds.isValid()) {
for (Mesh2D mesh : meshes) {
BoundingBox meshBounds = mesh.getBounds();
if (meshBounds != null && meshBounds.isValid()) {
// 变换到世界空间
Vector2f min = localToWorld(new Vector2f(meshBounds.getMinX(), meshBounds.getMinY()));
Vector2f max = localToWorld(new Vector2f(meshBounds.getMaxX(), meshBounds.getMaxY()));
worldBounds.expand(min.x, min.y);
worldBounds.expand(max.x, max.y);
}
}
}

View File

@@ -22,6 +22,10 @@ public class SecondaryVertex {
private float minControlRadius = 4.0f; // 最小允许半径
private float maxControlRadius = 200.0f; // 最大允许半径
private boolean fixedRadius = false; // 是否锁定半径(固定区域)
transient Vector2f worldPosition = new Vector2f();
// 【新增字段】用于存储渲染时的世界坐标,通常由 ModelPart 的世界变换计算而来
transient Vector2f renderPosition = new Vector2f();
public SecondaryVertex(float x, float y, float u, float v) {
this.position = new Vector2f(x, y);
@@ -43,6 +47,35 @@ public class SecondaryVertex {
return new Vector2f(originalPosition);
}
public Vector2f getWorldPosition() {
return new Vector2f(worldPosition);
}
public void setWorldPosition(float x, float y) {
this.worldPosition.set(x, y);
}
public void setWorldPosition(Vector2f p) {
if (p == null) return;
this.worldPosition.set(p);
}
// 【新增 Getter】
public Vector2f getRenderPosition() {
return new Vector2f(renderPosition);
}
// 【新增 Setter】
public void setRenderPosition(float x, float y) {
this.renderPosition.set(x, y);
}
// 【新增 Setter】
public void setRenderPosition(Vector2f p) {
if (p == null) return;
this.renderPosition.set(p);
}
public Vector2f getUV() {
return new Vector2f(uv);
}
@@ -162,4 +195,4 @@ public class SecondaryVertex {
return String.format("SecondaryVertex{id=%d, position=(%.2f, %.2f), uv=(%.2f, %.2f), pinned=%s, locked=%s}",
id, position.x, position.y, uv.x, uv.y, pinned, locked);
}
}
}

View File

@@ -19,7 +19,7 @@ public class AI3Test {
Set<String> faceLabels = Set.of("foreground");
wrapper.segmentAndSave(
Paths.get("C:\\Users\\Administrator\\Desktop\\b_7a8349adece17d1e4bebd20cb2387cf6.jpg").toFile(),
Paths.get("C:\\Users\\Administrator\\Desktop\\b_e15c587fab8a7291740d44e4ce57599f.jpg").toFile(),
faceLabels,
Paths.get("C:\\models\\out")
);

View File

@@ -5,6 +5,8 @@ import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
import com.chuangzhou.vivid2D.render.awt.TransformPanel;
import com.chuangzhou.vivid2D.render.model.Model2D;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import com.formdev.flatlaf.themes.FlatMacDarkLaf;
import com.formdev.flatlaf.themes.FlatMacLightLaf;
import javax.swing.*;
import java.awt.*;
@@ -20,6 +22,12 @@ import java.util.List;
public class ModelLayerPanelTest {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
//LookAndFeel defaultLaf = isDarkMode ? : new FlatMacLightLaf();
try {
UIManager.setLookAndFeel(new FlatMacDarkLaf());
} catch (UnsupportedLookAndFeelException e) {
throw new RuntimeException(e);
}
System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8));
// 创建示例模型并添加图层