feat(render): 实现图层管理和渲染优化功能- 新增 LayerCellRenderer 类,用于渲染模型图层列表,支持可见性切换和缩略图显示- 添加 LayerOperationManager 类,提供图层的增删改查和视觉顺序调整功能
- 实现 LayerReorderTransferHandler 类,支持通过拖拽方式重新排列图层顺序- 优化 Mesh2D 类,引入 renderVertices 渲染缓存机制,提升渲染性能 - 完善二级顶点系统,增强网格变形算法,修复顶点移动和平移相关问题 - 改进三角分配变形算法,增加 pinned 控制点支持和整体位移校正 - 更新 GLContextManager任务队列处理逻辑,增加超时和中断处理机制- 修正模型包装器文档注释格式,提高代码可读性
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
public class ModelAIPanel {
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// ==================== 参数管理 ====================
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
// 创建示例模型并添加图层
|
||||
|
||||
Reference in New Issue
Block a user