From b3c50ca7940926b30f6ae18104a1cebcc48ca86e Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Fri, 17 Oct 2025 21:28:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(render):=20=E6=B7=BB=E5=8A=A0=E7=BD=91?= =?UTF-8?q?=E6=A0=BC=E4=B8=AD=E5=BF=83=E7=82=B9=E5=92=8C=E6=97=8B=E8=BD=AC?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 Mesh2D 类添加 pivot 属性及对应的 getter/setter 方法 - 实现中心点和旋转手柄的可视化绘制逻辑 - 在 ModelRenderPanel 中新增旋转和移动中心点的交互模式 - 支持通过拖拽调整网格的中心点位置- 支持围绕自定义中心点进行旋转操作 - 更新 Mesh2D 的 copy、equals 和 hashCode 方法以包含 pivot 信息-优化选中网格的显示效果,添加多层边框和辅助标记 -修复 ModelPart 中设置中心点时的顶点坐标计算问题 (注意是测试版) --- .../vivid2D/render/awt/ModelLayerPanel.java | 1 + .../vivid2D/render/awt/ModelRenderPanel.java | 148 ++++++++++++-- .../vivid2D/render/model/ModelPart.java | 14 +- .../vivid2D/render/model/util/Mesh2D.java | 185 +++++++++++++++--- 4 files changed, 294 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java index afcc738..ff30363 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java @@ -265,6 +265,7 @@ public class ModelLayerPanel extends JPanel { // 先创建部件与 Mesh(基于图片尺寸) ModelPart part = model.createPart(name); + part.setPivot(0,0); Mesh2D mesh = createQuadForImage(img, name + "_mesh"); part.addMesh(mesh); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java index 3da7f49..059ce74 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java @@ -79,7 +79,9 @@ public class ModelRenderPanel extends JPanel { RESIZE_TOP_LEFT, // 调整左上角 RESIZE_TOP_RIGHT, // 调整右上角 RESIZE_BOTTOM_LEFT, // 调整左下角 - RESIZE_BOTTOM_RIGHT // 调整右下角 + RESIZE_BOTTOM_RIGHT, // 调整右下角 + ROTATE, // 新增:旋转 + MOVE_PIVOT // 新增:移动中心点 } // 新增:拖拽相关字段 @@ -102,7 +104,10 @@ public class ModelRenderPanel extends JPanel { private static final float ZOOM_MIN = 0.1f; private static final float ZOOM_MAX = 8.0f; private volatile boolean shiftDuringDrag = false; - + private volatile float rotationStartAngle = 0.0f; + private volatile float partInitialRotation = 0.0f; + private volatile Vector2f rotationCenter = new Vector2f(); + private static final float ROTATION_HANDLE_DISTANCE = 30.0f; /** * 构造函数:使用模型路径 */ @@ -298,7 +303,42 @@ public class ModelRenderPanel extends JPanel { // 首先检查是否点击了选择框的调整手柄 DragMode dragMode = checkResizeHandleHit(modelX, modelY); - if (dragMode != DragMode.NONE && selectedMesh != null) { + if (dragMode == DragMode.ROTATE && selectedMesh != null) { + // 开始旋转 + currentDragMode = DragMode.ROTATE; + dragStartX = modelX; + dragStartY = modelY; + + // 获取边界框和中心点 + BoundingBox bounds = selectedMesh.getBounds(); + rotationCenter.set((bounds.getMinX() + bounds.getMaxX()) / 2.0f, + (bounds.getMinY() + bounds.getMaxY()) / 2.0f); + + // 计算初始角度 + rotationStartAngle = (float) Math.atan2(dragStartY - rotationCenter.y, + dragStartX - rotationCenter.x); + + // 记录部件的初始旋转 + ModelPart selPart = findPartByMesh(selectedMesh); + if (selPart != null) { + partInitialRotation = selPart.getRotation(); + } + + logger.info("开始旋转,中心点: ({}, {})", rotationCenter.x, rotationCenter.y); + + }else if (dragMode == DragMode.MOVE_PIVOT && selectedMesh != null) { + // 开始移动中心点 + currentDragMode = DragMode.MOVE_PIVOT; + dragStartX = modelX; + dragStartY = modelY; + + // 记录初始中心点位置 + BoundingBox bounds = selectedMesh.getBounds(); + rotationCenter.set((bounds.getMinX() + bounds.getMaxX()) / 2.0f, + (bounds.getMinY() + bounds.getMaxY()) / 2.0f); + + logger.info("开始移动中心点"); + } else if (dragMode != DragMode.NONE && selectedMesh != null) { // 开始调整大小 currentDragMode = dragMode; dragStartX = modelX; // 记录拖拽起始位置 @@ -363,13 +403,25 @@ public class ModelRenderPanel extends JPanel { float maxX = bounds.getMaxX(); float maxY = bounds.getMaxY(); + // 使用 Mesh2D 的实际中心点,而不是边界框中心 + Vector2f actualPivot = selectedMesh.getPivot(); + float centerX = actualPivot.x; + float centerY = actualPivot.y; + // 动态计算检测阈值,基于面板缩放比例 float scaleFactor = calculateScaleFactor(); float borderThickness = BORDER_THICKNESS / scaleFactor; float cornerSize = CORNER_SIZE / scaleFactor; - //logger.info("检测阈值 - 缩放因子: {}, 边框: {}, 角点: {}", - // scaleFactor, borderThickness, cornerSize); + // 首先检查是否点击了中心点(移动中心点) + if (isPointInCenterHandle(modelX, modelY, centerX, centerY, cornerSize)) { + return DragMode.MOVE_PIVOT; + } + + // 检查是否点击了旋转手柄 + if (isPointInRotationHandle(modelX, modelY, centerX, centerY, minY, cornerSize)) { + return DragMode.ROTATE; + } // 扩展边界以包含调整手柄区域 float expandedMinX = minX - borderThickness; @@ -402,6 +454,24 @@ public class ModelRenderPanel extends JPanel { return DragMode.NONE; } + /** + * 检查点是否在中心点区域内 + */ + private boolean isPointInCenterHandle(float x, float y, float centerX, float centerY, float handleSize) { + return Math.abs(x - centerX) <= handleSize && Math.abs(y - centerY) <= handleSize; + } + + /** + * 检查点是否在旋转手柄区域内 + */ + private boolean isPointInRotationHandle(float x, float y, float centerX, float centerY, float topY, float handleSize) { + // 旋转手柄位于边界框上方一定距离 + float rotationHandleY = topY - ROTATION_HANDLE_DISTANCE / calculateScaleFactor(); + float rotationHandleX = centerX; + + return Math.abs(x - rotationHandleX) <= handleSize && Math.abs(y - rotationHandleY) <= handleSize; + } + /** * 计算当前缩放因子(模型单位与屏幕像素的比例) */ @@ -455,12 +525,20 @@ public class ModelRenderPanel extends JPanel { float modelX = modelCoords[0]; float modelY = modelCoords[1]; - if (currentDragMode == DragMode.MOVE) { - // 原有的移动逻辑 - handleMoveDrag(modelX, modelY); - } else { - // 新的调整大小逻辑 - handleResizeDrag(modelX, modelY); + switch (currentDragMode) { + case MOVE: + handleMoveDrag(modelX, modelY); + break; + case ROTATE: + handleRotateDrag(modelX, modelY); + break; + case MOVE_PIVOT: + handleMovePivotDrag(modelX, modelY); + break; + default: + // 调整大小逻辑 + handleResizeDrag(modelX, modelY); + break; } } catch (Exception ex) { @@ -469,6 +547,54 @@ public class ModelRenderPanel extends JPanel { }); } + /** + * 处理旋转拖拽 + */ + private void handleRotateDrag(float modelX, float modelY) { + if (selectedMesh == null) return; + + ModelPart selectedPart = findPartByMesh(selectedMesh); + if (selectedPart == null) return; + + // 计算当前角度 + float currentAngle = (float) Math.atan2(modelY - rotationCenter.y, + modelX - rotationCenter.x); + + // 计算旋转增量 + float deltaAngle = currentAngle - rotationStartAngle; + + // 应用旋转(基于初始旋转加上增量) + float newRotation = partInitialRotation + deltaAngle; + + // 如果按住Shift键,以15度为步长进行约束旋转 + if (shiftPressed || shiftDuringDrag) { + float constraintStep = (float) (Math.PI / 12); // 15度 + newRotation = Math.round(newRotation / constraintStep) * constraintStep; + } + + selectedPart.setRotation(newRotation); + + logger.debug("旋转角度: {} 度", Math.toDegrees(newRotation)); + } + + /** + * 处理移动中心点拖拽 + */ + private void handleMovePivotDrag(float modelX, float modelY) { + if (selectedMesh == null) return; + ModelPart selectedPart = findPartByMesh(selectedMesh); + if (selectedPart == null) return; + float deltaX = modelX - dragStartX; + float deltaY = modelY - dragStartY; + Vector2f currentPivot = selectedPart.getPivot(); + float newPivotX = currentPivot.x + deltaX; + float newPivotY = currentPivot.y + deltaY; + selectedPart.setPivot(newPivotX, newPivotY); + rotationCenter.set(newPivotX, newPivotY); + dragStartX = modelX; + dragStartY = modelY; + } + /** * 处理移动拖拽 */ diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java index 3768e85..0a94ab1 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -645,7 +645,6 @@ public class ModelPart { // 保存拷贝的原始(局部)顶点供后续重算 world 顶点使用 float[] originalVertices = m.getVertices().clone(); m.setOriginalVertices(originalVertices); - logger.info("addMesh: texture={} for mesh={}", m.getTexture(), m.getName()); // 保证 UV 不被篡改(通常 copy() 已经处理) // float[] uvs = m.getUVs(); // 如果需要可以在此处检查 @@ -663,16 +662,11 @@ public class ModelPart { // 确保 GPU 数据在下一次绘制时会被上传(如果当前在渲染线程,也可以直接 uploadToGPU) m.markDirty(); - // 如果你确定此处正在 GL 渲染线程并且想要立刻创建 VAO/VBO(可取消下面注释) - // m.uploadToGPU(); - // 将拷贝加入到本部件 meshes.add(m); boundsDirty = true; } - - /** * 设置中心点 */ @@ -686,17 +680,11 @@ public class ModelPart { } pivotInitialized = true; } - float dx = x - pivot.x; - float dy = y - pivot.y; pivot.set(x, y); for (Mesh2D mesh : meshes) { - for (int i = 0; i < mesh.getVertexCount(); i++) { - Vector2f v = mesh.getVertex(i); - v.sub(dx, dy); - mesh.setVertex(i, v.x, v.y); - } + mesh.setPivot(x, y); } markTransformDirty(); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java index 70f86e7..0de7496 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java @@ -50,6 +50,7 @@ public class Mesh2D { private boolean boundsDirty = true; private boolean bakedToWorld = false; private volatile boolean selected = false; + private Vector2f pivot = new Vector2f(0, 0); // ==================== 常量 ==================== public static final int POINTS = 0; @@ -59,6 +60,7 @@ public class Mesh2D { public static final int TRIANGLE_STRIP = 4; public static final int TRIANGLE_FAN = 5; + private static final float ROTATION_HANDLE_DISTANCE = 30.0f; // ==================== 构造器 ==================== public Mesh2D() { @@ -102,6 +104,31 @@ public class Mesh2D { markDirty(); } + /** + * 设置中心点 + */ + public void setPivot(float x, float y) { + this.pivot.set(x, y); + } + + public void setPivot(Vector2f pivot) { + this.pivot.set(pivot); + } + + /** + * 获取中心点 + */ + public Vector2f getPivot() { + return new Vector2f(pivot); + } + + /** + * 移动中心点 + */ + public void movePivot(float dx, float dy) { + this.pivot.add(dx, dy); + } + /** * 创建矩形网格 */ @@ -576,6 +603,7 @@ public class Mesh2D { float expand = 4.0f * 2.0f; + // 第1层:外发光边框 bb.begin(RenderSystem.GL_LINE_LOOP, 4); bb.setColor(new Vector4f(0.0f, 1.0f, 1.0f, 0.4f)); @@ -587,7 +615,7 @@ public class Mesh2D { // 第2层:主边框(实心粗边框)- 使用明亮的青色 bb.begin(RenderSystem.GL_LINE_LOOP, 4); - bb.setColor(new Vector4f(0.0f, 1.0f, 1.0f, 1.0f)); // 青色,100%不透明 + bb.setColor(new Vector4f(0.0f, 1.0f, 1.0f, 1.0f)); float mainExpand = 1.0f; bb.vertex(minX - mainExpand, minY - mainExpand, 0.0f, 0.0f); @@ -598,7 +626,7 @@ public class Mesh2D { // 第3层:内边框 - 使用白色增加对比度 bb.begin(RenderSystem.GL_LINE_LOOP, 4); - bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); // 白色,100%不透明 + bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); bb.vertex(minX, minY, 0.0f, 0.0f); bb.vertex(maxX, minY, 0.0f, 0.0f); @@ -608,6 +636,97 @@ public class Mesh2D { // 第4层:绘制角点标记和边线 drawResizeHandles(bb, minX, minY, maxX, maxY, CORNER_SIZE, BORDER_THICKNESS); + + // 新增:绘制中心点 + drawCenterPoint(bb, minX, minY, maxX, maxY); + drawRotationHandle(bb, minX, minY, maxX, maxY); + } + + private void drawCenterPoint(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { + // 使用 Mesh2D 的 pivot 作为中心点位置 + float centerX = pivot.x; + float centerY = pivot.y; + + float pointSize = 6.0f; // 中心点大小 + + Vector4f centerColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); // 红色中心点 + + // 绘制中心点(十字形) + bb.begin(GL11.GL_LINES, 4); // 使用 RenderSystem 常量 + bb.setColor(centerColor); + + // 水平线 + bb.vertex(centerX - pointSize, centerY, 0.0f, 0.0f); + bb.vertex(centerX + pointSize, centerY, 0.0f, 0.0f); + + // 垂直线 + bb.vertex(centerX, centerY - pointSize, 0.0f, 0.0f); + bb.vertex(centerX, centerY + pointSize, 0.0f, 0.0f); + + bb.endImmediate(); + + // 绘制中心点圆圈 + bb.begin(RenderSystem.GL_LINE_LOOP, 12); + bb.setColor(centerColor); + + float radius = pointSize * 0.8f; + for (int i = 0; i < 12; i++) { + float angle = (float) (i * 2 * Math.PI / 12); + float x = centerX + (float) Math.cos(angle) * radius; + float y = centerY + (float) Math.sin(angle) * radius; + bb.vertex(x, y, 0.0f, 0.0f); + } + bb.endImmediate(); + + logger.trace("绘制中心点: ({}, {})", centerX, centerY); + } + + /** + * 绘制旋转手柄 + */ + private void drawRotationHandle(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { + // 使用 Mesh2D 的 pivot 作为中心点位置 + float centerX = pivot.x; + float centerY = pivot.y; + + // 旋转手柄位置(在边界框上方) + float rotationHandleY = minY - ROTATION_HANDLE_DISTANCE; + float rotationHandleX = centerX; + + Vector4f rotationColor = new Vector4f(0.0f, 1.0f, 0.0f, 1.0f); // 绿色旋转手柄 + + // 绘制连接线(从中心点到旋转手柄) + bb.begin(GL11.GL_LINES, 2); + bb.setColor(rotationColor); + bb.vertex(centerX, minY, 0.0f, 0.0f); + bb.vertex(rotationHandleX, rotationHandleY, 0.0f, 0.0f); + bb.endImmediate(); + + // 绘制旋转手柄(圆圈) + float handleRadius = 6.0f; + bb.begin(RenderSystem.GL_LINE_LOOP, 12); + bb.setColor(rotationColor); + + for (int i = 0; i < 12; i++) { + float angle = (float) (i * 2 * Math.PI / 12); + float x = rotationHandleX + (float) Math.cos(angle) * handleRadius; + float y = rotationHandleY + (float) Math.sin(angle) * handleRadius; + bb.vertex(x, y, 0.0f, 0.0f); + } + bb.endImmediate(); + + // 绘制旋转箭头 + bb.begin(GL11.GL_LINES, 4); + bb.setColor(rotationColor); + + // 箭头线 + float arrowSize = 4.0f; + bb.vertex(rotationHandleX - arrowSize, rotationHandleY - arrowSize, 0.0f, 0.0f); + bb.vertex(rotationHandleX + arrowSize, rotationHandleY + arrowSize, 0.0f, 0.0f); + + bb.vertex(rotationHandleX + arrowSize, rotationHandleY - arrowSize, 0.0f, 0.0f); + bb.vertex(rotationHandleX - arrowSize, rotationHandleY + arrowSize, 0.0f, 0.0f); + bb.endImmediate(); } /** @@ -807,38 +926,41 @@ public class Mesh2D { /** * 创建网格的深拷贝 */ - public Mesh2D copy() { - Mesh2D copy = new Mesh2D(name + "_copy"); + public Mesh2D copy() { + Mesh2D copy = new Mesh2D(name + "_copy"); - // 深拷贝数组(保证互不影响) - copy.vertices = this.vertices != null ? this.vertices.clone() : new float[0]; - copy.uvs = this.uvs != null ? this.uvs.clone() : new float[0]; - copy.indices = this.indices != null ? this.indices.clone() : new int[0]; + // 深拷贝数组(保证互不影响) + copy.vertices = this.vertices != null ? this.vertices.clone() : new float[0]; + copy.uvs = this.uvs != null ? this.uvs.clone() : new float[0]; + copy.indices = this.indices != null ? this.indices.clone() : new int[0]; - // 保留 originalVertices(如果有),否则把当前 vertices 作为原始数据 - copy.originalVertices = this.originalVertices != null ? this.originalVertices.clone() : copy.vertices.clone(); + // 保留 originalVertices(如果有),否则把当前 vertices 作为原始数据 + copy.originalVertices = this.originalVertices != null ? this.originalVertices.clone() : copy.vertices.clone(); - // 复制渲染/状态字段(保留纹理引用,但重置 GPU 句柄) - copy.texture = this.texture; - copy.visible = this.visible; - copy.drawMode = this.drawMode; - copy.bakedToWorld = this.bakedToWorld; + // 复制中心点 + copy.pivot = new Vector2f(this.pivot); - // 重置 GPU 相关句柄,强制重新 uploadToGPU() 在渲染线程执行 - copy.vaoId = -1; - copy.vboId = -1; - copy.eboId = -1; - copy.indexCount = this.indices != null ? this.indices.length : 0; - copy.uploaded = false; + // 复制渲染/状态字段(保留纹理引用,但重置 GPU 句柄) + copy.texture = this.texture; + copy.visible = this.visible; + copy.drawMode = this.drawMode; + copy.bakedToWorld = this.bakedToWorld; - // 状态标记 - copy.dirty = true; - copy.boundsDirty = true; - copy.bounds = new BoundingBox(); - copy.selected = this.selected; + // 重置 GPU 相关句柄,强制重新 uploadToGPU() 在渲染线程执行 + copy.vaoId = -1; + copy.vboId = -1; + copy.eboId = -1; + copy.indexCount = this.indices != null ? this.indices.length : 0; + copy.uploaded = false; - return copy; - } + // 状态标记 + copy.dirty = true; + copy.boundsDirty = true; + copy.bounds = new BoundingBox(); + copy.selected = this.selected; + + return copy; + } public int getVaoId() { @@ -881,7 +1003,8 @@ public class Mesh2D { Objects.equals(name, mesh2D.name) && Objects.deepEquals(vertices, mesh2D.vertices) && Objects.deepEquals(uvs, mesh2D.uvs) && - Objects.deepEquals(indices, mesh2D.indices); + Objects.deepEquals(indices, mesh2D.indices) && + Objects.equals(pivot, mesh2D.pivot); } @Override @@ -890,6 +1013,7 @@ public class Mesh2D { java.util.Arrays.hashCode(vertices), java.util.Arrays.hashCode(uvs), java.util.Arrays.hashCode(indices), + pivot, visible, drawMode); } @@ -900,6 +1024,8 @@ public class Mesh2D { .append("name='").append(name).append('\'') .append(", vertices=").append(getVertexCount()) .append(", indices=").append(indices.length) + .append(", pivot=(").append(String.format("%.2f", pivot.x)) + .append(", ").append(String.format("%.2f", pivot.y)).append(")") // 新增这行 .append(", visible=").append(visible) .append(", drawMode=").append(getDrawModeString()) .append(", bounds=").append(getBounds()); @@ -920,5 +1046,4 @@ public class Mesh2D { sb.append('}'); return sb.toString(); } - }