From 8a01020cbe4046a73f8895a0d56285be4d070f47 Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Mon, 10 Nov 2025 23:12:19 +0800 Subject: [PATCH] =?UTF-8?q?refactor(render):=E9=87=8D=E6=9E=84=E5=85=B3?= =?UTF-8?q?=E9=94=AE=E5=B8=A7=E6=8F=92=E5=80=BC=E5=99=A8=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=A1=B6=E7=82=B9=E5=8F=98=E5=BD=A2-=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20FrameInterpolator=E4=BB=A5=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=20Mesh2D=20=E7=9A=84=E4=B8=80=E7=BA=A7?= =?UTF-8?q?=E9=A1=B6=E7=82=B9=20-=20=E5=BC=95=E5=85=A5=20deformationVertex?= =?UTF-8?q?=20=E5=8F=82=E6=95=B0=E6=8E=A7=E5=88=B6=E5=B8=A6=20VertexTag.DE?= =?UTF-8?q?FORMATION=20=E6=A0=87=E7=AD=BE=E7=9A=84=E9=A1=B6=E7=82=B9-=20?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=AF=B9=20secondaryVertex=20=E7=9A=84?= =?UTF-8?q?=E6=97=A7=E6=94=AF=E6=8C=81=E5=8F=8A=E7=9B=B8=E5=85=B3=E5=86=97?= =?UTF-8?q?=E4=BD=99=E4=BB=A3=E7=A0=81=20-=20=E7=AE=80=E5=8C=96=E6=8F=92?= =?UTF-8?q?=E5=80=BC=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91=E5=B9=B6=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E8=A7=92=E5=BA=A6=E5=8D=95=E4=BD=8D=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E5=AE=B9=E5=B7=AE=20-=20=E4=BC=98=E5=8C=96=E9=A1=B6=E7=82=B9?= =?UTF-8?q?=E7=9B=AE=E6=A0=87=E8=AE=A1=E7=AE=97=E6=96=B9=E6=B3=95=E5=B9=B6?= =?UTF-8?q?=E9=87=8D=E5=91=BD=E5=90=8D=E4=B8=BA=20DeformationVertexTarget?= =?UTF-8?q?=20-=20=E6=B8=85=E7=90=86=E6=97=A0=E7=94=A8=E7=9A=84=E5=8F=8D?= =?UTF-8?q?=E5=B0=84=E5=9B=9E=E9=80=80=E5=92=8C=E5=AE=89=E5=85=A8=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E6=9C=BA=E5=88=B6-=20=E7=A7=BB=E9=99=A4=20liquify=20?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=9B=B8=E5=85=B3=E7=9A=84=E9=A1=B6=E7=82=B9?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=BF=AB=E6=8D=B7=E9=94=AE=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=20-=20=E5=88=A0=E9=99=A4=E5=B7=B2=E5=BA=9F=E5=BC=83=E7=9A=84?= =?UTF-8?q?=20LiquifyTargetPartRander=20=E7=B1=BB=E6=96=87=E4=BB=B6=20-?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AF=BC=E5=85=A5=E8=AF=AD=E5=8F=A5=E5=B9=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E7=B1=BB=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vivid2D/render/ModelRender.java | 2 +- .../render/MultiSelectionBoxRenderer.java | 468 +- .../render/awt/ModelClickListener.java | 3 +- .../vivid2D/render/awt/ModelLayerPanel.java | 2 +- .../vivid2D/render/awt/ModelRenderPanel.java | 20 +- .../vivid2D/render/awt/ParametersPanel.java | 2 +- .../render/awt/SecondaryVertexPanel.java | 151 - .../render/awt/manager/KeyboardManager.java | 20 - .../render/awt/manager/ThumbnailManager.java | 2 +- .../vivid2D/render/awt/tools/LiquifyTool.java | 393 -- .../awt/tools/PuppetDeformationTool.java | 297 -- .../render/awt/tools/SelectionTool.java | 229 +- .../awt/tools/VertexDeformationTool.java | 890 +--- .../render/awt/util/FrameInterpolator.java | 741 +--- .../render/awt/util/MeshTextureUtil.java | 2 +- .../awt/util/OperationHistoryGlobal.java | 2 +- .../vivid2D/render/awt/util/PSDImporter.java | 4 +- .../vivid2D/render/model/Mesh2D.java | 1843 ++++++++ .../vivid2D/render/model/Model2D.java | 3 - .../vivid2D/render/model/ModelPart.java | 560 +-- .../vivid2D/render/model/data/MeshData.java | 383 +- .../vivid2D/render/model/data/ModelData.java | 1 + .../vivid2D/render/model/data/PartData.java | 2 +- .../model/transform/RotationDeformer.java | 2 +- .../render/model/transform/ScaleDeformer.java | 2 +- .../model/transform/VertexDeformer.java | 2 +- .../render/model/transform/WaveDeformer.java | 2 +- .../vivid2D/render/model/util/Deformer.java | 1 + .../vivid2D/render/model/util/Mesh2D.java | 3845 ----------------- .../vivid2D/render/model/util/PuppetPin.java | 1 + .../render/model/util/SecondaryVertex.java | 222 - .../vivid2D/render/model/util/Vertex.java | 130 + .../vivid2D/render/model/util/VertexList.java | 188 + .../vivid2D/render/model/util/VertexTag.java | 21 + .../util/manager/RanderToolsManager.java | 5 +- .../util/tools/LiquifyTargetPartRander.java | 363 -- .../util/tools/PuppetDeformationRander.java | 281 -- .../util/tools/VertexDeformationRander.java | 559 +-- .../vivid2D/test/ModelRenderLightingTest.java | 2 +- .../vivid2D/test/ModelRenderTest.java | 2 +- .../vivid2D/test/ModelRenderTest2.java | 2 +- .../vivid2D/test/ModelRenderTextureTest.java | 2 +- .../chuangzhou/vivid2D/test/ModelTest.java | 1 + .../chuangzhou/vivid2D/test/ModelTest2.java | 2 +- .../vivid2D/test/TestModelGLPanel.java | 2 +- .../chuangzhou/vivid2D/window/MainWindow.java | 38 +- 46 files changed, 3300 insertions(+), 8395 deletions(-) delete mode 100644 src/main/java/com/chuangzhou/vivid2D/render/awt/SecondaryVertexPanel.java delete mode 100644 src/main/java/com/chuangzhou/vivid2D/render/awt/tools/LiquifyTool.java delete mode 100644 src/main/java/com/chuangzhou/vivid2D/render/awt/tools/PuppetDeformationTool.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java delete mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java delete mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexList.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexTag.java delete mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/LiquifyTargetPartRander.java delete mode 100644 src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/PuppetDeformationRander.java diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java index 2a9462a..df28d05 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java @@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.render; import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.LightSource; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; import com.chuangzhou.vivid2D.render.systems.Camera; import com.chuangzhou.vivid2D.render.systems.RenderSystem; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java b/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java index 4288303..8eb3d37 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java @@ -9,328 +9,328 @@ import org.joml.Vector4f; import org.lwjgl.opengl.GL11; /** - * 现代化选择框渲染器(性能优化版) - * 主要优化点: - * 1) 复用 Tesselator 单例 BufferBuilder,减少频繁的 GPU 资源创建/销毁 - * 2) 批量提交顶点:把同一 primitive(LINES / TRIANGLES / LINE_LOOP)与同一颜色的顶点尽量合并到一次 begin/end - * 3) 手柄使用实心矩形(两三角形)批量绘制,保持美观且高效 - * 4) 增加轻微外发光(透明大边框)和阴影感以达到“现代”外观 - *

- * 注意:本类依赖你工程中已有的 RenderSystem/Tesselator/BufferBuilder/BufferUploader 实现。 + * MultiSelectionBoxRenderer — 修复摄像机缩放导致边框消失的问题(并保持美观) + * + * 要点: + * - drawSelectBox/drawMultiSelectionBox 接收 cameraScale 参数(世界单位 -> 像素的比例) + * - 将需要“屏幕稳定”的厚度与尺寸除以 cameraScale,保证缩放时视觉一致 + * - 兼容不传 cameraScale 的调用(默认 1.0f) */ public class MultiSelectionBoxRenderer { - // 常量定义(视觉可调) - public static final float DEFAULT_CORNER_SIZE = 10.0f; - public static final float DEFAULT_BORDER_THICKNESS = 6.0f; + // -------------------- 配置常量(可调) -------------------- + // 尺寸(以世界坐标为基准,这些会根据 cameraScale 自动调整为屏幕稳定值) + public static final float DEFAULT_CORNER_SIZE = 8.0f; + public static final float DEFAULT_BORDER_THICKNESS = 2.5f; public static final float DEFAULT_DASH_LENGTH = 10.0f; public static final float DEFAULT_GAP_LENGTH = 6.0f; - public static final float ROTATION_HANDLE_DISTANCE = 28.0f; - public static final float HANDLE_ROUNDNESS = 1.5f; // 保留,用于未来改进圆角手柄 - // 颜色(更现代的配色) - public static final Vector4f DASHED_BORDER_COLOR = new Vector4f(1.0f, 0.85f, 0.0f, 1.0f); // 黄色虚线 - public static final Vector4f SOLID_BORDER_COLOR_OUTER = new Vector4f(0.0f, 0.85f, 0.95f, 0.18f); // 轻微外发光 - public static final Vector4f SOLID_BORDER_COLOR_MAIN = new Vector4f(0.0f, 0.92f, 0.94f, 1.0f); // 主边框,青色 - public static final Vector4f SOLID_BORDER_COLOR_INNER = new Vector4f(1.0f, 1.0f, 1.0f, 0.9f); // 内边框,接近白 - public static final Vector4f HANDLE_COLOR = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); // 手柄白 + // 视觉厚度分层(这些是“世界坐标基准值”,实际绘制时会 / cameraScale) + private static final float OUTER_BORDER_THICKNESS = 0.8f; + private static final float MAIN_BORDER_THICKNESS = 0.6f; + private static final float INNER_BORDER_THICKNESS = 0.2f; + + // 手柄与中心点尺寸(世界坐标基准) + private static final float HANDLE_CORNER_SIZE = DEFAULT_CORNER_SIZE; + private static final float HANDLE_MID_SIZE = 2.8f; + private static final float CENTER_LINE_THICKNESS = 1.2f; + private static final float CENTER_RING_RADIUS = 5.0f; + private static final float CENTER_RING_THICKNESS = 1.2f; + + // 颜色(更现代、更清爽) + public static final Vector4f DASHED_BORDER_COLOR = new Vector4f(1.0f, 0.85f, 0.0f, 1.0f); // 黄色虚线 + public static final Vector4f SOLID_BORDER_COLOR_OUTER = new Vector4f(0.0f, 0.85f, 0.95f, 0.12f); // 轻微外发光(弱透明) + public static final Vector4f SOLID_BORDER_COLOR_MAIN = new Vector4f(0.0f, 0.92f, 0.94f, 1.0f); // 主边框(青) + public static final Vector4f SOLID_BORDER_COLOR_INNER = new Vector4f(1.0f, 1.0f, 1.0f, 0.9f); // 内描边(接近白) + public static final Vector4f HANDLE_COLOR = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); // 手柄白 public static final Vector4f MULTI_SELECTION_HANDLE_COLOR = new Vector4f(1.0f, 0.9f, 0.0f, 1.0f); // 黄色手柄 - public static final Vector4f CENTER_POINT_COLOR = new Vector4f(1.0f, 0.2f, 0.2f, 1.0f); // 中心点红 - public static final Vector4f ROTATION_HANDLE_COLOR = new Vector4f(0.14f, 0.95f, 0.3f, 1.0f); // 绿色旋转手柄 - public static final Vector4f SHADOW_COLOR = new Vector4f(0f, 0f, 0f, 0.18f); // 阴影/背板 + public static final Vector4f CENTER_POINT_COLOR = new Vector4f(1.0f, 0.2f, 0.2f, 1.0f); // 中心点红 + + // 小的保护常量 + private static final float MIN_CAMERA_SCALE = 1e-4f; + private static final float MIN_THICKNESS_PIXELS = 0.5f; // 最小像素视觉厚度保护 + + // -------------------- 公共绘制 API(带 cameraScale 参数) -------------------- /** - * 绘制单选状态下的选择框(高效批处理) + * 绘制单选的选择框(推荐:传入 cameraScale) + * + * @param bounds 包围盒(世界坐标) + * @param pivot 旋转中心 / 中心点(世界坐标) + * @param cameraScale 世界单位 -> 像素 比例(通常来自 CameraManagement.calculateScaleFactor()) */ - public static void drawSelectBox(BoundingBox bounds, Vector2f pivot) { - if (!bounds.isValid()) return; + public static void drawSelectBox(BoundingBox bounds, Vector2f pivot, float cameraScale) { + if (bounds == null || !bounds.isValid()) return; + if (cameraScale <= 0f) cameraScale = MIN_CAMERA_SCALE; float minX = bounds.getMinX(); float minY = bounds.getMinY(); float maxX = bounds.getMaxX(); float maxY = bounds.getMaxY(); + // 根据 cameraScale 计算“屏幕稳定”的厚度/尺寸 + float outerThickness = Math.max(MIN_THICKNESS_PIXELS / cameraScale, OUTER_BORDER_THICKNESS / cameraScale); + float mainThickness = Math.max(MIN_THICKNESS_PIXELS / cameraScale, MAIN_BORDER_THICKNESS / cameraScale); + float innerThickness = Math.max(MIN_THICKNESS_PIXELS / cameraScale, INNER_BORDER_THICKNESS / cameraScale); + + float handleCornerSize = Math.max(2.0f / cameraScale, HANDLE_CORNER_SIZE / cameraScale); + float handleMidSize = Math.max(2.0f / cameraScale, HANDLE_MID_SIZE / cameraScale); + + float centerLineThickness = Math.max(1.0f / cameraScale, CENTER_LINE_THICKNESS / cameraScale); + float centerRingThickness = Math.max(1.0f / cameraScale, CENTER_RING_THICKNESS / cameraScale); + float centerRingRadius = Math.max(3.0f / cameraScale, CENTER_RING_RADIUS / cameraScale); + Tesselator tesselator = Tesselator.getInstance(); BufferBuilder bb = tesselator.getBuilder(); RenderSystem.enableBlend(); RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - // 1) 阴影底板(轻微偏移) - bb.begin(RenderSystem.GL_TRIANGLES, 6); - bb.setColor(SHADOW_COLOR); - addFilledQuadTriangles(bb, minX + 4f, minY + 4f, maxX + 4f, maxY + 4f); - tesselator.end(); + // 估算顶点数并开始 TRIANGLES 绘制 + int estimatedQuads = (3 * 4) + 8 + 12; + bb.begin(RenderSystem.GL_TRIANGLES, estimatedQuads * 6); - // 2) 外发光边框(更柔和) - bb.begin(RenderSystem.GL_LINE_LOOP, 4); + // 外层发光边框(较宽、透明) bb.setColor(SOLID_BORDER_COLOR_OUTER); - bb.vertex(minX - 6.0f, minY - 6.0f, 0.0f, 0.0f); - bb.vertex(maxX + 6.0f, minY - 6.0f, 0.0f, 0.0f); - bb.vertex(maxX + 6.0f, maxY + 6.0f, 0.0f, 0.0f); - bb.vertex(minX - 6.0f, maxY + 6.0f, 0.0f, 0.0f); - tesselator.end(); + addQuadLineLoop(bb, outerThickness, minX, minY, maxX, minY, maxX, maxY, minX, maxY); - // 3) 主边框 + 内边框(两个 LINE_LOOP) - bb.begin(RenderSystem.GL_LINE_LOOP, 4); + // 主边框(核心线) bb.setColor(SOLID_BORDER_COLOR_MAIN); - bb.vertex(minX - 1.0f, minY - 1.0f, 0.0f, 0.0f); - bb.vertex(maxX + 1.0f, minY - 1.0f, 0.0f, 0.0f); - bb.vertex(maxX + 1.0f, maxY + 1.0f, 0.0f, 0.0f); - bb.vertex(minX - 1.0f, maxY + 1.0f, 0.0f, 0.0f); - tesselator.end(); + addQuadLineLoop(bb, mainThickness, minX, minY, maxX, minY, maxX, maxY, minX, maxY); - bb.begin(RenderSystem.GL_LINE_LOOP, 4); + // 内描边(细) bb.setColor(SOLID_BORDER_COLOR_INNER); - bb.vertex(minX, minY, 0.0f, 0.0f); - bb.vertex(maxX, minY, 0.0f, 0.0f); - bb.vertex(maxX, maxY, 0.0f, 0.0f); - bb.vertex(minX, maxY, 0.0f, 0.0f); - tesselator.end(); + addQuadLineLoop(bb, innerThickness, minX, minY, maxX, minY, maxX, maxY, minX, maxY); - // 4) 手柄(一次性 TRIANGLES 批次绘制所有手柄) - // 8 个手柄(四角 + 四边中点),每个 6 个顶点 - bb.begin(RenderSystem.GL_TRIANGLES, 6 * 8); + // 手柄(角点与边中点) bb.setColor(HANDLE_COLOR); - addHandleQuad(bb, minX, minY, DEFAULT_CORNER_SIZE); // 左上 - addHandleQuad(bb, maxX, minY, DEFAULT_CORNER_SIZE); // 右上 - addHandleQuad(bb, minX, maxY, DEFAULT_CORNER_SIZE); // 左下 - addHandleQuad(bb, maxX, maxY, DEFAULT_CORNER_SIZE); // 右下 + addHandleQuad(bb, minX, minY, handleCornerSize); + addHandleQuad(bb, maxX, minY, handleCornerSize); + addHandleQuad(bb, minX, maxY, handleCornerSize); + addHandleQuad(bb, maxX, maxY, handleCornerSize); - addHandleQuad(bb, (minX + maxX) / 2f, minY, DEFAULT_BORDER_THICKNESS); // 上中 - addHandleQuad(bb, (minX + maxX) / 2f, maxY, DEFAULT_BORDER_THICKNESS); // 下中 - addHandleQuad(bb, minX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); // 左中 - addHandleQuad(bb, maxX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); // 右中 - tesselator.end(); + addHandleQuad(bb, (minX + maxX) * 0.5f, minY, handleMidSize); + addHandleQuad(bb, (minX + maxX) * 0.5f, maxY, handleMidSize); + addHandleQuad(bb, minX, (minY + maxY) * 0.5f, handleMidSize); + addHandleQuad(bb, maxX, (minY + maxY) * 0.5f, handleMidSize); - // 5) 中心点(十字 + 圆环) - // 十字:LINES - bb.begin(GL11.GL_LINES, 4); + // 中心点:十字 + 环(圆环使用三角片段) bb.setColor(CENTER_POINT_COLOR); - bb.vertex(pivot.x - 6.0f, pivot.y, 0.0f, 0.0f); - bb.vertex(pivot.x + 6.0f, pivot.y, 0.0f, 0.0f); - bb.vertex(pivot.x, pivot.y - 6.0f, 0.0f, 0.0f); - bb.vertex(pivot.x, pivot.y + 6.0f, 0.0f, 0.0f); - tesselator.end(); + addQuadLine(bb, pivot.x - 6.0f / cameraScale, pivot.y, pivot.x + 6.0f / cameraScale, pivot.y, centerLineThickness); + addQuadLine(bb, pivot.x, pivot.y - 6.0f / cameraScale, pivot.x, pivot.y + 6.0f / cameraScale, centerLineThickness); + addRing(bb, pivot.x, pivot.y, centerRingRadius, centerRingThickness, 18); - // 圆环:LINE_LOOP - bb.begin(RenderSystem.GL_LINE_LOOP, 16); - bb.setColor(CENTER_POINT_COLOR); - float radius = 6.0f * 0.85f; - for (int i = 0; i < 16; i++) { - float angle = (float) (i * 2f * Math.PI / 16f); - bb.vertex(pivot.x + (float) Math.cos(angle) * radius, pivot.y + (float) Math.sin(angle) * radius, 0.0f, 0.0f); - } - tesselator.end(); - - // 6) 旋转手柄(连线 + 圆 + 箭头),分三次提交但数量小 - float topY = bounds.getMinY(); - float rotationHandleY = topY - ROTATION_HANDLE_DISTANCE; - - // 连线 - bb.begin(GL11.GL_LINES, 2); - bb.setColor(ROTATION_HANDLE_COLOR); - bb.vertex(pivot.x, topY, 0.0f, 0.0f); - bb.vertex(pivot.x, rotationHandleY, 0.0f, 0.0f); - tesselator.end(); - - // 圆 - bb.begin(RenderSystem.GL_LINE_LOOP, 16); - bb.setColor(ROTATION_HANDLE_COLOR); - float handleRadius = 6.0f; - for (int i = 0; i < 16; i++) { - float angle = (float) (i * 2f * Math.PI / 16f); - bb.vertex(pivot.x + (float) Math.cos(angle) * handleRadius, rotationHandleY + (float) Math.sin(angle) * handleRadius, 0.0f, 0.0f); - } - tesselator.end(); - - // 箭头(两条交叉线,提示旋转) - bb.begin(GL11.GL_LINES, 4); - bb.setColor(ROTATION_HANDLE_COLOR); - float arrow = 4.0f; - bb.vertex(pivot.x - arrow, rotationHandleY - arrow, 0.0f, 0.0f); - bb.vertex(pivot.x + arrow, rotationHandleY + arrow, 0.0f, 0.0f); - bb.vertex(pivot.x + arrow, rotationHandleY - arrow, 0.0f, 0.0f); - bb.vertex(pivot.x - arrow, rotationHandleY + arrow, 0.0f, 0.0f); tesselator.end(); } /** - * 绘制多选框(现代化外观,批量提交) + * 向后兼容:不传 cameraScale 时使用默认 1.0(即不做屏幕稳定) */ - public static void drawMultiSelectionBox(BoundingBox multiBounds) { - if (!multiBounds.isValid()) return; + public static void drawSelectBox(BoundingBox bounds, Vector2f pivot) { + drawSelectBox(bounds, pivot, 1.0f); + } + + /** + * 绘制多选框(推荐传入 cameraScale) + * + * @param multiBounds 包围盒 + * @param cameraScale 世界单位 -> 像素 比例 + */ + public static void drawMultiSelectionBox(BoundingBox multiBounds, float cameraScale) { + if (multiBounds == null || !multiBounds.isValid()) return; + if (cameraScale <= 0f) cameraScale = MIN_CAMERA_SCALE; float minX = multiBounds.getMinX(); float minY = multiBounds.getMinY(); float maxX = multiBounds.getMaxX(); float maxY = multiBounds.getMaxY(); + // 屏幕稳定尺寸 + float handleCornerSize = Math.max(2.0f / cameraScale, HANDLE_CORNER_SIZE / cameraScale); + float handleMidSize = Math.max(2.0f / cameraScale, HANDLE_MID_SIZE / cameraScale); + float centerLineThickness = Math.max(1.0f / cameraScale, CENTER_LINE_THICKNESS / cameraScale); + + float dashLen = Math.max(3.0f / cameraScale, DEFAULT_DASH_LENGTH / cameraScale); + float gapLen = Math.max(2.0f / cameraScale, DEFAULT_GAP_LENGTH / cameraScale); + Tesselator tesselator = Tesselator.getInstance(); BufferBuilder bb = tesselator.getBuilder(); RenderSystem.enableBlend(); RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - // 虚线边框 - 将所有虚线段放在同一个 GL_LINES 批次 - int estimatedSegments = Math.max(4, - (int) Math.ceil((2f * ((maxX - minX) + (maxY - minY))) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH))); + // 1) 虚线边框(使用 GL_LINES) + int estimatedSegments = Math.max(4, (int) Math.ceil((2f * (multiBounds.getWidth() + multiBounds.getHeight())) / (dashLen + gapLen))); bb.begin(GL11.GL_LINES, estimatedSegments * 2); bb.setColor(DASHED_BORDER_COLOR); - addDashedLineVertices(bb, minX, minY, maxX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH); - addDashedLineVertices(bb, maxX, minY, maxX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH); - addDashedLineVertices(bb, maxX, maxY, minX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH); - addDashedLineVertices(bb, minX, maxY, minX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH); + addDashedLineVertices(bb, minX, minY, maxX, minY, dashLen, gapLen); + addDashedLineVertices(bb, maxX, minY, maxX, maxY, dashLen, gapLen); + addDashedLineVertices(bb, maxX, maxY, minX, maxY, dashLen, gapLen); + addDashedLineVertices(bb, minX, maxY, minX, minY, dashLen, gapLen); tesselator.end(); - // 手柄(一次性 TRIANGLES 批次) - bb.begin(RenderSystem.GL_TRIANGLES, 6 * 8); + // 2) 手柄与中心(合并为一次三角形绘制) + bb.begin(RenderSystem.GL_TRIANGLES, (8 + 2) * 6); bb.setColor(MULTI_SELECTION_HANDLE_COLOR); - addHandleQuad(bb, minX, minY, DEFAULT_CORNER_SIZE); - addHandleQuad(bb, maxX, minY, DEFAULT_CORNER_SIZE); - addHandleQuad(bb, minX, maxY, DEFAULT_CORNER_SIZE); - addHandleQuad(bb, maxX, maxY, DEFAULT_CORNER_SIZE); - addHandleQuad(bb, (minX + maxX) / 2f, minY, DEFAULT_BORDER_THICKNESS); - addHandleQuad(bb, (minX + maxX) / 2f, maxY, DEFAULT_BORDER_THICKNESS); - addHandleQuad(bb, minX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); - addHandleQuad(bb, maxX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); - tesselator.end(); + addHandleQuad(bb, minX, minY, handleCornerSize); + addHandleQuad(bb, maxX, minY, handleCornerSize); + addHandleQuad(bb, minX, maxY, handleCornerSize); + addHandleQuad(bb, maxX, maxY, handleCornerSize); - // 中心点 - Vector2f center = new Vector2f((minX + maxX) / 2f, (minY + maxY) / 2f); - bb.begin(GL11.GL_LINES, 4); + addHandleQuad(bb, (minX + maxX) * 0.5f, minY, handleMidSize); + addHandleQuad(bb, (minX + maxX) * 0.5f, maxY, handleMidSize); + addHandleQuad(bb, minX, (minY + maxY) * 0.5f, handleMidSize); + addHandleQuad(bb, maxX, (minY + maxY) * 0.5f, handleMidSize); + + Vector2f center = multiBounds.getCenter(); bb.setColor(CENTER_POINT_COLOR); - bb.vertex(center.x - 6.0f, center.y, 0.0f, 0.0f); - bb.vertex(center.x + 6.0f, center.y, 0.0f, 0.0f); - bb.vertex(center.x, center.y - 6.0f, 0.0f, 0.0f); - bb.vertex(center.x, center.y + 6.0f, 0.0f, 0.0f); - tesselator.end(); + addQuadLine(bb, center.x - 6.0f / cameraScale, center.y, center.x + 6.0f / cameraScale, center.y, centerLineThickness); + addQuadLine(bb, center.x, center.y - 6.0f / cameraScale, center.x, center.y + 6.0f / cameraScale, centerLineThickness); - // 旋转手柄(沿用单选逻辑) - drawMultiSelectionRotationHandle(bb, minX, minY, maxX, maxY); + tesselator.end(); } - // ------ 辅助顶点生成方法(批量写入当前 begin() 的 BufferBuilder) ------ + public static void drawMultiSelectionBox(BoundingBox multiBounds) { + drawMultiSelectionBox(multiBounds, 1.0f); + } - // 向当前 TRIANGLES 批次添加一个填充矩形(两三角形) - private static void addFilledQuadTriangles(BufferBuilder bb, float x0, float y0, float x1, float y1) { - // 三角形 1 + // -------------------- 辅助绘图方法(保持不变) -------------------- + + /** + * 添加一个填充四边形(用两个三角形表示) + */ + private static void addFilledQuad(BufferBuilder bb, float x0, float y0, float x1, float y1) { + // tri 1 bb.vertex(x0, y0, 0.0f, 0.0f); bb.vertex(x1, y0, 0.0f, 0.0f); bb.vertex(x1, y1, 0.0f, 0.0f); - // 三角形 2 + // tri 2 bb.vertex(x1, y1, 0.0f, 0.0f); bb.vertex(x0, y1, 0.0f, 0.0f); bb.vertex(x0, y0, 0.0f, 0.0f); } - // 向当前 TRIANGLES 批次添加一个手柄方块(中心在 cx,cy,边长 size) + /** + * 手柄:以中心点绘制正方形手柄(填充) + */ private static void addHandleQuad(BufferBuilder bb, float cx, float cy, float size) { - float half = size / 2f; - addFilledQuadTriangles(bb, cx - half, cy - half, cx + half, cy + half); + float half = size * 0.5f; + addFilledQuad(bb, cx - half, cy - half, cx + half, cy + half); } - // 向当前 LINES 批次添加一段虚线(将多个线段顶点 push 到当前 begin()) + /** + * 绘制一条由四边形模拟的线段(厚度以输入值为准) + */ + private static void addQuadLine(BufferBuilder bb, float x0, float y0, float x1, float y1, float thickness) { + float dx = x1 - x0; + float dy = y1 - y0; + float len = (float) Math.sqrt(dx * dx + dy * dy); + if (len < 1e-6f) return; + + float halfThick = thickness * 0.5f; + float nx = -dy / len * halfThick; + float ny = dx / len * halfThick; + + float v0x = x0 + nx; float v0y = y0 + ny; + float v1x = x1 + nx; float v1y = y1 + ny; + float v2x = x1 - nx; float v2y = y1 - ny; + float v3x = x0 - nx; float v3y = y0 - ny; + + // tri1 + bb.vertex(v3x, v3y, 0.0f, 0.0f); + bb.vertex(v0x, v0y, 0.0f, 0.0f); + bb.vertex(v1x, v1y, 0.0f, 0.0f); + // tri2 + bb.vertex(v1x, v1y, 0.0f, 0.0f); + bb.vertex(v2x, v2y, 0.0f, 0.0f); + bb.vertex(v3x, v3y, 0.0f, 0.0f); + } + + /** + * 绘制一个闭合的四边形线环(用于边框三层绘制) + * + * @param thickness 厚度(以外部传入的值为准,通常已根据 cameraScale 调整) + * @param vertices 顶点序列 x1,y1,x2,y2,... + */ + private static void addQuadLineLoop(BufferBuilder bb, float thickness, float... vertices) { + if (vertices == null || vertices.length < 4) return; + int n = vertices.length / 2; + for (int i = 0; i < n; i++) { + float x0 = vertices[(i * 2)]; + float y0 = vertices[(i * 2) + 1]; + float x1 = vertices[((i + 1) % n) * 2]; + float y1 = vertices[((i + 1) % n) * 2 + 1]; + addQuadLine(bb, x0, y0, x1, y1, thickness); + } + } + + /** + * 绘制圆环(由多个三角形片段组成) + * + * @param cx 中心 x + * @param cy 中心 y + * @param radius 半径(通常已按 cameraScale 调整) + * @param thickness 环厚度(通常已按 cameraScale 调整) + * @param segments 分段数(建议 >= 8) + */ + private static void addRing(BufferBuilder bb, float cx, float cy, float radius, float thickness, int segments) { + if (segments < 6) segments = 6; + float halfThick = thickness * 0.5f; + float innerR = Math.max(0.5f, radius - halfThick); + float outerR = radius + halfThick; + + for (int i = 0; i < segments; i++) { + float a0 = (float) (i * 2.0 * Math.PI / segments); + float a1 = (float) ((i + 1) * 2.0 * Math.PI / segments); + + float cos0 = (float) Math.cos(a0), sin0 = (float) Math.sin(a0); + float cos1 = (float) Math.cos(a1), sin1 = (float) Math.sin(a1); + + float x0i = cx + cos0 * innerR, y0i = cy + sin0 * innerR; + float x1i = cx + cos1 * innerR, y1i = cy + sin1 * innerR; + float x0o = cx + cos0 * outerR, y0o = cy + sin0 * outerR; + float x1o = cx + cos1 * outerR, y1o = cy + sin1 * outerR; + + // tri 1 + bb.vertex(x0i, y0i, 0.0f, 0.0f); + bb.vertex(x0o, y0o, 0.0f, 0.0f); + bb.vertex(x1o, y1o, 0.0f, 0.0f); + // tri 2 + bb.vertex(x1o, y1o, 0.0f, 0.0f); + bb.vertex(x1i, y1i, 0.0f, 0.0f); + bb.vertex(x0i, y0i, 0.0f, 0.0f); + } + } + + /** + * 在两点之间生成虚线段顶点(使用 GL_LINES) + * + * @param dashLen 虚线长度(通常已按 cameraScale 调整) + * @param gapLen 间隙长度(通常已按 cameraScale 调整) + */ private static void addDashedLineVertices(BufferBuilder bb, float startX, float startY, float endX, float endY, float dashLen, float gapLen) { float dx = endX - startX; float dy = endY - startY; float len = (float) Math.sqrt(dx * dx + dy * dy); - if (len < 0.001f) return; + if (len < 1e-6f) return; + float dirX = dx / len, dirY = dy / len; - float seg = dashLen + gapLen; - int count = (int) Math.ceil(len / seg); + float segment = dashLen + gapLen; + int count = Math.max(1, (int) Math.ceil(len / segment)); + for (int i = 0; i < count; i++) { - float s = i * seg; + float s = i * segment; if (s >= len) break; float e = Math.min(s + dashLen, len); + float sx = startX + dirX * s; float sy = startY + dirY * s; float ex = startX + dirX * e; float ey = startY + dirY * e; + bb.vertex(sx, sy, 0.0f, 0.0f); bb.vertex(ex, ey, 0.0f, 0.0f); } } - - // 适配:在 multi selection 中把旋转手柄渲染写入到传入的 bb(会在函数内部使用 tesselator.end()) - public static void drawMultiSelectionRotationHandle(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { - Vector2f center = new Vector2f((minX + maxX) / 2f, (minY + maxY) / 2f); - drawRotationHandle(center, new BoundingBox(minX, minY, maxX, maxY)); - } - - // 单独绘制旋转手柄(内部会 new / begin / end,因为包含多种 primitive) - private static void drawRotationHandle(Vector2f pivot, BoundingBox bounds) { - float centerX = pivot.x; - float centerY = pivot.y; - float topY = bounds.getMinY(); - - boolean pivotInBounds = (centerX >= bounds.getMinX() && centerX <= bounds.getMaxX() && - centerY >= bounds.getMinY() && centerY <= bounds.getMaxY()); - if (!pivotInBounds) { - centerX = (bounds.getMinX() + bounds.getMaxX()) * 0.5f; - centerY = (bounds.getMinY() + bounds.getMaxY()) * 0.5f; - topY = bounds.getMinY(); - } - - float rotationHandleY = topY - ROTATION_HANDLE_DISTANCE; - - Tesselator t = Tesselator.getInstance(); - BufferBuilder bb = t.getBuilder(); - - // 连线 - bb.begin(GL11.GL_LINES, 2); - bb.setColor(ROTATION_HANDLE_COLOR); - bb.vertex(centerX, topY, 0.0f, 0.0f); - bb.vertex(centerX, rotationHandleY, 0.0f, 0.0f); - t.end(); - - // 圆环 - bb.begin(RenderSystem.GL_LINE_LOOP, 16); - bb.setColor(ROTATION_HANDLE_COLOR); - float r = 6.0f; - for (int i = 0; i < 16; i++) { - float ang = (float) (i * 2f * Math.PI / 16f); - bb.vertex(centerX + (float) Math.cos(ang) * r, rotationHandleY + (float) Math.sin(ang) * r, 0.0f, 0.0f); - } - t.end(); - - // 箭头 - bb.begin(GL11.GL_LINES, 4); - bb.setColor(ROTATION_HANDLE_COLOR); - float arrow = 4.0f; - bb.vertex(centerX - arrow, rotationHandleY - arrow, 0.0f, 0.0f); - bb.vertex(centerX + arrow, rotationHandleY + arrow, 0.0f, 0.0f); - bb.vertex(centerX + arrow, rotationHandleY - arrow, 0.0f, 0.0f); - bb.vertex(centerX - arrow, rotationHandleY + arrow, 0.0f, 0.0f); - t.end(); - } - - /** - * 仅绘制简化的多选虚线边框(保留单次批量绘制) - */ - public static void drawSimpleMultiSelectionBox(BoundingBox multiBounds) { - if (!multiBounds.isValid()) return; - - float minX = multiBounds.getMinX(); - float minY = multiBounds.getMinY(); - float maxX = multiBounds.getMaxX(); - float maxY = multiBounds.getMaxY(); - - Tesselator tesselator = Tesselator.getInstance(); - BufferBuilder bb = tesselator.getBuilder(); - - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - - int est = Math.max(4, - (int) Math.ceil((2f * ((maxX - minX) + (maxY - minY))) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH))); - bb.begin(GL11.GL_LINES, est * 2); - bb.setColor(DASHED_BORDER_COLOR); - addDashedLineVertices(bb, minX, minY, maxX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH); - addDashedLineVertices(bb, maxX, minY, maxX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH); - addDashedLineVertices(bb, maxX, maxY, minX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH); - addDashedLineVertices(bb, minX, maxY, minX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH); - tesselator.end(); - } } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java index 4989614..bdb5a9d 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java @@ -1,7 +1,6 @@ package com.chuangzhou.vivid2D.render.awt; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.Mesh2D; /** * 模型点击事件监听器接口 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 00b80d5..01482ed 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java @@ -9,7 +9,7 @@ import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerCellRenderer; import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerReorderTransferHandler; 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.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.Texture; import com.chuangzhou.vivid2D.window.MainWindow; import org.joml.Vector2f; 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 c7fbc8f..87c8de6 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java @@ -2,20 +2,16 @@ package com.chuangzhou.vivid2D.render.awt; import com.chuangzhou.vivid2D.render.ModelRender; import com.chuangzhou.vivid2D.render.awt.manager.*; -import com.chuangzhou.vivid2D.render.awt.tools.PuppetDeformationTool; import com.chuangzhou.vivid2D.render.awt.tools.SelectionTool; import com.chuangzhou.vivid2D.render.awt.tools.Tool; import com.chuangzhou.vivid2D.render.awt.tools.VertexDeformationTool; -import com.chuangzhou.vivid2D.render.awt.tools.LiquifyTool; import com.chuangzhou.vivid2D.render.awt.util.FrameInterpolator; import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal; import com.chuangzhou.vivid2D.render.model.AnimationParameter; 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.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.manager.RanderToolsManager; -import com.chuangzhou.vivid2D.render.model.util.tools.LiquifyTargetPartRander; -import com.chuangzhou.vivid2D.render.model.util.tools.PuppetDeformationRander; import com.chuangzhou.vivid2D.render.model.util.tools.VertexDeformationRander; import com.chuangzhou.vivid2D.render.systems.Camera; import com.chuangzhou.vivid2D.test.TestModelGLPanel; @@ -90,9 +86,7 @@ public class ModelRenderPanel extends JPanel { this.toolManagement = new ToolManagement(this, randerToolsManager); // 注册所有工具 - toolManagement.registerTool(new PuppetDeformationTool(this), new PuppetDeformationRander()); toolManagement.registerTool(new VertexDeformationTool(this), new VertexDeformationRander()); - toolManagement.registerTool(new LiquifyTool(this), new LiquifyTargetPartRander()); initialize(); keyboardManager.initKeyboardShortcuts(); @@ -143,9 +137,7 @@ public class ModelRenderPanel extends JPanel { this.cameraManagement = new CameraManagement(this, glContextManager, worldManagement); this.mouseManagement = new MouseManagement(this, glContextManager, cameraManagement, keyboardManager); this.toolManagement = new ToolManagement(this, randerToolsManager); - toolManagement.registerTool(new PuppetDeformationTool(this),new PuppetDeformationRander()); toolManagement.registerTool(new VertexDeformationTool(this),new VertexDeformationRander()); - toolManagement.registerTool(new LiquifyTool(this), new LiquifyTargetPartRander()); initialize(); keyboardManager.initKeyboardShortcuts(); doubleClickTimer = new Timer(DOUBLE_CLICK_INTERVAL, e -> { @@ -275,14 +267,6 @@ public class ModelRenderPanel extends JPanel { return null; } - /** - * 获取液化工具实例 - */ - public LiquifyTool getLiquifyTool() { - Tool tool = toolManagement.getTool("液化工具"); - return tool instanceof LiquifyTool ? (LiquifyTool) tool : null; - } - private void initialize() { setLayout(new BorderLayout()); setPreferredSize(new Dimension(glContextManager.getWidth(), glContextManager.getHeight())); @@ -739,7 +723,7 @@ public class ModelRenderPanel extends JPanel { RESIZE_BOTTOM_RIGHT, // 调整右下角 ROTATE, // 新增:旋转 MOVE_PIVOT, // 新增:移动中心点 - MOVE_SECONDARY_VERTEX, // 新增:移动二级顶点 + MOVE_PRIMARY_VERTEX, // 新增:移动二级顶点 MOVE_PUPPET_PIN // 新增:移动 puppetPin } } \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ParametersPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ParametersPanel.java index 542a520..217f9bc 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ParametersPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ParametersPanel.java @@ -5,7 +5,7 @@ import com.chuangzhou.vivid2D.render.awt.tools.VertexDeformationTool; import com.chuangzhou.vivid2D.render.model.AnimationParameter; 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.Mesh2D; import javax.swing.*; import javax.swing.Timer; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/SecondaryVertexPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/SecondaryVertexPanel.java deleted file mode 100644 index 153a9f5..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/SecondaryVertexPanel.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt; - -import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex; -import org.joml.Vector2f; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.text.DecimalFormat; - -public class SecondaryVertexPanel extends JPanel { - - private SecondaryVertex currentVertex; - private final JLabel statusLabel; - private final JPanel contentPanel; - private final CardLayout cardLayout; - private final JPanel vertexInfoPanel; - - private JLabel idValue; - private JLabel posValue; - private JLabel uvValue; - private JCheckBox pinnedCheckBox; - private JCheckBox lockedCheckBox; - - private static final DecimalFormat DF = new DecimalFormat("0.00"); - - public SecondaryVertexPanel() { - cardLayout = new CardLayout(); - this.setLayout(new BorderLayout()); - - contentPanel = new JPanel(cardLayout); - - JPanel emptyPanel = new JPanel(new GridBagLayout()); - statusLabel = new JLabel("当前无顶点", SwingConstants.CENTER); - statusLabel.setFont(new Font("SansSerif", Font.ITALIC, 14)); - emptyPanel.add(statusLabel); - - vertexInfoPanel = createVertexInfoPanel(); - - contentPanel.add(emptyPanel, "EMPTY"); - contentPanel.add(vertexInfoPanel, "INFO"); - - this.add(contentPanel, BorderLayout.CENTER); - - cardLayout.show(contentPanel, "EMPTY"); - } - - private JPanel createVertexInfoPanel() { - JPanel panel = new JPanel(new GridBagLayout()); - panel.setBorder(new EmptyBorder(10, 10, 10, 10)); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(3, 5, 3, 5); - gbc.anchor = GridBagConstraints.WEST; - - int row = 0; - - gbc.gridx = 0; - gbc.weightx = 0; - panel.add(new JLabel("ID:"), gbc); - gbc.gridx = 1; - gbc.weightx = 1; - idValue = new JLabel("N/A"); - panel.add(idValue, gbc); - - row++; - gbc.gridx = 0; - gbc.gridy = row; - gbc.weightx = 0; - panel.add(new JLabel("Position (X, Y):"), gbc); - gbc.gridx = 1; - gbc.weightx = 1; - posValue = new JLabel("N/A"); - panel.add(posValue, gbc); - - row++; - gbc.gridx = 0; - gbc.gridy = row; - gbc.weightx = 0; - panel.add(new JLabel("UV (U, V):"), gbc); - gbc.gridx = 1; - gbc.weightx = 1; - uvValue = new JLabel("N/A"); - panel.add(uvValue, gbc); - - row++; - gbc.gridy = row; - gbc.gridx = 0; - gbc.gridwidth = 2; - panel.add(new JSeparator(), gbc); - - row++; - gbc.gridy = row; - gbc.gridwidth = 1; - gbc.gridx = 0; - pinnedCheckBox = new JCheckBox("Pinned (钉住)"); - pinnedCheckBox.setToolTipText("是否作为固定点,可用于拖动整个网格"); - pinnedCheckBox.addActionListener(this::handleCheckboxChange); - panel.add(pinnedCheckBox, gbc); - - gbc.gridx = 1; - lockedCheckBox = new JCheckBox("Locked (锁定)"); - lockedCheckBox.setToolTipText("锁定顶点,不允许移动"); - lockedCheckBox.addActionListener(this::handleCheckboxChange); - panel.add(lockedCheckBox, gbc); - - row++; - gbc.gridy = row; - gbc.gridx = 0; - gbc.gridwidth = 2; - gbc.weighty = 1.0; - panel.add(new JPanel(), gbc); - - return panel; - } - - public void setSecondaryVertex(SecondaryVertex vertex) { - this.currentVertex = vertex; - - if (vertex == null) { - cardLayout.show(contentPanel, "EMPTY"); - } else { - updatePanelContent(vertex); - cardLayout.show(contentPanel, "INFO"); - } - } - - private void updatePanelContent(SecondaryVertex vertex) { - idValue.setText(String.valueOf(vertex.getId())); - Vector2f pos = vertex.getPosition(); - posValue.setText(String.format("(%s, %s)", DF.format(pos.x), DF.format(pos.y))); - Vector2f uv = vertex.getUV(); - uvValue.setText(String.format("(%s, %s)", DF.format(uv.x), DF.format(uv.y))); - - pinnedCheckBox.setSelected(vertex.isPinned()); - lockedCheckBox.setSelected(vertex.isLocked()); - } - - private void handleCheckboxChange(ActionEvent e) { - if (currentVertex == null) return; - - if (e.getSource() == pinnedCheckBox) { - currentVertex.setPinned(pinnedCheckBox.isSelected()); - } else if (e.getSource() == lockedCheckBox) { - currentVertex.setLocked(lockedCheckBox.isSelected()); - } - - updatePanelContent(currentVertex); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/KeyboardManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/KeyboardManager.java index 8d9a52a..3ffba4b 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/KeyboardManager.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/KeyboardManager.java @@ -2,9 +2,7 @@ package com.chuangzhou.vivid2D.render.awt.manager; import com.chuangzhou.vivid2D.render.ModelRender; import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.awt.tools.LiquifyTool; import com.chuangzhou.vivid2D.render.awt.tools.Tool; -import com.chuangzhou.vivid2D.render.model.util.tools.LiquifyTargetPartRander; import com.chuangzhou.vivid2D.render.systems.Camera; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -218,16 +216,6 @@ public class KeyboardManager { // 更新修饰键状态 if (keyCode == KeyEvent.VK_SHIFT) { shiftPressed = true; - } else if (keyCode == KeyEvent.VK_CONTROL) { - ctrlPressed = true; - // 液化模式下按住Ctrl显示顶点 - if (panel.getCurrentTool() instanceof LiquifyTool liquifyTool) { - LiquifyTargetPartRander rander = liquifyTool.getAssociatedRanderTools(); - if (rander != null) { - rander.setAlgorithmEnabled("isRenderVertices",true); - logger.debug("液化模式下按住Ctrl,开启顶点渲染"); - } - } } // 处理功能快捷键 @@ -298,14 +286,6 @@ public class KeyboardManager { shiftPressed = false; } else if (keyCode == KeyEvent.VK_CONTROL) { ctrlPressed = false; - // 液化模式下松开Ctrl隐藏顶点 - if (panel.getCurrentTool() instanceof LiquifyTool liquifyTool) { - LiquifyTargetPartRander rander = liquifyTool.getAssociatedRanderTools(); - if (rander != null) { - rander.setAlgorithmEnabled("isRenderVertices",false); - logger.debug("液化模式下松开Ctrl,关闭顶点渲染"); - } - } } } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ThumbnailManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ThumbnailManager.java index df95a87..ed911fc 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ThumbnailManager.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ThumbnailManager.java @@ -2,7 +2,7 @@ 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.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.Texture; import java.awt.*; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/LiquifyTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/LiquifyTool.java deleted file mode 100644 index fe2340c..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/LiquifyTool.java +++ /dev/null @@ -1,393 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.tools; - -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -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.tools.LiquifyTargetPartRander; -import org.joml.Vector2f; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; - -/** - * 液化工具 - * 用于对网格进行液化变形操作 - */ -public class LiquifyTool extends Tool { - private static final Logger logger = LoggerFactory.getLogger(LiquifyTool.class); - - private ModelPart liquifyTargetPart = null; - private Mesh2D liquifyTargetMesh = null; - - private float liquifyBrushSize = 50.0f; - private float liquifyBrushStrength = 2.0f; - private ModelPart.LiquifyMode currentLiquifyMode = ModelPart.LiquifyMode.PUSH; - - private boolean renderEnabled = false; - - public LiquifyTool(ModelRenderPanel renderPanel) { - super(renderPanel, "液化工具", "对网格进行液化变形操作"); - } - - // ================== 启动和关闭方法 ================== - - /** - * 启动液化工具渲染 - * 启用液化覆盖层和顶点渲染 - */ - public void startLiquifyRendering() { - if (associatedRanderTools == null) { - logger.warn("液化渲染器未初始化"); - return; - } - - // 启用渲染算法 - enableRenderingAlgorithms(); - renderEnabled = true; - - logger.info("启动液化工具渲染"); - } - - /** - * 关闭液化工具渲染 - * 禁用所有液化相关的渲染效果 - */ - public void stopLiquifyRendering() { - if (associatedRanderTools == null) { - return; - } - - // 禁用渲染算法 - disableRenderingAlgorithms(); - renderEnabled = false; - - logger.info("关闭液化工具渲染"); - } - - /** - * 启用渲染算法 - */ - private void enableRenderingAlgorithms() { - if (associatedRanderTools != null) { - // 使用正确的方法设置算法状态 - associatedRanderTools.setAlgorithmEnabled("showLiquifyOverlay", true); - - // 根据当前状态决定是否显示顶点 - boolean showVertices = renderPanel.getKeyboardManager().getIsCtrlPressed(); - associatedRanderTools.setAlgorithmEnabled("isRenderVertices", showVertices); - } - } - - - /** - * 禁用渲染算法 - */ - private void disableRenderingAlgorithms() { - if (associatedRanderTools != null) { - associatedRanderTools.setAlgorithmEnabled("showLiquifyOverlay", false); - associatedRanderTools.setAlgorithmEnabled("isRenderVertices", false); - } - } - - - /** - * 切换顶点显示状态 - */ - public void toggleVertexRendering() { - if (associatedRanderTools != null && renderEnabled) { - boolean currentState = associatedRanderTools.isAlgorithmEnabled("isRenderVertices"); - associatedRanderTools.setAlgorithmEnabled("isRenderVertices", !currentState); - logger.info("切换顶点显示状态: {}", !currentState); - } - } - - /** - * 设置顶点显示状态 - */ - public void setVertexRendering(boolean enabled) { - if (associatedRanderTools != null && renderEnabled) { - associatedRanderTools.setAlgorithmEnabled("isRenderVertices", enabled); - logger.info("设置顶点显示状态: {}", enabled); - } - } - - // ================== 工具生命周期方法 ================== - - @Override - public void activate() { - if (isActive) return; - - isActive = true; - - // 尝试获取选中的网格作为液化目标 - if (!renderPanel.getSelectedMeshes().isEmpty()) { - liquifyTargetMesh = renderPanel.getSelectedMesh(); - liquifyTargetPart = renderPanel.findPartByMesh(liquifyTargetMesh); - } else { - // 如果没有选中的网格,尝试获取第一个可见网格 - liquifyTargetMesh = findFirstVisibleMesh(); - liquifyTargetPart = renderPanel.findPartByMesh(liquifyTargetMesh); - } - - if (liquifyTargetPart != null) { - liquifyTargetPart.setStartLiquefy(true); - // 启动渲染 - startLiquifyRendering(); - logger.info("激活液化工具: {}", liquifyTargetMesh != null ? liquifyTargetMesh.getName() : "null"); - } else { - logger.warn("没有找到可用的网格用于液化"); - } - } - - @Override - public void deactivate() { - if (!isActive) return; - - isActive = false; - - // 停止渲染 - stopLiquifyRendering(); - - if (liquifyTargetPart != null) { - liquifyTargetPart.setStartLiquefy(false); - } - liquifyTargetMesh = null; - liquifyTargetPart = null; - - logger.info("停用液化工具"); - } - - // ================== 事件处理方法 ================== - - @Override - public void onMousePressed(MouseEvent e, float modelX, float modelY) { - if (!isActive || liquifyTargetPart == null) return; - - // 液化模式下,左键按下直接开始液化操作 - if (e.getButton() == MouseEvent.BUTTON1) { - applyLiquifyEffect(modelX, modelY); - } - - // 右键可以切换顶点显示 - if (e.getButton() == MouseEvent.BUTTON3) { - toggleVertexRendering(); - renderPanel.repaint(); - } - } - - @Override - public void onMouseReleased(MouseEvent e, float modelX, float modelY) { - // 液化工具不需要特殊的释放处理 - } - - @Override - public void onMouseDragged(MouseEvent e, float modelX, float modelY) { - if (!isActive || liquifyTargetPart == null) return; - - // 液化模式下拖拽时连续应用液化效果 - if (e.getButton() == MouseEvent.BUTTON1) { - applyLiquifyEffect(modelX, modelY); - } - } - - @Override - public void onMouseMoved(MouseEvent e, float modelX, float modelY) { - // 液化工具不需要特殊的移动处理 - } - - @Override - public void onMouseClicked(MouseEvent e, float modelX, float modelY) { - // 单单击已在 pressed 中处理 - } - - @Override - public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) { - if (!isActive) return; - - // 双击空白处退出液化模式 - if (liquifyTargetPart == null || !isOverTargetMesh(modelX, modelY)) { - // 切换到选择工具 - renderPanel.getToolManagement().switchToDefaultTool(); - } - } - - @Override - public Cursor getToolCursor() { - return createLiquifyCursor(); - } - - // ================== 工具特定方法 ================== - - /** - * 应用液化效果 - */ - private void applyLiquifyEffect(float modelX, float modelY) { - if (liquifyTargetPart == null) return; - Vector2f brushCenter = new Vector2f(modelX, modelY); - boolean createVertices = renderPanel.getKeyboardManager().getIsCtrlPressed(); - liquifyTargetPart.applyLiquify(brushCenter, liquifyBrushSize, - liquifyBrushStrength, currentLiquifyMode, 1, createVertices); - renderPanel.repaint(); - } - - /** - * 检查是否在目标网格上 - */ - private boolean isOverTargetMesh(float modelX, float modelY) { - if (liquifyTargetMesh == null) return false; - - liquifyTargetMesh.updateBounds(); - return liquifyTargetMesh.containsPoint(modelX, modelY); - } - - /** - * 查找第一个可见的网格 - */ - private Mesh2D findFirstVisibleMesh() { - Model2D model = renderPanel.getModel(); - if (model == null) return null; - - java.util.List parts = model.getParts(); - if (parts == null || parts.isEmpty()) return null; - - for (ModelPart part : parts) { - if (part != null && part.isVisible()) { - java.util.List meshes = part.getMeshes(); - if (meshes != null && !meshes.isEmpty()) { - for (Mesh2D mesh : meshes) { - if (mesh != null && mesh.isVisible()) { - return mesh; - } - } - } - } - } - return null; - } - - /** - * 创建液化模式光标 - */ - private Cursor createLiquifyCursor() { - // 创建自定义液化光标(圆圈) - int size = 32; - BufferedImage cursorImg = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = cursorImg.createGraphics(); - - // 设置抗锯齿 - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // 绘制透明背景 - g2d.setColor(new Color(0, 0, 0, 0)); - g2d.fillRect(0, 0, size, size); - - // 绘制圆圈 - int center = size / 2; - int radius = (int) (liquifyBrushSize * 0.1f); // 根据画笔大小缩放光标 - - // 外圈 - g2d.setColor(Color.RED); - g2d.setStroke(new BasicStroke(2f)); - g2d.drawOval(center - radius, center - radius, radius * 2, radius * 2); - - // 内圈 - g2d.setColor(new Color(255, 100, 100, 150)); - g2d.setStroke(new BasicStroke(1f)); - g2d.drawOval(center - radius / 2, center - radius / 2, radius, radius); - - // 中心点 - g2d.setColor(Color.RED); - g2d.fillOval(center - 2, center - 2, 4, 4); - - g2d.dispose(); - - return Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new Point(center, center), "LiquifyCursor"); - } - - // ================== 配置方法 ================== - - /** - * 设置液化画笔大小 - */ - public void setLiquifyBrushSize(float size) { - this.liquifyBrushSize = Math.max(1.0f, Math.min(500.0f, size)); - } - - /** - * 设置液化画笔强度 - */ - public void setLiquifyBrushStrength(float strength) { - this.liquifyBrushStrength = Math.max(0.0f, Math.min(2.0f, strength)); - } - - /** - * 设置液化模式 - */ - public void setLiquifyMode(ModelPart.LiquifyMode mode) { - this.currentLiquifyMode = mode; - } - - // ================== 获取工具状态 ================== - - public ModelPart getLiquifyTargetPart() { - return liquifyTargetPart; - } - - public Mesh2D getLiquifyTargetMesh() { - return liquifyTargetMesh; - } - - public float getLiquifyBrushSize() { - return liquifyBrushSize; - } - - public float getLiquifyBrushStrength() { - return liquifyBrushStrength; - } - - public ModelPart.LiquifyMode getCurrentLiquifyMode() { - return currentLiquifyMode; - } - - /** - * 获取渲染器实例 - */ - public LiquifyTargetPartRander getAssociatedRanderTools() { - return (LiquifyTargetPartRander) associatedRanderTools; - } - - /** - * 检查渲染是否启用 - */ - public boolean isRenderEnabled() { - return renderEnabled; - } - - /** - * 检查是否显示顶点 - */ - public boolean isVertexRenderingEnabled() { - return associatedRanderTools != null && - associatedRanderTools.getAlgorithmEnabled().getOrDefault("isRenderVertices", false); - } - - /** - * 检查是否显示液化覆盖层 - */ - public boolean isLiquifyOverlayEnabled() { - return associatedRanderTools != null && - associatedRanderTools.getAlgorithmEnabled().getOrDefault("showLiquifyOverlay", false); - } - - @Override - public void dispose() { - // 清理资源 - stopLiquifyRendering(); - associatedRanderTools = null; - super.dispose(); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/PuppetDeformationTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/PuppetDeformationTool.java deleted file mode 100644 index 2c0bbc9..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/PuppetDeformationTool.java +++ /dev/null @@ -1,297 +0,0 @@ -package com.chuangzhou.vivid2D.render.awt.tools; - -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.PuppetPin; -import com.chuangzhou.vivid2D.render.model.util.BoundingBox; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.*; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; - -/** - * 木偶变形工具 - * 用于通过控制点对网格进行变形 - */ -public class PuppetDeformationTool extends Tool { - private static final Logger logger = LoggerFactory.getLogger(PuppetDeformationTool.class); - - private Mesh2D targetMesh = null; - private PuppetPin selectedPin = null; - private PuppetPin hoveredPin = null; - - private static final float PIN_TOLERANCE = 8.0f; - private ModelRenderPanel.DragMode currentDragMode = ModelRenderPanel.DragMode.NONE; - private float dragStartX, dragStartY; - - public PuppetDeformationTool(ModelRenderPanel renderPanel) { - super(renderPanel, "木偶变形工具", "通过控制点对网格进行变形操作"); - } - - @Override - public void activate() { - if (isActive) return; - - // 检查是否有选中的网格 - if (renderPanel.getSelectedMeshes().isEmpty()) { - logger.warn("请先选择一个网格以进入木偶变形工具模式"); - return; - } - - isActive = true; - targetMesh = renderPanel.getSelectedMesh(); - - if (targetMesh != null) { - // 显示木偶控制点 - associatedRanderTools.setAlgorithmEnabled("showPuppetPins", true); - targetMesh.setShowPuppetPins(true); - - // 如果没有木偶控制点,创建默认的四个角点 - if (targetMesh.getPuppetPinCount() == 0) { - createDefaultPuppetPins(); - } - - // 预计算权重 - targetMesh.precomputeAllPuppetWeights(); - } - - logger.info("激活木偶变形工具: {}", targetMesh != null ? targetMesh.getName() : "null"); - } - - @Override - public void deactivate() { - if (!isActive) return; - - isActive = false; - if (targetMesh != null) { - associatedRanderTools.setAlgorithmEnabled("showPuppetPins", false); - targetMesh.setShowPuppetPins(false); - } - targetMesh = null; - selectedPin = null; - hoveredPin = null; - - logger.info("停用木偶变形工具"); - } - - @Override - public void onMousePressed(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null) return; - - // 选择木偶控制点 - PuppetPin clickedPin = findPuppetPinAtPosition(modelX, modelY); - if (clickedPin != null) { - targetMesh.setSelectedPuppetPin(clickedPin); - selectedPin = clickedPin; - - // 开始拖拽 - currentDragMode = ModelRenderPanel.DragMode.MOVE_PUPPET_PIN; - dragStartX = modelX; - dragStartY = modelY; - - logger.debug("开始移动木偶控制点: ID={}", clickedPin.getId()); - } else { - // 点击空白处,取消选择 - targetMesh.setSelectedPuppetPin(null); - selectedPin = null; - } - } - - @Override - public void onMouseReleased(MouseEvent e, float modelX, float modelY) { - if (!isActive) return; - - currentDragMode = ModelRenderPanel.DragMode.NONE; - } - - @Override - public void onMouseDragged(MouseEvent e, float modelX, float modelY) { - if (!isActive || selectedPin == null) return; - - if (currentDragMode == ModelRenderPanel.DragMode.MOVE_PUPPET_PIN) { - selectedPin.setPosition(modelX, modelY); - dragStartX = modelX; - dragStartY = modelY; - targetMesh.updateVerticesFromPuppetPins(); - targetMesh.markDirty(); - renderPanel.repaint(); - } - } - - @Override - public void onMouseMoved(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null) return; - - // 更新悬停的木偶控制点 - PuppetPin newHoveredPin = findPuppetPinAtPosition(modelX, modelY); - - if (newHoveredPin != hoveredPin) { - hoveredPin = newHoveredPin; - } - } - - @Override - public void onMouseClicked(MouseEvent e, float modelX, float modelY) { - // 单单击不需要特殊处理 - } - - @Override - public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null) return; - - // 检查是否双击了木偶控制点 - PuppetPin clickedPin = findPuppetPinAtPosition(modelX, modelY); - if (clickedPin != null) { - // 双击木偶控制点:删除该控制点 - deletePuppetPin(clickedPin); - } else { - // 双击空白处:创建新的木偶控制点 - createPuppetPinAt(modelX, modelY); - } - } - - @Override - public Cursor getToolCursor() { - return createPuppetCursor(); - } - - // ================== 工具特定方法 ================== - - /** - * 创建默认的四个角点木偶控制点 - */ - private void createDefaultPuppetPins() { - if (targetMesh == null) return; - - BoundingBox bounds = targetMesh.getBounds(); - if (bounds == null || !bounds.isValid()) return; - - float minX = bounds.getMinX(); - float minY = bounds.getMinY(); - float maxX = bounds.getMaxX(); - float maxY = bounds.getMaxY(); - - // 创建四个角点 - targetMesh.addPuppetPin(minX, minY, 0.0f, 1.0f); // 左下 - targetMesh.addPuppetPin(maxX, minY, 1.0f, 1.0f); // 右下 - targetMesh.addPuppetPin(maxX, maxY, 1.0f, 0.0f); // 右上 - targetMesh.addPuppetPin(minX, maxY, 0.0f, 0.0f); // 左上 - - logger.debug("为网格 {} 创建了4个默认木偶控制点", targetMesh.getName()); - } - - /** - * 在指定位置创建木偶控制点 - */ - private void createPuppetPinAt(float x, float y) { - if (targetMesh == null) return; - - // 计算UV坐标(基于边界框) - BoundingBox bounds = targetMesh.getBounds(); - if (bounds == null || !bounds.isValid()) return; - - float u = (x - bounds.getMinX()) / bounds.getWidth(); - float v = (y - bounds.getMinY()) / bounds.getHeight(); - - // 限制UV在0-1范围内 - u = Math.max(0.0f, Math.min(1.0f, u)); - v = Math.max(0.0f, Math.min(1.0f, v)); - - PuppetPin newPin = targetMesh.addPuppetPin(x, y, u, v); - logger.info("创建木偶控制点: ID={}, 位置({}, {}), UV({}, {})", - newPin.getId(), x, y, u, v); - - renderPanel.repaint(); - } - - /** - * 删除木偶控制点 - */ - private void deletePuppetPin(PuppetPin pin) { - if (targetMesh == null || pin == null) return; - - boolean removed = targetMesh.removePuppetPin(pin); - if (removed) { - if (selectedPin == pin) { - selectedPin = null; - } - if (hoveredPin == pin) { - hoveredPin = null; - } - logger.info("删除木偶控制点: ID={}", pin.getId()); - } - - renderPanel.repaint(); - } - - /** - * 在指定位置查找木偶控制点 - */ - private PuppetPin findPuppetPinAtPosition(float x, float y) { - if (targetMesh == null) return null; - - float tolerance = PIN_TOLERANCE / calculateScaleFactor(); - return targetMesh.selectPuppetPinAt(x, y, tolerance); - } - - /** - * 计算当前缩放因子 - */ - private float calculateScaleFactor() { - return renderPanel.getCameraManagement().calculateScaleFactor(); - } - - /** - * 创建木偶工具光标 - */ - private Cursor createPuppetCursor() { - int size = 32; - BufferedImage cursorImg = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = cursorImg.createGraphics(); - - // 设置抗锯齿 - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // 绘制透明背景 - g2d.setColor(new Color(0, 0, 0, 0)); - g2d.fillRect(0, 0, size, size); - - // 绘制木偶图标 - int center = size / 2; - - // 外圈(蓝色) - g2d.setColor(Color.BLUE); - g2d.setStroke(new BasicStroke(2f)); - g2d.drawOval(center - 6, center - 6, 12, 12); - - // 内圈 - g2d.setColor(new Color(0, 0, 200, 150)); - g2d.setStroke(new BasicStroke(1f)); - g2d.drawOval(center - 3, center - 3, 6, 6); - - // 中心点 - g2d.setColor(Color.BLUE); - g2d.fillOval(center - 1, center - 1, 2, 2); - - g2d.dispose(); - - return Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new Point(center, center), "PuppetCursor"); - } - - // ================== 获取工具状态 ================== - - public Mesh2D getTargetMesh() { - return targetMesh; - } - - public PuppetPin getSelectedPin() { - return selectedPin; - } - - public PuppetPin getHoveredPin() { - return hoveredPin; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java index a5993cf..0e860f0 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java @@ -1,18 +1,18 @@ package com.chuangzhou.vivid2D.render.awt.tools; import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement; +import com.chuangzhou.vivid2D.render.awt.manager.CameraManagement; 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.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; import java.awt.*; import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; import java.util.*; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -42,11 +42,78 @@ public class SelectionTool extends Tool { private final Map dragStartScales = new HashMap<>(); private final Map dragStartRotations = new HashMap<>(); private final Map dragStartPivots = new HashMap<>(); + private volatile Cursor rotationCursor = null; + private volatile float resizeAnchorX = 0.0f; + private volatile float resizeAnchorY = 0.0f; + private static final int ROTATION_CURSOR_PREFERRED_SIZE = 32; + private static final float MIN_RESIZE_PIXEL_DIM = 6.0f; // 最小像素尺寸,避免非常小的初始宽度导致放大失控 + private static final float MIN_REL_SCALE = 0.01f; // 最小相对缩放,防止接近或等于 0 + private static final float MAX_REL_SCALE = 100.0f; // 最大相对缩放,防止一两像素拖动导致极端放大 public SelectionTool(ModelRenderPanel renderPanel) { super(renderPanel, "选择工具", "选择和操作网格对象"); + + // 初始化自定义旋转光标(现代风格的圆弧箭头) + try { + Toolkit tk = Toolkit.getDefaultToolkit(); + Dimension best = tk.getBestCursorSize(ROTATION_CURSOR_PREFERRED_SIZE, ROTATION_CURSOR_PREFERRED_SIZE); + int w = (best.width > 0 ? best.width : ROTATION_CURSOR_PREFERRED_SIZE); + int h = (best.height > 0 ? best.height : ROTATION_CURSOR_PREFERRED_SIZE); + + BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = img.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setComposite(AlphaComposite.Src); + + // 缩小比例(让箭头与弧变小) + float scale = 0.5f; + + int cx = w / 2; + int cy = h / 2; + int baseRadius = Math.min(w, h) / 2 - 4; + int radius = Math.max(6, (int) (baseRadius * scale)); + + // 外层白色弧(较粗)——制造白色描边效果 + float outerStroke = Math.max(2f, w / 10f * scale); + g.setStroke(new BasicStroke(outerStroke, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g.setColor(Color.WHITE); + g.drawArc(cx - radius - 2, cy - radius - 2, (radius + 2) * 2, (radius + 2) * 2, 45, 270); + + // 内层黑色弧(覆盖在白弧之上) + float innerStroke = Math.max(1.5f, outerStroke * 0.6f); + g.setStroke(new BasicStroke(innerStroke, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g.setColor(Color.BLACK); + g.drawArc(cx - radius, cy - radius, radius * 2, radius * 2, 45, 270); + + // 箭头(缩小、黑色填充、白色描边) + double angle = Math.toRadians(45); + int ax = cx + (int) (Math.cos(angle) * radius); + int ay = cy - (int) (Math.sin(angle) * radius); + + int ah = Math.max(4, (int) (w / 12.0 * scale)); + Polygon arrow = new Polygon(); + arrow.addPoint(ax, ay); + arrow.addPoint(ax - ah, ay + ah + 1); + arrow.addPoint(ax + ah, ay + ah / 2); + + // 填充黑色 + g.setColor(Color.BLACK); + g.fill(arrow); + + // 白色描边(细) + g.setStroke(new BasicStroke(Math.max(1f, innerStroke * 0.4f), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g.setColor(Color.WHITE); + g.draw(arrow); + + g.dispose(); + + rotationCursor = tk.createCustomCursor(img, new Point(cx, cy), "rotationCursor"); + } catch (Throwable t) { + rotationCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); + } } + @Override public void activate() { isActive = true; @@ -136,10 +203,12 @@ public class SelectionTool extends Tool { (bounds.getMinY() + bounds.getMaxY()) / 2.0f ); } else if (dragMode != ModelRenderPanel.DragMode.NONE) { - // 开始调整大小 - 记录初始缩放状态 + // 开始调整大小 - 记录初始缩放状态,同时记录初始位置与中心点,计算缩放锚点 List selectedParts = getSelectedParts(); for (ModelPart part : selectedParts) { dragStartScales.put(part, new Vector2f(part.getScale())); + dragStartPositions.put(part, new Vector2f(part.getPosition())); // 新增:记录位置 + dragStartPivots.put(part, new Vector2f(part.getPivot())); // 新增:记录中心点 } currentDragMode = dragMode; dragStartX = modelX; @@ -150,6 +219,46 @@ public class SelectionTool extends Tool { resizeStartWidth = bounds.getWidth(); resizeStartHeight = bounds.getHeight(); + // 计算缩放锚点(根据不同手柄,锚点为对侧固定点) + switch (dragMode) { + case RESIZE_LEFT: + resizeAnchorX = bounds.getMaxX(); + resizeAnchorY = (bounds.getMinY() + bounds.getMaxY()) / 2.0f; + break; + case RESIZE_RIGHT: + resizeAnchorX = bounds.getMinX(); + resizeAnchorY = (bounds.getMinY() + bounds.getMaxY()) / 2.0f; + break; + case RESIZE_TOP: + resizeAnchorY = bounds.getMaxY(); + resizeAnchorX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f; + break; + case RESIZE_BOTTOM: + resizeAnchorY = bounds.getMinY(); + resizeAnchorX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f; + break; + case RESIZE_TOP_LEFT: + resizeAnchorX = bounds.getMaxX(); + resizeAnchorY = bounds.getMaxY(); + break; + case RESIZE_TOP_RIGHT: + resizeAnchorX = bounds.getMinX(); + resizeAnchorY = bounds.getMaxY(); + break; + case RESIZE_BOTTOM_LEFT: + resizeAnchorX = bounds.getMaxX(); + resizeAnchorY = bounds.getMinY(); + break; + case RESIZE_BOTTOM_RIGHT: + resizeAnchorX = bounds.getMinX(); + resizeAnchorY = bounds.getMinY(); + break; + default: + // 保持默认(中心缩放) + resizeAnchorX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f; + resizeAnchorY = (bounds.getMinY() + bounds.getMaxY()) / 2.0f; + break; + } } else { // 检查是否点击了网格(移动操作) Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY); @@ -402,74 +511,120 @@ public class SelectionTool extends Tool { } /** - * 处理调整大小拖拽 + * 替换 handleResizeDrag:基于“宽/高的绝对变化 = 初始宽高 ± 鼠标世界坐标位移”来计算新的宽高, + * 然后通过 newWidth/startWidth 得到相对缩放比。相比之前的算法: + * - 不做像素/相机缩放折算(直接用世界坐标),避免像素换算错误引起跳变; + * - 不在每帧更新 dragStartX/Y 或 startWidth/startHeight,避免指数式累积; + * - 对宽高做合理下限保护(采用初始尺寸的小比例)以避免除零/反向导致巨大缩放; + * - 同时保持以 resizeAnchor 为锚点调整部件位置,使视觉上“跟随”鼠标。 */ private void handleResizeDrag(float modelX, float modelY) { if (lastSelectedMesh == null) return; + // 鼠标相对按下时的世界坐标增量 float deltaX = modelX - dragStartX; float deltaY = modelY - dragStartY; - float relScaleX = 1.0f; - float relScaleY = 1.0f; + // 保护初始边界,避免除以 0 + float startW = Math.max(resizeStartWidth, 1e-4f); + float startH = Math.max(resizeStartHeight, 1e-4f); + + // 计算新的宽高(世界坐标) + float newW = startW; + float newH = startH; switch (currentDragMode) { case RESIZE_LEFT: - relScaleX = (resizeStartWidth - deltaX) / Math.max(1e-6f, resizeStartWidth); + newW = startW - deltaX; break; case RESIZE_RIGHT: - relScaleX = (resizeStartWidth + deltaX) / Math.max(1e-6f, resizeStartWidth); + newW = startW + deltaX; break; case RESIZE_TOP: - relScaleY = (resizeStartHeight - deltaY) / Math.max(1e-6f, resizeStartHeight); + newH = startH - deltaY; break; case RESIZE_BOTTOM: - relScaleY = (resizeStartHeight + deltaY) / Math.max(1e-6f, resizeStartHeight); + newH = startH + deltaY; break; case RESIZE_TOP_LEFT: - relScaleX = (resizeStartWidth - deltaX) / Math.max(1e-6f, resizeStartWidth); - relScaleY = (resizeStartHeight - deltaY) / Math.max(1e-6f, resizeStartHeight); + newW = startW - deltaX; + newH = startH - deltaY; break; case RESIZE_TOP_RIGHT: - relScaleX = (resizeStartWidth + deltaX) / Math.max(1e-6f, resizeStartWidth); - relScaleY = (resizeStartHeight - deltaY) / Math.max(1e-6f, resizeStartHeight); + newW = startW + deltaX; + newH = startH - deltaY; break; case RESIZE_BOTTOM_LEFT: - relScaleX = (resizeStartWidth - deltaX) / Math.max(1e-6f, resizeStartWidth); - relScaleY = (resizeStartHeight + deltaY) / Math.max(1e-6f, resizeStartHeight); + newW = startW - deltaX; + newH = startH + deltaY; break; case RESIZE_BOTTOM_RIGHT: - relScaleX = (resizeStartWidth + deltaX) / Math.max(1e-6f, resizeStartWidth); - relScaleY = (resizeStartHeight + deltaY) / Math.max(1e-6f, resizeStartHeight); + newW = startW + deltaX; + newH = startH + deltaY; + break; + default: break; } - // Shift 键等比例缩放 + // 最小尺寸保护:使用按下时尺寸的 1% 或绝对小值,避免鼠标移动越过锚点导致 newW <= 0 从而放大异常 + float minWorldDim = Math.max(Math.min(startW, startH) * 0.01f, 1e-4f); + newW = Math.max(newW, minWorldDim); + newH = Math.max(newH, minWorldDim); + + // 计算相对缩放比(以按下时为基准) + float relScaleX = newW / startW; + float relScaleY = newH / startH; + + // 如果按住 Shift 做等比缩放,使用平均值 if (renderPanel.getKeyboardManager().getIsShiftPressed() || shiftDuringDrag) { float uniform = (relScaleX + relScaleY) * 0.5f; relScaleX = uniform; relScaleY = uniform; } + // 在应用到 ModelPart 上前再用上下限保护防止极端值 + relScaleX = Math.max(relScaleX, MIN_REL_SCALE); + relScaleY = Math.max(relScaleY, MIN_REL_SCALE); + relScaleX = Math.min(relScaleX, MAX_REL_SCALE); + relScaleY = Math.min(relScaleY, MAX_REL_SCALE); + List selectedParts = getSelectedParts(); - // 使用 dragStartScales 中记录的初始缩放来避免累积误差 + if (selectedParts.isEmpty()) return; + for (ModelPart part : selectedParts) { Vector2f startScale = dragStartScales.getOrDefault(part, new Vector2f(part.getScale())); + Vector2f startPos = dragStartPositions.getOrDefault(part, new Vector2f(part.getPosition())); + + // 计算新的绝对缩放(基于按下时的 scale) float newScaleX = startScale.x * relScaleX; float newScaleY = startScale.y * relScaleY; - // 更新部件自身缩放(先应用再广播绝对值) + // 保护缩放范围 + newScaleX = Math.max(newScaleX, MIN_REL_SCALE); + newScaleY = Math.max(newScaleY, MIN_REL_SCALE); + newScaleX = Math.min(newScaleX, startScale.x * MAX_REL_SCALE); + newScaleY = Math.min(newScaleY, startScale.y * MAX_REL_SCALE); + + // 使用 resizeAnchor 作为锚点,按比例变换位置(世界坐标) + float newPosX = resizeAnchorX + (startPos.x - resizeAnchorX) * relScaleX; + float newPosY = resizeAnchorY + (startPos.y - resizeAnchorY) * relScaleY; + + // 应用:先设置缩放再设置位置(以确保内部变换顺序保持一致) part.setScale(newScaleX, newScaleY); + part.setPosition(newPosX, newPosY); + + // 广播同步参数 renderPanel.getParametersManagement().broadcast(part, "scale", List.of(newScaleX, newScaleY)); + renderPanel.getParametersManagement().broadcast(part, "position", List.of(newPosX, newPosY)); } - // 更新拖拽起始点和尺寸(保持与开始状态的相对关系) dragStartX = modelX; dragStartY = modelY; resizeStartWidth *= relScaleX; resizeStartHeight *= relScaleY; } + private Vector2f getMultiSelectionCenter() { List selectedParts = getSelectedParts(); if (selectedParts.isEmpty()) return new Vector2f(0, 0); @@ -543,7 +698,7 @@ public class SelectionTool extends Tool { // 检查旋转手柄 if (result == ModelRenderPanel.DragMode.NONE && - isPointInRotationHandle(modelX, modelY, center.x, center.y, minY, cornerSize)) { + isPointInRotationHandle(modelX, modelY, center.x, minY, cornerSize, minX, maxX, scaleFactor)) { result = ModelRenderPanel.DragMode.ROTATE; } @@ -600,11 +755,23 @@ public class SelectionTool extends Tool { 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 - renderPanel.getCameraManagement().ROTATION_HANDLE_DISTANCE / - renderPanel.getCameraManagement().calculateScaleFactor(); - float rotationHandleX = centerX; - return Math.abs(x - rotationHandleX) <= handleSize && Math.abs(y - rotationHandleY) <= handleSize; + private boolean isPointInRotationHandle(float x, float y, float centerX, float topY, float handleSize, + float minX, float maxX, float scaleFactor) { + // 旋转手柄距离顶部一定像素(以世界坐标换算) + float rotationHandleY = topY - CameraManagement.ROTATION_HANDLE_DISTANCE / Math.max(1e-6f, scaleFactor); + + // 垂直带宽:以 handleSize 为基准,同时保证像素级的最小高度(提高可点击性) + float band = Math.max(handleSize * 1.2f, 8.0f / Math.max(1e-6f, scaleFactor)); + + // 水平方向:允许沿着选择框上方一段横向区域触发(比仅中心点更宽松) + float horizontalPadding = Math.max(handleSize * 2.0f, 6.0f / Math.max(1e-6f, scaleFactor)); + float left = minX - horizontalPadding; + float right = maxX + horizontalPadding; + + boolean inVerticalBand = (y >= rotationHandleY - band) && (y <= rotationHandleY + band); + boolean inHorizontalRange = (x >= left) && (x <= right); + + return inVerticalBand && inHorizontalRange; } private boolean isPointInCorner(float x, float y, float cornerX, float cornerY, float cornerSize) { @@ -640,10 +807,10 @@ public class SelectionTool extends Tool { case RESIZE_BOTTOM_LEFT: return Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR); case ROTATE: - return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); + return (rotationCursor != null) ? rotationCursor : Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); case MOVE_PIVOT: return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); - case MOVE_SECONDARY_VERTEX: + case MOVE_PRIMARY_VERTEX: case MOVE_PUPPET_PIN: return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); case NONE: diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java index 99d5d55..104d35f 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java @@ -1,125 +1,81 @@ package com.chuangzhou.vivid2D.render.awt.tools; import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; +import com.chuangzhou.vivid2D.render.model.Mesh2D; 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.SecondaryVertex; -import com.chuangzhou.vivid2D.render.model.util.BoundingBox; +import com.chuangzhou.vivid2D.render.model.util.Vertex; +import com.chuangzhou.vivid2D.render.model.util.VertexTag; import org.joml.Vector2f; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.*; +import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.awt.event.KeyEvent; -import java.util.HashSet; -import java.util.Set; -import java.util.Comparator; +import java.util.stream.Collectors; -/** - * 顶点变形工具(完整) - */ public class VertexDeformationTool extends Tool { private static final Logger logger = LoggerFactory.getLogger(VertexDeformationTool.class); - private Mesh2D targetMesh = null; - private SecondaryVertex selectedVertex = null; - private SecondaryVertex hoveredVertex = null; + // [MODIFIED] 单选被替换为多选列表 + private final List selectedVertices = new ArrayList<>(); + + private Vertex hoveredVertex = null; private static final float VERTEX_TOLERANCE = 8.0f; private ModelRenderPanel.DragMode currentDragMode = ModelRenderPanel.DragMode.NONE; private float savedCameraRotation = Float.NaN; - private Vector2f savedCameraScale = new Vector2f(1,1); + private Vector2f savedCameraScale = new Vector2f(1, 1); private boolean cameraStateSaved = false; - private final List changeListeners = new ArrayList<>(); - private final List drawPath = new ArrayList<>(); - private boolean isDrawingPath = false; - private static final float SNAP_TOLERANCE_MODEL = 8.0f; + // [MODIFIED] 恢复使用 orderedControlVertices 来维护控制点的顺序 + private final List orderedControlVertices = new ArrayList<>(); public VertexDeformationTool(ModelRenderPanel renderPanel) { - super(renderPanel, "顶点变形工具", "通过二级顶点对网格进行精细变形操作"); - } - - public interface SecondaryVertexChangeListener { - void onSecondaryVertexChange(ModelPart part, Mesh2D mesh, SecondaryVertex vertex, ChangeType type); - } - - public enum ChangeType { - CREATE, MOVE, DELETE - } - - public void addChangeListener(SecondaryVertexChangeListener listener) { - if (listener != null && !changeListeners.contains(listener)) { - changeListeners.add(listener); - } - } - - public void removeChangeListener(SecondaryVertexChangeListener listener) { - changeListeners.remove(listener); - } - - private void notifyListeners(SecondaryVertex vertex, ChangeType type) { - if (targetMesh == null || targetMesh.getModelPart() == null) return; - ModelPart part = targetMesh.getModelPart(); - EventQueue.invokeLater(() -> { - for (SecondaryVertexChangeListener listener : changeListeners) { - try { - listener.onSecondaryVertexChange(part, targetMesh, vertex, type); - } catch (Exception e) { - logger.error("SecondaryVertexChangeListener 通知失败", e); - } - } - }); + super(renderPanel, "顶点变形工具", "直接对网格顶点进行精细变形操作"); } @Override public void activate() { if (isActive) return; - isActive = true; + orderedControlVertices.clear(); + selectedVertices.clear(); + if (!renderPanel.getSelectedMeshes().isEmpty()) { targetMesh = renderPanel.getSelectedMesh(); } else { targetMesh = findFirstVisibleMesh(); } + // 关键:在激活时重置部件变换,确保鼠标坐标与顶点局部坐标一致,解决偏移问题 try { if (renderPanel.getCameraManagement() != null && targetMesh != null && targetMesh.getModelPart() != null) { savedCameraRotation = targetMesh.getModelPart().getRotation(); - savedCameraScale = new Vector2f(targetMesh.getModelPart().getScale().x, targetMesh.getModelPart().getScale().y); + savedCameraScale = new Vector2f(targetMesh.getModelPart().getScale()); cameraStateSaved = true; - renderPanel.getGlContextManager().executeInGLContext(() -> { try { targetMesh.getModelPart().setRotation(0f); targetMesh.getModelPart().setScale(1f); targetMesh.getModelPart().updateMeshVertices(); - } catch (Throwable t) { - logger.debug("设置相机/部件默认状态时失败: {}", t.getMessage()); - } + } catch (Throwable t) { logger.debug("设置部件默认状态时失败: {}", t.getMessage()); } }); } - } catch (Throwable t) { - logger.debug("无法备份/设置相机状态: {}", t.getMessage()); - } + } catch (Throwable t) { logger.debug("无法备份/设置相机状态: {}", t.getMessage()); } if (targetMesh != null) { - associatedRanderTools.setAlgorithmEnabled("showSecondaryVertices", true); + targetMesh.setStates("showDeformationVertices", true); renderPanel.getGlContextManager().executeInGLContext(() -> { try { - targetMesh.setShowSecondaryVertices(true); targetMesh.setRenderVertices(true); targetMesh.updateBounds(); - } catch (Throwable t) { - logger.debug("激活顶点显示失败: {}", t.getMessage()); - } + } catch (Throwable t) { logger.debug("激活顶点显示失败: {}", t.getMessage()); } }); logger.info("激活顶点变形工具: {}", targetMesh.getName()); } else { @@ -131,6 +87,7 @@ public class VertexDeformationTool extends Tool { public void deactivate() { if (!isActive) return; isActive = false; + try { if (cameraStateSaved && renderPanel.getCameraManagement() != null && targetMesh != null && targetMesh.getModelPart() != null) { renderPanel.getGlContextManager().executeInGLContext(() -> { @@ -138,66 +95,71 @@ public class VertexDeformationTool extends Tool { targetMesh.getModelPart().setRotation(savedCameraRotation); targetMesh.getModelPart().setScale(savedCameraScale); targetMesh.getModelPart().updateMeshVertices(); - } catch (Throwable t) { - logger.debug("恢复相机/部件状态失败: {}", t.getMessage()); - } + targetMesh.saveAsOriginal(); + } catch (Throwable t) { logger.debug("恢复部件状态失败: {}", t.getMessage()); } }); } - } catch (Throwable t) { - logger.debug("无法恢复相机状态: {}", t.getMessage()); - } finally { + } catch (Throwable t) { logger.debug("无法恢复相机状态: {}", t.getMessage()); } finally { cameraStateSaved = false; savedCameraRotation = Float.NaN; - savedCameraScale = new Vector2f(1,1); + savedCameraScale = new Vector2f(1, 1); } if (targetMesh != null) { - associatedRanderTools.setAlgorithmEnabled("showSecondaryVertices", false); + // 在停用前,清除所有顶点的DEFORMATION标签 + for (Vertex v : orderedControlVertices) { + v.setTag(VertexTag.DEFAULT); + } + targetMesh.setDeformationControlVertices(new ArrayList<>()); + orderedControlVertices.clear(); + + targetMesh.setStates("showDeformationVertices", false); try { - targetMesh.setShowSecondaryVertices(false); targetMesh.setRenderVertices(false); if (targetMesh.getModelPart() != null) { targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); targetMesh.getModelPart().updateMeshVertices(); + targetMesh.saveAsOriginal(); } - } catch (Throwable t) { - logger.debug("停用时清理失败: {}", t.getMessage()); - } + } catch (Throwable t) { logger.debug("停用时清理失败: {}", t.getMessage()); } } targetMesh = null; - selectedVertex = null; + selectedVertices.clear(); hoveredVertex = null; currentDragMode = ModelRenderPanel.DragMode.NONE; - logger.info("停用顶点变形工具"); } + /** + * [MODIFIED] onMousePressed 现在支持 Ctrl 多选。 + */ @Override public void onMousePressed(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null || isDrawingPath) return; + if (!isActive || targetMesh == null) return; renderPanel.getGlContextManager().executeInGLContext(() -> { try { - SecondaryVertex clickedVertex = findSecondaryVertexAtPosition(modelX, modelY); - - if (!e.isShiftDown()) { - List svs = targetMesh.getSecondaryVertices(); - for (SecondaryVertex sv : svs) { - sv.setSelected(false); - } - selectedVertex = null; - } - + Vertex clickedVertex = findDeformationVertexAtPosition(modelX, modelY); if (clickedVertex != null) { - clickedVertex.setSelected(!clickedVertex.isSelected() || e.isShiftDown()); - if (clickedVertex.isSelected()) { - selectedVertex = clickedVertex; - } else if (selectedVertex == clickedVertex) { - selectedVertex = null; + if (e.isControlDown()) { + // Ctrl 多选逻辑 + if (selectedVertices.contains(clickedVertex)) { + selectedVertices.remove(clickedVertex); // 已选中的,再次点击则取消 + } else { + selectedVertices.add(clickedVertex); // 未选中的,添加到列表 + } + } else { + // 普通单击逻辑 + if (!selectedVertices.contains(clickedVertex)) { + selectedVertices.clear(); + selectedVertices.add(clickedVertex); + } } - currentDragMode = ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX; - logger.debug("开始移动二级顶点: ID={}, 位置({}, {})", - clickedVertex.getId(), modelX, modelY); + currentDragMode = ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX; } else { + // 点击空白处,取消所有选择 + if (!e.isControlDown()) { + selectedVertices.clear(); + } currentDragMode = ModelRenderPanel.DragMode.NONE; } } catch (Throwable t) { @@ -208,654 +170,178 @@ public class VertexDeformationTool extends Tool { }); } - @Override public void onMouseReleased(MouseEvent e, float modelX, float modelY) { if (!isActive) return; - - if (currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX && selectedVertex != null) { - logger.debug("完成移动二级顶点: ID={}", selectedVertex.getId()); - notifyListeners(selectedVertex, ChangeType.MOVE); + if (currentDragMode == ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX && !selectedVertices.isEmpty()) { + renderPanel.getGlContextManager().executeInGLContext(() -> { + try { + if (targetMesh != null) { + targetMesh.saveAsOriginal(); + if (targetMesh.getModelPart() != null) { + targetMesh.getModelPart().updateMeshVertices(); + } + } + } catch (Throwable t) { logger.error("onMouseReleased 保存基准失败", t); } + }); } - currentDragMode = ModelRenderPanel.DragMode.NONE; + renderPanel.repaint(); } @Override public void onMouseDragged(MouseEvent e, float modelX, float modelY) { - if (!isActive || selectedVertex == null) return; + if (!isActive || selectedVertices.isEmpty() || targetMesh == null || currentDragMode != ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX) return; - if (currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - selectedVertex.setPosition(modelX, modelY); - - notifyListeners(selectedVertex, ChangeType.MOVE); - - try { - if (targetMesh != null && targetMesh.getModelPart() != null) { - Map payload = Map.of( - "id", selectedVertex.getId(), - "pos", List.of(modelX, modelY) - ); - renderPanel.getParametersManagement().broadcast(targetMesh.getModelPart(), "secondaryVertex", payload); - } - } catch (Throwable bx) { - logger.debug("广播 secondaryVertex 失败: {}", bx.getMessage()); - } - - if (targetMesh != null && targetMesh.getModelPart() != null) { - targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); - targetMesh.updateBounds(); - targetMesh.getModelPart().updateMeshVertices(); - } - } catch (Throwable t) { - logger.error("onMouseDragged (VertexDeformationTool) 处理失败", t); - } finally { - renderPanel.repaint(); - } - }); - } - } - - @Override - public void onMouseMoved(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null) return; - if (isDrawingPath) { - targetMesh.setPreviewPoint(new Vector2f(modelX, modelY)); - renderPanel.repaint(); - } - SecondaryVertex newHoveredVertex = findSecondaryVertexAtPosition(modelX, modelY); - if (newHoveredVertex != hoveredVertex) { - hoveredVertex = newHoveredVertex; - if (hoveredVertex != null) { - renderPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - } else if (isDrawingPath) { - renderPanel.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); - } else { - renderPanel.setCursor(createVertexCursor()); - } - } - } - - @Override - public void onMouseClicked(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null) return; - - if (isDrawingPath) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - addPointToPath(modelX, modelY); - } catch (Throwable t) { - logger.error("onMouseClicked(addPointToPath) 失败", t); - } finally { - renderPanel.repaint(); - } - }); - return; - } - } - - @Override - public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null) return; + // 我们需要计算位移,以便批量移动 + // (这里简化处理,只移动最后一个选中的点,完整的批量拖拽需要更复杂的逻辑) + Vertex primaryVertex = selectedVertices.get(selectedVertices.size() - 1); renderPanel.getGlContextManager().executeInGLContext(() -> { try { - SecondaryVertex clickedVertex = findSecondaryVertexAtPosition(modelX, modelY); - - if (!isDrawingPath) { - if (clickedVertex != null) { - drawPath.clear(); - Vector2f p = clickedVertex.getPosition(); - drawPath.add(new Vector2f(p.x, p.y)); - isDrawingPath = true; - renderPanel.repaint(); - logger.info("开始绘制路径(起点为顶点 ID={})", clickedVertex.getId()); - } else { - createSecondaryVertexAt(modelX, modelY); - } - } else { - if (clickedVertex != null) { - addPointToPath(clickedVertex.getPosition().x, clickedVertex.getPosition().y); - commitPath(); - } else { - SnapResult snap = snapToVertexOrSegment(modelX, modelY); - if (snap.type == SnapResult.Type.VERTEX) { - // 如果吸附到顶点,并且该顶点不是路径最后一个点 - Vector2f last = drawPath.get(drawPath.size() - 1); - if (last.distance(new Vector2f((float)snap.px, (float)snap.py)) > 1e-6f) { - addPointToPath((float) snap.px, (float) snap.py); - } - commitPath(); - } else if (snap.type == SnapResult.Type.SEGMENT) { - addPointToPath((float) snap.px, (float) snap.py); - commitPath(); - } else { - if (drawPath.size() >= 2) { - Vector2f first = drawPath.get(0); - float dx = modelX - first.x; - float dy = modelY - first.y; - float tol = SNAP_TOLERANCE_MODEL / calculateScaleFactor(); - if (dx * dx + dy * dy <= tol * tol) { - commitPath(); - } else { - addPointToPath(modelX, modelY); - commitPath(); - } - } else { - drawPath.clear(); - isDrawingPath = false; - renderPanel.repaint(); - } - } - } - } + primaryVertex.position.set(modelX, modelY); + targetMesh.applyDeformation(); } catch (Throwable t) { - logger.error("onMouseDoubleClicked 处理失败", t); - drawPath.clear(); - isDrawingPath = false; + logger.error("onMouseDragged (VertexDeformationTool) 处理失败", t); + } finally { renderPanel.repaint(); } }); } @Override - public void onKeyPressed(KeyEvent e) { - if (!isActive) return; - - if (selectedVertex != null) { - int kc = e.getKeyCode(); - if (kc == KeyEvent.VK_BACK_SPACE || kc == KeyEvent.VK_DELETE || kc == KeyEvent.VK_LEFT) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - deleteSecondaryVertex(selectedVertex); - selectedVertex = null; - renderPanel.repaint(); - } catch (Throwable t) { - logger.error("onKeyPressed 删除顶点失败", t); - } - }); - } - } - - if (e.getKeyCode() == KeyEvent.VK_ESCAPE && isDrawingPath) { - drawPath.clear(); - isDrawingPath = false; - renderPanel.repaint(); + public void onMouseMoved(MouseEvent e, float modelX, float modelY) { + if (!isActive || targetMesh == null) return; + Vertex newHoveredVertex = findDeformationVertexAtPosition(modelX, modelY); + if (newHoveredVertex != hoveredVertex) { + hoveredVertex = newHoveredVertex; + renderPanel.setCursor(hoveredVertex != null ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : createVertexCursor()); } } - private void addPointToPath(float x, float y) { - if (targetMesh == null || !isDrawingPath) return; - float tol = SNAP_TOLERANCE_MODEL / calculateScaleFactor(); - SecondaryVertex nearest = targetMesh.selectSecondaryVertexAt(x, y, tol); - if (nearest != null) { - Vector2f p = nearest.getPosition(); - if (drawPath.size() >= 2 && drawPath.get(0).distance(p) <= 1e-6f) { - commitPath(); - return; - } - drawPath.add(new Vector2f(p.x, p.y)); - logger.debug("路径吸附到顶点 ID={} at ({},{})", nearest.getId(), p.x, p.y); - targetMesh.setPreviewPoint(null); - return; - } - List svs = targetMesh.getSecondaryVertices(); - float bestD = Float.POSITIVE_INFINITY; - Vector2f bestProj = null; - for (int i = 0; i < svs.size(); i++) { - for (int j = i + 1; j < svs.size(); j++) { - Vector2f a = svs.get(i).getPosition(); - Vector2f b = svs.get(j).getPosition(); - double proj[] = projectPointToSegment(x, y, a.x, a.y, b.x, b.y); - double px = proj[0], py = proj[1], dist = proj[2]; - if (dist < bestD) { - bestD = (float) dist; - bestProj = new Vector2f((float) px, (float) py); + @Override + public void onMouseClicked(MouseEvent e, float modelX, float modelY) { + // No action + } + + /** + * [MODIFIED] 双击逻辑现在是:双击已有控制点则删除,双击空白则添加。 + */ + @Override + public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) { + if (!isActive || targetMesh == null) return; + renderPanel.getGlContextManager().executeInGLContext(() -> { + try { + Vertex clickedVertex = findDeformationVertexAtPosition(modelX, modelY); + if (clickedVertex != null) { + // 双击已有的点 -> 删除 + untagDeformationVertex(clickedVertex); + } else { + // 双击空白处 -> 添加 + tagNearestVertexAsDeformation(modelX, modelY); } - } - } - if (bestProj != null && bestD <= tol) { - drawPath.add(new Vector2f(bestProj.x, bestProj.y)); - logger.debug("路径吸附到线段投影 at ({},{}) dist={}", bestProj.x, bestProj.y, bestD); - targetMesh.setPreviewPoint(null); - return; - } - drawPath.add(new Vector2f(x, y)); - targetMesh.setPreviewPoint(null); - } - - private boolean pointsAreEqual(Vector2f a, Vector2f b) { - return a.distanceSquared(b) < 1e-12f; - } - - private void commitPath() { - if (targetMesh == null) return; - - if (drawPath.size() < 3) { - logger.info("路径闭合失败:点数不足"); - drawPath.clear(); - isDrawingPath = false; - targetMesh.setPreviewPoint(null); - renderPanel.setCursor(createVertexCursor()); - renderPanel.repaint(); - return; - } - - // 保留原始顺序,不去重 - List poly = new ArrayList<>(drawPath); - - // 确保首尾闭合 - Vector2f first = poly.get(0); - Vector2f last = poly.get(poly.size() - 1); - if (!pointsAreEqual(first, last)) { - poly.add(new Vector2f(first.x, first.y)); - } - - List verticesInPolygon = new ArrayList<>(); - for (Vector2f p : poly) { - SecondaryVertex v = targetMesh.addSecondaryVertex(p.x, p.y, 0f, 0f); - if (v != null) { - v.setSelected(true); - verticesInPolygon.add(v); - } - } - - if (!verticesInPolygon.isEmpty()) { - // 第一个顶点作为 master - SecondaryVertex masterVertex = verticesInPolygon.get(0); - SecondaryVertex.ControlShape masterShape = masterVertex.getControlShape(); - - // 清理 masterShape - masterShape.clearControlVertices(); - - float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE; - float maxX = Float.MIN_VALUE, maxY = Float.MIN_VALUE; - - for (SecondaryVertex sv : verticesInPolygon) { - Vector2f pos = sv.getPosition(); - masterShape.addControlVertex(sv); - - minX = Math.min(minX, pos.x); - minY = Math.min(minY, pos.y); - maxX = Math.max(maxX, pos.x); - maxY = Math.max(maxY, pos.y); - } - - masterShape.setMinControlPoint(new Vector2f(minX, minY)); - masterShape.setMaxControlPoint(new Vector2f(maxX, maxY)); - } - - selectedVertex = null; - drawPath.clear(); - isDrawingPath = false; - targetMesh.setPreviewPoint(null); - renderPanel.setCursor(createVertexCursor()); - - if (targetMesh.getModelPart() != null) { - targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); - targetMesh.getModelPart().updateMeshVertices(); - } - renderPanel.repaint(); - - logger.info("路径闭合完成:总顶点数量={}", verticesInPolygon.size()); - } - - private double[] projectPointToSegment(double px, double py, double x1, double y1, double x2, double y2) { - double vx = x2 - x1, vy = y2 - y1; - double wx = px - x1, wy = py - y1; - double c1 = vx * wx + vy * wy; - if (c1 <= 0) { - double d = Math.hypot(px - x1, py - y1); - return new double[]{x1, y1, d}; - } - double c2 = vx * vx + vy * vy; - if (c2 <= c1) { - double d = Math.hypot(px - x2, py - y2); - return new double[]{x2, y2, d}; - } - double t = c1 / c2; - double projx = x1 + t * vx; - double projy = y1 + t * vy; - double d = Math.hypot(px - projx, py - projy); - return new double[]{projx, projy, d}; - } - - private boolean pointInPolygonRayCast(float x, float y, java.util.List poly) { - boolean inside = false; - int n = poly.size(); - for (int i = 0, j = n - 1; i < n; j = i++) { - Vector2f vi = poly.get(i); - Vector2f vj = poly.get(j); - boolean intersect = ((vi.y > y) != (vj.y > y)) && - (x < (vj.x - vi.x) * (y - vi.y) / (vj.y - vi.y + 1e-12f) + vi.x); - if (intersect) inside = !inside; - } - return inside; - } - - private boolean polygonIsSelfIntersecting(java.util.List poly) { - if (poly == null) return false; - int n = poly.size(); - if (n < 4) return false; - for (int i = 0; i < n; i++) { - Vector2f a1 = poly.get(i); - Vector2f a2 = poly.get((i + 1) % n); - for (int j = i + 1; j < n; j++) { - if (Math.abs(i - j) <= 1 || (i == 0 && j == n - 1)) continue; - Vector2f b1 = poly.get(j); - Vector2f b2 = poly.get((j + 1) % n); - if (segmentsIntersect(a1.x, a1.y, a2.x, a2.y, b1.x, b1.y, b2.x, b2.y)) return true; - } - } - return false; - } - - private boolean segmentsIntersect(double x1, double y1, double x2, double y2, - double x3, double y3, double x4, double y4) { - if (Math.max(x1, x2) < Math.min(x3, x4) || Math.max(x3, x4) < Math.min(x1, x2) || - Math.max(y1, y2) < Math.min(y3, y4) || Math.max(y3, y4) < Math.min(y1, y2)) { - return false; - } - double d1 = orient(x3, y3, x4, y4, x1, y1); - double d2 = orient(x3, y3, x4, y4, x2, y2); - double d3 = orient(x1, y1, x2, y2, x3, y3); - double d4 = orient(x1, y1, x2, y2, x4, y4); - - if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && - ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) { - return true; - } - if (Math.abs(d1) < 1e-10 && onSegment(x3,y3,x4,y4,x1,y1)) return true; - if (Math.abs(d2) < 1e-10 && onSegment(x3,y3,x4,y4,x2,y2)) return true; - if (Math.abs(d3) < 1e-10 && onSegment(x1,y1,x2,y2,x3,y3)) return true; - if (Math.abs(d4) < 1e-10 && onSegment(x1,y1,x2,y2,x4,y4)) return true; - return false; - } - private double orient(double ax,double ay,double bx,double by,double cx,double cy) { - return (bx-ax)*(cy-ay) - (by-ay)*(cx-ax); - } - private boolean onSegment(double ax,double ay,double bx,double by,double px,double py) { - return px >= Math.min(ax,bx) - 1e-8 && px <= Math.max(ax,bx) + 1e-8 && - py >= Math.min(ay,by) - 1e-8 && py <= Math.max(ay,by) + 1e-8 && - Math.abs(orient(ax,ay,bx,by,px,py)) < 1e-8; - } - - private java.util.List convexHull(java.util.List points) { - java.util.List pts = new java.util.ArrayList<>(); - if (points == null || points.isEmpty()) return pts; - java.util.Set seen = new java.util.HashSet<>(); - for (Vector2f p : points) { - String k = String.format("%.6f_%.6f", p.x, p.y); - if (!seen.contains(k)) { seen.add(k); pts.add(new Vector2f(p.x, p.y)); } - } - if (pts.size() <= 1) return new java.util.ArrayList<>(pts); - pts.sort(new Comparator() { - @Override - public int compare(Vector2f a, Vector2f b) { - int c = Float.compare(a.x, b.x); - if (c != 0) return c; - return Float.compare(a.y, b.y); + } catch (Throwable t) { + logger.error("onMouseDoubleClicked 处理失败", t); } }); - java.util.List lower = new java.util.ArrayList<>(); - for (Vector2f p : pts) { - while (lower.size() >= 2) { - Vector2f p1 = lower.get(lower.size()-2), p2 = lower.get(lower.size()-1); - if (orient(p1.x,p1.y,p2.x,p2.y,p.x,p.y) <= 0) lower.remove(lower.size()-1); - else break; - } - lower.add(p); - } - java.util.List upper = new java.util.ArrayList<>(); - for (int i = pts.size()-1; i >= 0; i--) { - Vector2f p = pts.get(i); - while (upper.size() >= 2) { - Vector2f p1 = upper.get(upper.size()-2), p2 = upper.get(upper.size()-1); - if (orient(p1.x,p1.y,p2.x,p2.y,p.x,p.y) <= 0) upper.remove(upper.size()-1); - else break; - } - upper.add(p); - } - lower.remove(lower.size()-1); - upper.remove(upper.size()-1); - lower.addAll(upper); - return lower; } + /** + * [MODIFIED] 按键逻辑现在支持批量删除。 + */ + @Override + public void onKeyPressed(KeyEvent e) { + if (!isActive || selectedVertices.isEmpty()) return; + int kc = e.getKeyCode(); + if (kc == KeyEvent.VK_BACK_SPACE || kc == KeyEvent.VK_DELETE) { + renderPanel.getGlContextManager().executeInGLContext(() -> { + // 创建一个副本进行遍历,以避免在遍历时修改列表 + List toDelete = new ArrayList<>(selectedVertices); + for (Vertex v : toDelete) { + untagDeformationVertex(v); + } + selectedVertices.clear(); + }); + } + } @Override public Cursor getToolCursor() { return createVertexCursor(); } - private Mesh2D findFirstVisibleMesh() { - Model2D model = renderPanel.getModel(); - if (model == null) return null; + private void tagNearestVertexAsDeformation(float x, float y) { + if (targetMesh == null) return; + Vertex nearestVertex = null; + float minDistanceSq = Float.MAX_VALUE; - List parts = model.getParts(); - if (parts == null || parts.isEmpty()) return null; - - for (ModelPart part : parts) { - if (part != null && part.isVisible()) { - List meshes = part.getMeshes(); - if (meshes != null && !meshes.isEmpty()) { - for (Mesh2D mesh : meshes) { - if (mesh != null && mesh.isVisible()) { - return mesh; - } - } + for (Vertex v : targetMesh.getActiveVertexList()) { + // 使用原始位置进行查找,因为它是稳定的 + if (v.getTag() != VertexTag.DEFORMATION) { + float distSq = v.originalPosition.distanceSquared(x, y); + if (distSq < minDistanceSq) { + minDistanceSq = distSq; + nearestVertex = v; } } } + + if (nearestVertex != null) { + nearestVertex.setTag(VertexTag.DEFORMATION); + if (!orderedControlVertices.contains(nearestVertex)) { + orderedControlVertices.add(nearestVertex); + } + logger.info("已添加控制点 {} (总数: {})", targetMesh.getActiveVertexList().vertices.indexOf(nearestVertex), orderedControlVertices.size()); + updateDeformationRegion(); + } + } + + private void untagDeformationVertex(Vertex vertex) { + if (targetMesh == null || vertex == null) return; + vertex.setTag(VertexTag.DEFAULT); + orderedControlVertices.remove(vertex); + selectedVertices.remove(vertex); // 确保从选择列表中也移除 + if (hoveredVertex == vertex) hoveredVertex = null; + logger.info("已移除一个控制点 (剩余: {})", orderedControlVertices.size()); + updateDeformationRegion(); + } + + private void updateDeformationRegion() { + if (targetMesh == null) return; + targetMesh.setDeformationControlVertices(new ArrayList<>(orderedControlVertices)); + renderPanel.repaint(); + } + + private Vertex findDeformationVertexAtPosition(float x, float y) { + if (targetMesh == null) return null; + float tolerance = VERTEX_TOLERANCE / calculateScaleFactor(); + float toleranceSq = tolerance * tolerance; + for (Vertex v : orderedControlVertices) { // 优化:只在控制点中查找 + if (v.position.distanceSquared(x, y) < toleranceSq) { + return v; + } + } return null; } - private void createSecondaryVertexAt(float x, float y) { - if (targetMesh == null) return; - - try { - targetMesh.updateBounds(); - BoundingBox bounds = targetMesh.getBounds(); - if (bounds == null || !bounds.isValid()) { - logger.warn("无法创建二级顶点:边界框无效"); - return; - } - - float u = (x - bounds.getMinX()) / bounds.getWidth(); - float v = (y - bounds.getMinY()) / bounds.getHeight(); - - u = Math.max(0.0f, Math.min(1.0f, u)); - v = Math.max(0.0f, Math.min(1.0f, v)); - - SecondaryVertex newVertex = targetMesh.addSecondaryVertex(x, y, u, v); - if (newVertex != null) { - logger.info("创建二级顶点: ID={}, 位置({}, {}), UV({}, {})", - newVertex.getId(), x, y, u, v); - - notifyListeners(newVertex, ChangeType.CREATE); - - try { - if (targetMesh.getModelPart() != null) { - Map payload = Map.of( - "id", newVertex.getId(), - "pos", List.of(x, y) - ); - renderPanel.getParametersManagement().broadcast(targetMesh.getModelPart(), "secondaryVertex", payload); - } - } catch (Throwable bx) { - logger.debug("广播 secondaryVertex(创建) 失败: {}", bx.getMessage()); - } - - if (targetMesh.getModelPart() != null) { - targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); - targetMesh.getModelPart().updateMeshVertices(); - } - renderPanel.repaint(); - } else { - logger.warn("创建二级顶点失败"); - } - } catch (Throwable t) { - logger.error("createSecondaryVertexAt 失败", t); - } - } - - private void deleteSecondaryVertex(SecondaryVertex vertex) { - if (targetMesh == null || vertex == null) return; - - try { - boolean removed = targetMesh.removeSecondaryVertex(vertex); - if (removed) { - if (selectedVertex == vertex) { - selectedVertex = null; - } - if (hoveredVertex == vertex) { - hoveredVertex = null; - } - logger.info("删除二级顶点: ID={}", vertex.getId()); - - List svs = targetMesh.getSecondaryVertices(); - for (SecondaryVertex sv : svs) { - // 清理所有ControlShape中对被删除顶点的引用 - sv.getControlShape().removeControlVertex(vertex); - } - - notifyListeners(vertex, ChangeType.DELETE); - - try { - if (targetMesh.getModelPart() != null) { - Map payload = Map.of( - "id", vertex.getId(), - "pos", null - ); - renderPanel.getParametersManagement().broadcast(targetMesh.getModelPart(), "secondaryVertex", payload); - } - } catch (Throwable bx) { - logger.debug("广播 secondaryVertex(删除) 失败: {}", bx.getMessage()); - } - - if (targetMesh.getModelPart() != null) { - targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); - targetMesh.getModelPart().updateMeshVertices(); - } - renderPanel.repaint(); - } else { - logger.warn("删除二级顶点失败: ID={}", vertex.getId()); - } - } catch (Throwable t) { - logger.error("deleteSecondaryVertex 失败", t); - } - } - - private SecondaryVertex findSecondaryVertexAtPosition(float x, float y) { - if (targetMesh == null) return null; - - float tolerance = VERTEX_TOLERANCE / calculateScaleFactor(); - return targetMesh.selectSecondaryVertexAt(x, y, tolerance); - } - private float calculateScaleFactor() { return renderPanel.getCameraManagement().calculateScaleFactor(); } + private Mesh2D findFirstVisibleMesh() { + Model2D model = renderPanel.getModel(); if (model == null) return null; + for (ModelPart part : model.getParts()) if (part != null && part.isVisible()) + for (Mesh2D mesh : part.getMeshes()) if (mesh != null && mesh.isVisible()) return mesh; + return null; + } + private Cursor createVertexCursor() { - int size = 32; - BufferedImage cursorImg = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = cursorImg.createGraphics(); - - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - g2d.setColor(new Color(0, 0, 0, 0)); - g2d.fillRect(0, 0, size, size); - - int center = size / 2; - - g2d.setColor(Color.GREEN); - g2d.setStroke(new BasicStroke(2f)); - g2d.drawOval(center - 6, center - 6, 12, 12); - - g2d.setColor(new Color(0, 200, 0, 150)); - g2d.setStroke(new BasicStroke(1f)); - g2d.drawOval(center - 3, center - 3, 6, 6); - - g2d.setColor(Color.GREEN); - g2d.fillOval(center - 1, center - 1, 2, 2); - - g2d.dispose(); - - return Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new Point(center, center), "VertexCursor"); + int size = 32; BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = img.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + int center = size / 2; g2.setColor(Color.GREEN); g2.setStroke(new BasicStroke(2f)); + g2.drawOval(center - 6, center - 6, 12, 12); g2.dispose(); + return Toolkit.getDefaultToolkit().createCustomCursor(img, new Point(center, center), "VertexCursor"); } - public Mesh2D getTargetMesh() { - return targetMesh; - } - - public SecondaryVertex getSelectedVertex() { - return selectedVertex; - } - - public SecondaryVertex getHoveredVertex() { - return hoveredVertex; - } - - public boolean isDragging() { - return currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX; - } - - private static class SnapResult { - enum Type { NONE, VERTEX, SEGMENT } - final Type type; - final double px; - final double py; - final SecondaryVertex vertex; - - SnapResult(Type type, double px, double py, SecondaryVertex vertex) { - this.type = type; - this.px = px; - this.py = py; - this.vertex = vertex; - } - - static SnapResult none() { return new SnapResult(Type.NONE, 0, 0, null); } - static SnapResult vertex(SecondaryVertex v) { Vector2f p = v.getPosition(); return new SnapResult(Type.VERTEX, p.x, p.y, v); } - static SnapResult segment(double x, double y) { return new SnapResult(Type.SEGMENT, x, y, null); } - } - - private SnapResult snapToVertexOrSegment(float x, float y) { - if (targetMesh == null) return SnapResult.none(); - - float tol = SNAP_TOLERANCE_MODEL / calculateScaleFactor(); - - SecondaryVertex nearest = targetMesh.selectSecondaryVertexAt(x, y, tol); - if (nearest != null) { - return SnapResult.vertex(nearest); - } - - List svs = targetMesh.getSecondaryVertices(); - if (svs == null || svs.size() < 2) return SnapResult.none(); - - double bestDist = Double.POSITIVE_INFINITY; - double bestPx = 0, bestPy = 0; - - for (int i = 0; i < svs.size(); i++) { - Vector2f a = svs.get(i).getPosition(); - for (int j = i + 1; j < svs.size(); j++) { - Vector2f b = svs.get(j).getPosition(); - double[] proj = projectPointToSegment(x, y, a.x, a.y, b.x, b.y); - double px = proj[0], py = proj[1], dist = proj[2]; - if (dist < bestDist) { - bestDist = dist; - bestPx = px; - bestPy = py; - } - } - } - - if (bestDist <= tol) { - return SnapResult.segment(bestPx, bestPy); - } - return SnapResult.none(); - } - -} + public Mesh2D getTargetMesh() {return targetMesh;} +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/FrameInterpolator.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/FrameInterpolator.java index deee44b..2c5265d 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/FrameInterpolator.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/FrameInterpolator.java @@ -2,6 +2,9 @@ package com.chuangzhou.vivid2D.render.awt.util; import com.chuangzhou.vivid2D.render.awt.manager.*; import com.chuangzhou.vivid2D.render.model.*; +import com.chuangzhou.vivid2D.render.model.util.Vertex; +import com.chuangzhou.vivid2D.render.model.util.VertexTag; +import org.joml.Vector2f; import org.slf4j.Logger; import java.lang.reflect.Method; @@ -10,6 +13,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +/** + * [MODIFIED] 关键帧插值器 + * 已更新为直接操作 Mesh2D 的一级顶点 (Vertex),通过 "deformationVertex" 参数 + * 来控制带有 VertexTag.DEFORMATION 标签的顶点。 + */ public class FrameInterpolator { private FrameInterpolator() {} @@ -18,54 +26,30 @@ public class FrameInterpolator { if (o == null) return 0f; if (o instanceof Number) return ((Number) o).floatValue(); if (o instanceof String) { - try { - return Float.parseFloat((String) o); - } catch (NumberFormatException ignored) { } + try { return Float.parseFloat((String) o); } catch (NumberFormatException ignored) { } } return 0f; } private static float[] readVec2(Object o) { float[] out = new float[]{0f, 0f}; - if (o == null) return out; if (o instanceof List) { List l = (List) o; if (l.size() > 0) out[0] = toFloat(l.get(0)); if (l.size() > 1) out[1] = toFloat(l.get(1)); - return out; - } - // 支持数组情况(float[] / Double[] / Number[]) - if (o.getClass().isArray()) { - Object[] arr = (Object[]) o; - if (arr.length > 0) out[0] = toFloat(arr[0]); - if (arr.length > 1) out[1] = toFloat(arr[1]); - return out; - } - // 单个数字 -> 两分量相同(兼容性) - if (o instanceof Number || o instanceof String) { + } else if (o != null && o.getClass().isArray()) { + try { + Object[] arr = (Object[]) o; + if (arr.length > 0) out[0] = toFloat(arr[0]); + if (arr.length > 1) out[1] = toFloat(arr[1]); + } catch (Exception ignored) {} + } else if (o instanceof Number || o instanceof String) { float v = toFloat(o); out[0] = out[1] = v; } return out; } - // 兼容 AnimationParameter.getValue() 的安全读取(保留原反射回退) - private static float getAnimValueSafely(Object animParam) { - if (animParam == null) return 0f; - try { - if (animParam instanceof AnimationParameter) { - Object v = ((AnimationParameter) animParam).getValue(); - return toFloat(v); - } else { - try { - Object v = animParam.getClass().getMethod("getValue").invoke(animParam); - return toFloat(v); - } catch (NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException ignored) {} - } - } catch (Throwable ignored) {} - return 0f; - } - private static float normalizeAngle(float a) { while (a <= -Math.PI) a += 2 * Math.PI; while (a > Math.PI) a -= 2 * Math.PI; @@ -73,29 +57,21 @@ public class FrameInterpolator { } private static float normalizeAnimAngleUnits(float a) { - float abs = Math.abs(a); - if (abs > Math.PI * 2f) { - // 很可能是度而不是弧度 + if (Math.abs(a) > Math.PI * 2.1f) { // 增加一点容差 return (float) Math.toRadians(a); } return a; } - // ---- 找 paramId 对应索引集合 ---- + // ---- 查找与特定动画参数关联的 paramId 索引 ---- private static List findIndicesForParam(ParametersManagement.Parameter fullParam, String paramId, AnimationParameter currentAnimationParameter) { List indices = new ArrayList<>(); if (fullParam == null || fullParam.paramId() == null || currentAnimationParameter == null) return indices; List pids = fullParam.paramId(); - // (NEW) Get the list of associated animation parameters List animParams = fullParam.animationParameter(); - // (NEW) Safety check - if (animParams == null || animParams.size() != pids.size()) { - // Mismatch in list sizes, cannot safely proceed - return indices; - } + if (animParams == null || animParams.size() != pids.size()) return indices; for (int i = 0; i < pids.size(); i++) { - // (MODIFIED) Check both paramId AND the animation parameter if (paramId.equals(pids.get(i)) && currentAnimationParameter.equals(animParams.get(i))) { indices.add(i); } @@ -103,25 +79,19 @@ public class FrameInterpolator { return indices; } - // ---- 在指定索引集合中查找围绕 current 的前后关键帧(返回全局索引) ---- + // ---- 在指定索引集合中查找围绕 current 的前后关键帧 ---- private static int[] findSurroundingKeyframesForIndices(List keyframes, List indices, float current) { - int prevIndex = -1; - int nextIndex = -1; - float prevVal = Float.NEGATIVE_INFINITY; - float nextVal = Float.POSITIVE_INFINITY; + int prevIndex = -1, nextIndex = -1; + float prevVal = Float.NEGATIVE_INFINITY, nextVal = Float.POSITIVE_INFINITY; if (keyframes == null || indices == null) return new int[]{-1, -1}; for (int idx : indices) { - if (idx < 0 || idx >= keyframes.size()) continue; - // 注意:这里不再强制要求 isKeyframe 为 true,因实时广播可能没有标记为 keyframe - float val = keyframes.get(idx); - if (val <= current) { - if (prevIndex == -1 || val >= prevVal) { + if (idx >= 0 && idx < keyframes.size()) { + float val = keyframes.get(idx); + if (val <= current && (prevIndex == -1 || val >= prevVal)) { prevIndex = idx; prevVal = val; } - } - if (val >= current) { - if (nextIndex == -1 || val <= nextVal) { + if (val >= current && (nextIndex == -1 || val <= nextVal)) { nextIndex = idx; nextVal = val; } @@ -132,520 +102,245 @@ public class FrameInterpolator { private static float computeT(float prevVal, float nextVal, float current) { if (Float.compare(nextVal, prevVal) == 0) return 0f; - float t = (current - prevVal) / (nextVal - prevVal); - if (t < 0f) t = 0f; - if (t > 1f) t = 1f; - return t; + return Math.max(0f, Math.min(1f, (current - prevVal) / (nextVal - prevVal))); } - // ---- 计算 position/scale/pivot 的目标值(在 fullParam 的特定 paramId 索引集合中计算) ---- - private static boolean computeVec2Target(ParametersManagement.Parameter fullParam, String paramId, float current, float[] out, AnimationParameter currentAnimationParameter) { - if (fullParam == null || out == null) return false; - // (MODIFIED) Pass currentAnimationParameter - List idxs = findIndicesForParam(fullParam, paramId, currentAnimationParameter); + // ---- 计算 position/scale/pivot 的目标值 ---- + private static boolean computeVec2Target(ParametersManagement.Parameter fullParam, String paramId, float current, float[] out, AnimationParameter animParam) { + List idxs = findIndicesForParam(fullParam, paramId, animParam); if (idxs.isEmpty()) return false; - List animParams = fullParam.animationParameter(); - //List isKey = fullParam.isKeyframe(); // 不强制使用 isKeyframe - int[] idx = findSurroundingKeyframesForIndices(fullParam.keyframe(), idxs, current); - int prevIndex = idx[0], nextIndex = idx[1]; + int[] surrounding = findSurroundingKeyframesForIndices(fullParam.keyframe(), idxs, current); + int prevIndex = surrounding[0], nextIndex = surrounding[1]; List values = fullParam.value(); - if (values == null) return false; - try { - if (prevIndex != -1 && nextIndex != -1) { - if (prevIndex == nextIndex) { - float[] v = readVec2(values.get(prevIndex)); - out[0] = v[0]; out[1] = v[1]; - return true; - } else { - float[] prev = readVec2(values.get(prevIndex)); - float[] next = readVec2(values.get(nextIndex)); - float prevVal = fullParam.keyframe().get(prevIndex); - float nextVal = fullParam.keyframe().get(nextIndex); - float t = computeT(prevVal, nextVal, current); - out[0] = prev[0] + t * (next[0] - prev[0]); - out[1] = prev[1] + t * (next[1] - prev[1]); - return true; - } - } else if (prevIndex != -1) { + + if (prevIndex != -1 && nextIndex != -1) { + if (prevIndex == nextIndex) { float[] v = readVec2(values.get(prevIndex)); out[0] = v[0]; out[1] = v[1]; return true; - } else if (nextIndex != -1) { - float[] v = readVec2(values.get(nextIndex)); - out[0] = v[0]; out[1] = v[1]; - return true; - } else { - // 精确匹配(兜底) - for (int i : idxs) { - if (i < 0 || i >= animParams.size()) continue; - // 允许非 keyframe 的值作为实时覆盖 - float val = fullParam.keyframe().get(i); - if (Float.compare(val, current) == 0) { - float[] v = readVec2(values.get(i)); - out[0] = v[0]; out[1] = v[1]; - return true; - } - } } - } catch (Throwable ignored) {} - return false; - } - - private static boolean computeRotationTargetGeneric(ParametersManagement.Parameter fullParam, String paramId, float current, float[] outSingle, AnimationParameter currentAnimationParameter) { - if (fullParam == null || outSingle == null) return false; - // (MODIFIED) Pass currentAnimationParameter - List idxs = findIndicesForParam(fullParam, paramId, currentAnimationParameter); - if (idxs.isEmpty()) return false; - List animParams = fullParam.animationParameter(); - List values = fullParam.value(); - int[] idx = findSurroundingKeyframesForIndices(fullParam.keyframe(), idxs, current); - int prevIndex = idx[0], nextIndex = idx[1]; - - try { - float target; - if (prevIndex != -1 && nextIndex != -1) { - if (prevIndex == nextIndex) { - target = toFloat(values.get(prevIndex)); - } else { - float p = toFloat(values.get(prevIndex)); - float q = toFloat(values.get(nextIndex)); - float prevVal = fullParam.keyframe().get(prevIndex); - float nextVal = fullParam.keyframe().get(nextIndex); - float t = computeT(prevVal, nextVal, current); - float diff = normalizeAngle(q - p); - target = p + diff * t; - } - } else if (prevIndex != -1) { - target = toFloat(values.get(prevIndex)); - } else if (nextIndex != -1) { - target = toFloat(values.get(nextIndex)); - } else { - float found = Float.NaN; - for (int i : idxs) { - if (i < 0 || i >= animParams.size()) continue; - float val = fullParam.keyframe().get(i); - if (Float.compare(val, current) == 0) { - found = toFloat(values.get(i)); - break; - } - } - if (Float.isNaN(found)) return false; - target = found; - } - - outSingle[0] = normalizeAnimAngleUnits(target); + float[] prev = readVec2(values.get(prevIndex)); + float[] next = readVec2(values.get(nextIndex)); + float t = computeT(fullParam.keyframe().get(prevIndex), fullParam.keyframe().get(nextIndex), current); + out[0] = prev[0] + t * (next[0] - prev[0]); + out[1] = prev[1] + t * (next[1] - prev[1]); return true; - } catch (Throwable ignored) {} + } else if (prevIndex != -1) { + float[] v = readVec2(values.get(prevIndex)); + out[0] = v[0]; out[1] = v[1]; + return true; + } else if (nextIndex != -1) { + float[] v = readVec2(values.get(nextIndex)); + out[0] = v[0]; out[1] = v[1]; + return true; + } return false; } - // ---- Secondary vertex 插值(为每个 vertex id 计算目标) ---- - // 返回列表:每个 SecondaryVertexTarget 表示 id -> 插值后的位置 或 标记为 deleted - private static List computeAllSecondaryVertexTargets(ParametersManagement.Parameter fullParam, String paramId, float current, AnimationParameter currentAnimationParameter) { - List results = new ArrayList<>(); - if (fullParam == null) return results; + // ---- 计算 rotation 的目标值 ---- + private static boolean computeRotationTarget(ParametersManagement.Parameter fullParam, String paramId, float current, float[] out, AnimationParameter animParam) { + List idxs = findIndicesForParam(fullParam, paramId, animParam); + if (idxs.isEmpty()) return false; - // (MODIFIED) Pass currentAnimationParameter - List idxs = findIndicesForParam(fullParam, paramId, currentAnimationParameter); - if (idxs.isEmpty()) return results; - - List animParams = fullParam.animationParameter(); + int[] surrounding = findSurroundingKeyframesForIndices(fullParam.keyframe(), idxs, current); + int prevIndex = surrounding[0], nextIndex = surrounding[1]; List values = fullParam.value(); - if (animParams == null || values == null) return results; - // 按 vertex id 分组(包含 keyframe 与 非 keyframe,允许实时覆盖) - Map> idToIndices = new HashMap<>(); - for (int i : idxs) { - if (i < 0 || i >= values.size() || i >= animParams.size()) continue; - ParsedVertex pv = parseVertexValue(values.get(i)); - if (pv == null) continue; - idToIndices.computeIfAbsent(pv.id, k -> new ArrayList<>()).add(i); - } - - // 对每个 id 单独计算 prev/next 并插值(包括处理删除标记) - for (Map.Entry> e : idToIndices.entrySet()) { - int vid = e.getKey(); - List list = e.getValue(); - if (list.isEmpty()) continue; - - int prevIndex = -1, nextIndex = -1; - float prevVal = Float.NEGATIVE_INFINITY, nextVal = Float.POSITIVE_INFINITY; - for (int idx : list) { - float val = fullParam.keyframe().get(idx); - if (val <= current) { - if (prevIndex == -1 || val >= prevVal) { - prevIndex = idx; - prevVal = val; - } - } - if (val >= current) { - if (nextIndex == -1 || val <= nextVal) { - nextIndex = idx; - nextVal = val; - } - } + float target; + if (prevIndex != -1 && nextIndex != -1) { + if (prevIndex == nextIndex) { + target = toFloat(values.get(prevIndex)); + } else { + float p = toFloat(values.get(prevIndex)); + float q = toFloat(values.get(nextIndex)); + float t = computeT(fullParam.keyframe().get(prevIndex), fullParam.keyframe().get(nextIndex), current); + target = p + t * normalizeAngle(q - p); } - - try { - // 优先 prev/next 插值或取值;若存在 pos==null 的条目(删除),则将其转为 deleted 标记 - if (prevIndex != -1 && nextIndex != -1) { - if (prevIndex == nextIndex) { - ParsedVertex pv = parseVertexValue(values.get(prevIndex)); - if (pv != null) { - SecondaryVertexTarget tgt = new SecondaryVertexTarget(); - tgt.id = vid; - tgt.deleted = pv.deleted; - tgt.x = pv.x; - tgt.y = pv.y; - results.add(tgt); - } - } else { - ParsedVertex pv = parseVertexValue(values.get(prevIndex)); - ParsedVertex nv = parseVertexValue(values.get(nextIndex)); - if (pv == null || nv == null) { - // 若任意一端为 null,则退化为非插值处理(尝试取有意义的一端) - if (pv != null) { - SecondaryVertexTarget tgt = new SecondaryVertexTarget(); - tgt.id = vid; tgt.deleted = pv.deleted; tgt.x = pv.x; tgt.y = pv.y; - results.add(tgt); - } else if (nv != null) { - SecondaryVertexTarget tgt = new SecondaryVertexTarget(); - tgt.id = vid; tgt.deleted = nv.deleted; tgt.x = nv.x; tgt.y = nv.y; - results.add(tgt); - } - } else { - // 如果任意一侧为 deleted(pos==null),则不做插值(选择 prev 的删除/存在状态) - if (pv.deleted || nv.deleted) { - // 选择较靠近 current 的那端(这里优先 prev) - SecondaryVertexTarget tgt = new SecondaryVertexTarget(); - tgt.id = vid; - tgt.deleted = pv.deleted; - tgt.x = pv.x; - tgt.y = pv.y; - results.add(tgt); - } else { - float t = computeT(prevVal, nextVal, current); - SecondaryVertexTarget tgt = new SecondaryVertexTarget(); - tgt.id = vid; - tgt.deleted = false; - tgt.x = pv.x + t * (nv.x - pv.x); - tgt.y = pv.y + t * (nv.y - pv.y); - results.add(tgt); - } - } - } - } else if (prevIndex != -1) { - ParsedVertex pv = parseVertexValue(values.get(prevIndex)); - if (pv != null) { - SecondaryVertexTarget tgt = new SecondaryVertexTarget(); - tgt.id = vid; tgt.deleted = pv.deleted; tgt.x = pv.x; tgt.y = pv.y; - results.add(tgt); - } - } else if (nextIndex != -1) { - ParsedVertex nv = parseVertexValue(values.get(nextIndex)); - if (nv != null) { - SecondaryVertexTarget tgt = new SecondaryVertexTarget(); - tgt.id = vid; tgt.deleted = nv.deleted; tgt.x = nv.x; tgt.y = nv.y; - results.add(tgt); - } - } else { - // 兜底:查找与 current 相等的条目 - for (int idx : list) { - float val = fullParam.keyframe().get(idx); - if (Float.compare(val, current) == 0) { - ParsedVertex pv = parseVertexValue(values.get(idx)); - if (pv != null) { - SecondaryVertexTarget tgt = new SecondaryVertexTarget(); - tgt.id = vid; tgt.deleted = pv.deleted; tgt.x = pv.x; tgt.y = pv.y; - results.add(tgt); - break; - } - } - } - } - } catch (Throwable ignored) {} + } else if (prevIndex != -1) { + target = toFloat(values.get(prevIndex)); + } else if (nextIndex != -1) { + target = toFloat(values.get(nextIndex)); + } else { + return false; } - - return results; + out[0] = normalizeAnimAngleUnits(target); + return true; } - // 辅助结构 - private static class ParsedVertex { - int id; + // ---- [NEW] Deformation Vertex 插值 ---- + + // [MODIFIED] 辅助结构,现在使用顶点索引(idx)替代ID + private static class DeformationVertexTarget { + int idx; float x; float y; - boolean deleted; // pos == null 表示删除 - ParsedVertex(int id, float x, float y, boolean deleted) { this.id = id; this.x = x; this.y = y; this.deleted = deleted; } + boolean deleted = false; // pos == null 表示应取消变形标记 } - private static ParsedVertex parseVertexValue(Object v) { - if (v == null) return null; - try { - if (v instanceof Map) { - Map m = (Map) v; - Object idObj = m.get("id"); - // pos 可能为 null(表示删除) - Object posObj = m.get("pos"); - if (idObj == null) return null; - int id = (idObj instanceof Number) ? ((Number) idObj).intValue() : Integer.parseInt(String.valueOf(idObj)); + // [MODIFIED] 解析新的变形顶点数据格式 { "idx": int, "pos": [x, y] } + private static DeformationVertexTarget parseDeformationVertexValue(Object v) { + if (v instanceof Map) { + Map m = (Map) v; + Object idxObj = m.get("idx"); + Object posObj = m.get("pos"); + if (idxObj instanceof Number) { + int idx = ((Number) idxObj).intValue(); + DeformationVertexTarget target = new DeformationVertexTarget(); + target.idx = idx; if (posObj == null) { - return new ParsedVertex(id, 0f, 0f, true); + target.deleted = true; + } else { + float[] p = readVec2(posObj); + target.x = p[0]; + target.y = p[1]; } - float[] p = readVec2(posObj); - return new ParsedVertex(id, p[0], p[1], false); - } else if (v instanceof List) { - List l = (List) v; - if (l.size() >= 3) { - Object idObj = l.get(0); - if (!(idObj instanceof Number)) return null; - int id = ((Number) idObj).intValue(); - float x = toFloat(l.get(1)); - float y = toFloat(l.get(2)); - return new ParsedVertex(id, x, y, false); - } else if (l.size() == 2) { - // [ id, null ] 之类(不常见) - Object idObj = l.get(0); - if (!(idObj instanceof Number)) return null; - int id = ((Number) idObj).intValue(); - Object posObj = l.get(1); - if (posObj == null) return new ParsedVertex(id, 0f, 0f, true); - } - } else { - // 可能是自定义对象,尝试反射获取 id/pos(容错) - try { - Object idObj = v.getClass().getMethod("getId").invoke(v); - Object px = v.getClass().getMethod("getX").invoke(v); - Object py = v.getClass().getMethod("getY").invoke(v); - int id = idObj instanceof Number ? ((Number) idObj).intValue() : Integer.parseInt(String.valueOf(idObj)); - return new ParsedVertex(id, toFloat(px), toFloat(py), false); - } catch (Throwable ignored) {} + return target; } - } catch (Throwable ignored) {} + } return null; } - // 用于输出 secondary vertex 结果 - private static class SecondaryVertexTarget { - int id; - float x; - float y; - boolean deleted = false; + // [MODIFIED] 计算所有变形顶点的目标状态 + private static List computeAllDeformationVertexTargets(ParametersManagement.Parameter fullParam, String paramId, float current, AnimationParameter animParam) { + List results = new ArrayList<>(); + List allIndicesForParam = findIndicesForParam(fullParam, paramId, animParam); + if (allIndicesForParam.isEmpty()) return results; + + Map> vertexIdxToParamIndices = new HashMap<>(); + for (int paramIdx : allIndicesForParam) { + DeformationVertexTarget parsed = parseDeformationVertexValue(fullParam.value().get(paramIdx)); + if (parsed != null) { + vertexIdxToParamIndices.computeIfAbsent(parsed.idx, k -> new ArrayList<>()).add(paramIdx); + } + } + + for (Map.Entry> entry : vertexIdxToParamIndices.entrySet()) { + int vertexIdx = entry.getKey(); + List paramIndices = entry.getValue(); + + int[] surrounding = findSurroundingKeyframesForIndices(fullParam.keyframe(), paramIndices, current); + int prevParamIdx = surrounding[0], nextParamIdx = surrounding[1]; + + if (prevParamIdx != -1 && nextParamIdx != -1) { + DeformationVertexTarget prevTarget = parseDeformationVertexValue(fullParam.value().get(prevParamIdx)); + DeformationVertexTarget nextTarget = parseDeformationVertexValue(fullParam.value().get(nextParamIdx)); + + if (prevTarget == null || nextTarget == null) continue; + + if (prevParamIdx == nextParamIdx) { + results.add(prevTarget); + } else { + // 如果任意一侧被删除,则不进行插值,而是取前一个关键帧的状态 + if (prevTarget.deleted || nextTarget.deleted) { + results.add(prevTarget); + } else { + float t = computeT(fullParam.keyframe().get(prevParamIdx), fullParam.keyframe().get(nextParamIdx), current); + DeformationVertexTarget interpolated = new DeformationVertexTarget(); + interpolated.idx = vertexIdx; + interpolated.x = prevTarget.x + t * (nextTarget.x - prevTarget.x); + interpolated.y = prevTarget.y + t * (nextTarget.y - prevTarget.y); + results.add(interpolated); + } + } + } else if (prevParamIdx != -1) { + DeformationVertexTarget target = parseDeformationVertexValue(fullParam.value().get(prevParamIdx)); + if (target != null) results.add(target); + } else if (nextParamIdx != -1) { + DeformationVertexTarget target = parseDeformationVertexValue(fullParam.value().get(nextParamIdx)); + if (target != null) results.add(target); + } + } + return results; } /** - * 将 SelectionTool 的四类操作(pivot/scale/rotate/position/secondaryVertex)按当前关键帧插值并应用到 parts。 - * 该方法应在 GL 上下文线程中被调用(即 glContextManager.executeInGLContext 内)。 - * - * 对每个 part:先计算所有目标(如果存在),再一次性按 pivot->scale->rotation->position 的顺序应用, - * 并在最后只做一次 updateMeshVertices/updateBounds。 + * 将变换操作按当前关键帧插值并应用到 parts。 + * 应在 GL 上下文线程中调用。 */ - public static void applyFrameInterpolations(ParametersManagement pm, - List parts, - AnimationParameter currentAnimationParameter, - Logger logger) { + public static void applyFrameInterpolations(ParametersManagement pm, List parts, AnimationParameter currentAnimationParameter, Logger logger) { if (pm == null || parts == null || parts.isEmpty() || currentAnimationParameter == null) return; - float current = 0f; - try { - Object v = currentAnimationParameter.getValue(); - current = toFloat(v); - } catch (Exception ex) { - logger.debug("读取当前动画参数值失败,使用0作为默认值", ex); - } + + float current = toFloat(currentAnimationParameter.getValue()); for (ModelPart part : parts) { + if (!pm.getParametersPanel().getSelectParameter().equals(currentAnimationParameter)) continue; + + ParametersManagement.Parameter fullParam = pm.getModelPartParameters(part); + if (fullParam == null) continue; + + float[] targetPivot = null, targetScale = null, targetPosition = null; + Float targetRotation = null; + // [MODIFIED] 使用新的类型和名称 + List dvTargets = null; + + float[] tmp2 = new float[2]; + if (computeVec2Target(fullParam, "pivot", current, tmp2, currentAnimationParameter)) targetPivot = tmp2.clone(); + if (computeVec2Target(fullParam, "scale", current, tmp2, currentAnimationParameter)) targetScale = tmp2.clone(); + if (computeVec2Target(fullParam, "position", current, tmp2, currentAnimationParameter)) targetPosition = tmp2.clone(); + + float[] tmp1 = new float[1]; + if (computeRotationTarget(fullParam, "rotate", current, tmp1, currentAnimationParameter)) targetRotation = tmp1[0]; + + // [MODIFIED] 计算可变形顶点的目标 + dvTargets = computeAllDeformationVertexTargets(fullParam, "deformationVertex", current, currentAnimationParameter); + + if (targetPivot == null && targetScale == null && targetRotation == null && targetPosition == null && (dvTargets == null || dvTargets.isEmpty())) { + continue; + } + try { - if (!pm.getParametersPanel().getSelectParameter().equals(currentAnimationParameter)){ - return; - } - // Full parameter record for this ModelPart (contains lists for all paramIds) - ParametersManagement.Parameter fullParam = pm.getModelPartParameters(part); - if (fullParam == null) { - continue; - } - - // 目标容器(null 表示未设置) - float[] targetPivot = null; - float[] targetScale = null; - Float targetRotation = null; - float[] targetPosition = null; - List svTargets = null; - - // pivot - float[] tmp2 = new float[2]; - // (MODIFIED) Pass currentAnimationParameter - if (computeVec2Target(fullParam, "pivot", current, tmp2, currentAnimationParameter)) { - targetPivot = new float[]{tmp2[0], tmp2[1]}; - } - - // scale - // (MODIFIED) Pass currentAnimationParameter - if (computeVec2Target(fullParam, "scale", current, tmp2, currentAnimationParameter)) { - targetScale = new float[]{tmp2[0], tmp2[1]}; - } - - // rotate - float[] tmp1 = new float[1]; - // (MODIFIED) Pass currentAnimationParameter - if (computeRotationTargetGeneric(fullParam, "rotate", current, tmp1, currentAnimationParameter)) { - targetRotation = tmp1[0]; - } - - // position - // (MODIFIED) Pass currentAnimationParameter - if (computeVec2Target(fullParam, "position", current, tmp2, currentAnimationParameter)) { - targetPosition = new float[]{tmp2[0], tmp2[1]}; - } - - // secondaryVertex: 为每个记录的 vertex id 计算目标位置(包含实时广播) - // (MODIFIED) Pass currentAnimationParameter - List computedSV = computeAllSecondaryVertexTargets(fullParam, "secondaryVertex", current, currentAnimationParameter); - if (computedSV != null && !computedSV.isEmpty()) { - svTargets = computedSV; - } - - // 如果没有任何要修改的,跳过 - if (targetPivot == null && targetScale == null && targetRotation == null && targetPosition == null && (svTargets == null || svTargets.isEmpty())) { - continue; - } - - // 记录当前状态(用于没有 setRotation 方法时计算 delta) - float currentRot = part.getRotation(); - - // 一次性应用:pivot -> scale -> rotation -> position(最小化中间更新) - try { - if (targetPivot != null) { - part.setPivot(targetPivot[0], targetPivot[1]); + if (targetPivot != null) part.setPivot(targetPivot[0], targetPivot[1]); + if (targetScale != null) part.setScale(targetScale[0], targetScale[1]); + if (targetPosition != null) part.setPosition(targetPosition[0], targetPosition[1]); + if (targetRotation != null) { + try { + part.getClass().getMethod("setRotation", float.class).invoke(part, targetRotation); + } catch (NoSuchMethodException e) { + part.rotate(normalizeAngle(targetRotation - part.getRotation())); } + } - if (targetScale != null) { - part.setScale(targetScale[0], targetScale[1]); - } - - if (targetRotation != null) { - try { - Method setRotation = part.getClass().getMethod("setRotation", float.class); - setRotation.invoke(part, targetRotation); - } catch (NoSuchMethodException nsme) { - float delta = normalizeAngle(targetRotation - currentRot); - part.rotate(delta); - } catch (Exception ignored) {} - } - - if (targetPosition != null) { - part.setPosition(targetPosition[0], targetPosition[1]); - } - - // 处理 secondary vertex:对每个计算出的目标,找到对应 mesh 与 vertex id,设置顶点位置或删除 - if (svTargets != null && !svTargets.isEmpty()) { - try { - List meshes = part.getMeshes(); - if (meshes != null) { - for (SecondaryVertexTarget s : svTargets) { - boolean appliedGlobal = false; - for (com.chuangzhou.vivid2D.render.model.util.Mesh2D mesh : meshes) { - if (mesh == null) continue; - try { - boolean applied = false; - // 优先尝试 mesh.getSecondaryVertices() - try { - List svs = - (List) mesh.getClass().getMethod("getSecondaryVertices").invoke(mesh); - if (svs != null) { - com.chuangzhou.vivid2D.render.model.util.SecondaryVertex found = null; - for (com.chuangzhou.vivid2D.render.model.util.SecondaryVertex sv : svs) { - if (sv != null && sv.getId() == s.id) { - found = sv; - break; - } - } - if (found != null) { - if (s.deleted) { - // 尝试通过 mesh 提供的 remove 方法 - try { - mesh.getClass().getMethod("removeSecondaryVertex", com.chuangzhou.vivid2D.render.model.util.SecondaryVertex.class) - .invoke(mesh, found); - } catch (NoSuchMethodException nsme) { - // 没有 remove 方法则尝试 mesh.removeSecondaryVertex(found) via known signature - try { - mesh.getClass().getMethod("removeSecondaryVertexById", int.class).invoke(mesh, s.id); - } catch (Throwable ignore) { - // 最后作为保险:尝试直接从 list 删除(不推荐,但做容错) - try { - svs.remove(found); - } catch (Throwable ignore2) {} - } - } catch (Throwable ignore) {} - } else { - found.setPosition(s.x, s.y); - } - applied = true; - } - } - } catch (NoSuchMethodException nsme) { - // 忽略:没有该方法 - } catch (Throwable ignore2) { - // 忽略运行时异常 - } - - // 回退策略:尝试 getSecondaryVertexById(int) - if (!applied) { - try { - Object svObj = mesh.getClass().getMethod("getSecondaryVertexById", int.class).invoke(mesh, s.id); - if (svObj instanceof com.chuangzhou.vivid2D.render.model.util.SecondaryVertex) { - com.chuangzhou.vivid2D.render.model.util.SecondaryVertex found = (com.chuangzhou.vivid2D.render.model.util.SecondaryVertex) svObj; - if (s.deleted) { - try { - mesh.getClass().getMethod("removeSecondaryVertex", com.chuangzhou.vivid2D.render.model.util.SecondaryVertex.class) - .invoke(mesh, found); - } catch (Throwable ignore) {} - } else { - found.setPosition(s.x, s.y); - } - applied = true; - } - } catch (NoSuchMethodException ignoreMethod) { - // 忽略 - } catch (Throwable ignore3) {} - } - - if (applied) { - appliedGlobal = true; - break; - } - } catch (Throwable ignored) {} + // [MODIFIED] 应用变形顶点更改 + if (dvTargets != null && !dvTargets.isEmpty()) { + for (Mesh2D mesh : part.getMeshes()) { + if (mesh == null) continue; + boolean meshModified = false; + for (DeformationVertexTarget target : dvTargets) { + if (target.idx >= 0 && target.idx < mesh.getVertexCount()) { + Vertex vertex = mesh.getVertexInstance(target.idx); + if (target.deleted) { + // "删除"操作意味着取消其变形资格 + if (vertex.getTag() == VertexTag.DEFORMATION) { + vertex.setTag(VertexTag.DEFAULT); + meshModified = true; } - // 未找到对应 id 的 mesh/vertex 则忽略(容错) - if (!appliedGlobal) { - // 可选: 记录日志方便调试 - logger.debug("未找到二级顶点 id={} 的对应 mesh/vertex", s.id); + } else { + // 更新位置并确保其为可变形顶点 + vertex.position.set(target.x, target.y); + if (vertex.getTag() != VertexTag.DEFORMATION) { + vertex.setTag(VertexTag.DEFORMATION); } + meshModified = true; } } - } catch (Throwable ignored) {} - } - - // 最后统一刷新一次顶点与 bounds,避免频繁刷新导致与交互冲突 - try { - part.updateMeshVertices(); - if (part.getMeshes() != null) { - for (Object m : part.getMeshes()) { - try { - if (m instanceof com.chuangzhou.vivid2D.render.model.util.Mesh2D) { - ((com.chuangzhou.vivid2D.render.model.util.Mesh2D) m).updateBounds(); - } - } catch (Throwable ignored) {} - } } - } catch (Throwable ignored) {} - - } catch (Throwable t) { - logger.debug("应用目标变换时失败(single part): {}", t.getMessage()); + if (meshModified) { + // 如果顶点数据被修改,需要标记mesh为dirty + mesh.markDirty(); + } + } } - } catch (Exception ex) { - logger.error("FrameInterpolator 在应用插值时发生异常", ex); + // 统一刷新 + part.updateMeshVertices(); + for (Mesh2D mesh : part.getMeshes()) { + if (mesh != null) mesh.updateBounds(); + } + + } catch (Exception e) { + logger.error("在对部件 '{}' 应用插值时发生异常", part.getName(), e); } } } -} +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java index 619a4b9..26d3a1c 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java @@ -1,6 +1,6 @@ package com.chuangzhou.vivid2D.render.awt.util; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.Texture; import org.joml.Vector2f; import org.lwjgl.system.MemoryUtil; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java index ad5e6cb..af66065 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java @@ -1,7 +1,7 @@ package com.chuangzhou.vivid2D.render.awt.util; import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import org.joml.Vector2f; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSDImporter.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSDImporter.java index ee78c6d..431fcdf 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSDImporter.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSDImporter.java @@ -2,14 +2,12 @@ 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.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; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java new file mode 100644 index 0000000..b2f6410 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java @@ -0,0 +1,1843 @@ +package com.chuangzhou.vivid2D.render.model; + +import com.chuangzhou.vivid2D.render.ModelRender; +import com.chuangzhou.vivid2D.render.MultiSelectionBoxRenderer; +import com.chuangzhou.vivid2D.render.TextRenderer; +import com.chuangzhou.vivid2D.render.model.util.*; +import com.chuangzhou.vivid2D.render.model.util.manager.RanderToolsManager; +import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; +import com.chuangzhou.vivid2D.render.systems.RenderSystem; +import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; +import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; +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 java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.*; +import java.util.stream.Collectors; + +import org.joml.Vector4f; +import org.lwjgl.opengl.*; +import org.lwjgl.system.MemoryUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 2D网格类,用于存储和管理2D模型的几何数据 + * 支持顶点、UV坐标、索引和变形操作 + * + * @author tzdwindows 7 + */ +public class Mesh2D { + private static final Logger logger = LoggerFactory.getLogger(Mesh2D.class); + // ==================== 网格数据 ==================== + private String name; + //private float[] vertices; // 顶点数据 [x0, y0, x1, y1, ...] + //private float[] uvs; // UV坐标 [u0, v0, u1, v1, ...] + //private float[] originalVertices; // 原始顶点数据(用于变形恢复) + private VertexList activeVertexList; // 当前活动的顶点列表 + private int[] indices; + private ModelPart modelPart; + + // ==================== 渲染属性 ==================== + private Texture texture; + private boolean visible = true; + private int drawMode = TRIANGLES; // 绘制模式 + private int vaoId = -1; + private int vboId = -1; + private int eboId = -1; + private int indexCount = 0; + private boolean uploaded = false; + + // ==================== 状态管理 ==================== + private boolean dirty = true; // 数据是否已修改 + private BoundingBox bounds; + private boolean boundsDirty = true; + private boolean bakedToWorld = false; + private volatile boolean selected = false; + private Vector2f pivot = new Vector2f(0, 0); + private Vector2f originalPivot = new Vector2f(0, 0); + private boolean isSuspension = false; + private boolean isRenderVertices = false; + private final Map statesList = new LinkedHashMap<>(); + + // [NEW] ==================== 变形引擎字段 ==================== + private List deformationControlVertices = new ArrayList<>(); + private Map deformationWeights = new HashMap<>(); + private boolean deformationWeightsDirty = true; + + // ==================== 多选支持 ==================== + private final List multiSelectedParts = new ArrayList<>(); + private final BoundingBox multiSelectionBounds = new BoundingBox(); + private boolean multiSelectionDirty = true; + + // ==================== 液化状态渲染 ==================== + private boolean showLiquifyOverlay = false; + private Vector4f liquifyOverlayColor = new Vector4f(1.0f, 0.5f, 0.0f, 0.3f); // 半透明橙色 + + // ==================== 常量 ==================== + public static final int POINTS = 0; + public static final int LINES = 1; + public static final int LINE_STRIP = 2; + public static final int TRIANGLES = 3; + 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() { + this("unnamed"); + } + + public Mesh2D(String name) { + this.name = name; + this.activeVertexList = new VertexList("KongFuZi"); + this.indices = new int[0]; + this.bounds = new BoundingBox(); + statesList.put("showDeformationVertices",false); + } + + public Mesh2D(String name, float[] vertices, float[] uvs, int[] indices) { + this(name); + setMeshData(vertices, uvs, indices); + } + + + public void setStates(String key, boolean value){ + statesList.put(key,value); + } + + public boolean getStates(String key){ + return statesList.get(key); + } + + // ==================== 变形引擎方法 ==================== + + + /** + * 核心方法:应用变形。 + * 采用直接的仿射组合,这是应用重心坐标的正确方法。 + * 新位置 p' = Σ w_i * c_i',其中 w_i 是权重, c_i' 是控制点的新位置。 + */ + public void applyDeformation() { + if (deformationControlVertices.isEmpty() || activeVertexList.isEmpty()) return; + if (deformationWeightsDirty) { + precomputeDeformationWeights(); + deformationWeightsDirty = false; + } + + for (Map.Entry entry : deformationWeights.entrySet()) { + Vertex vertex = entry.getKey(); + float[] weights = entry.getValue(); + Vector2f newPosition = new Vector2f(0, 0); + Vector2f newUV = new Vector2f(0, 0); + for (int i = 0; i < deformationControlVertices.size(); i++) { + Vertex controlVertex = deformationControlVertices.get(i); + newPosition.add(new Vector2f(controlVertex.position).mul(weights[i])); + newUV.add(new Vector2f(controlVertex.uv).mul(weights[i])); + } + vertex.position.set(newPosition); + vertex.uv.set(newUV); + } + markDirty(); + } + + /** + * [FINAL FIX] 设置变形控制点。这是现在唯一的入口。 + * 它无条件地信任并存储工具传入的【有序】列表。 + * 删除了所有内部排序逻辑。 + * @param controlVertices 工具传入的、已经包含了用户操作顺序的列表 + */ + public void setDeformationControlVertices(List controlVertices) { + this.deformationControlVertices.clear(); + if (controlVertices != null && !controlVertices.isEmpty()) { + if (controlVertices.size() <= 2) { + this.deformationControlVertices.addAll(controlVertices); + } else { + List sortedVertices = new ArrayList<>(controlVertices); + Vector2f center = new Vector2f(0, 0); + for (Vertex v : sortedVertices) { center.add(v.originalPosition); } + center.div(sortedVertices.size()); + sortedVertices.sort(Comparator.comparingDouble(v -> + Math.atan2(v.originalPosition.y - center.y, v.originalPosition.x - center.x) + )); + this.deformationControlVertices.addAll(sortedVertices); + } + } + this.deformationWeightsDirty = true; + logger.info("已设置并排序 {} 个变形控制点。", this.deformationControlVertices.size()); + } + + + + /** + * 预计算权重。对于每个 DEFAULT 顶点,计算其相对于所有控制点的重心坐标。 + * 这是一个简化的实现,适用于凸多边形控制笼。 + */ + private void precomputeDeformationWeights() { + deformationWeights.clear(); + if (deformationControlVertices.size() < 2) { // 至少需要2个点才能形成边 + return; + } + + logger.debug("开始预计算权重..."); + + List controlBoundary = new ArrayList<>(); + for (Vertex v : this.deformationControlVertices) { + controlBoundary.add(v.originalPosition); + } + + int count = 0; + for (Vertex vertex : activeVertexList) { + if (vertex.getTag() == VertexTag.DEFAULT) { + + boolean isOnBoundary = false; + // [CRUCIAL ADDITION] 步骤 1: 检查顶点是否在控制多边形的某条边上 + for (int i = 0; i < deformationControlVertices.size(); i++) { + Vertex control1 = deformationControlVertices.get(i); + Vertex control2 = deformationControlVertices.get((i + 1) % deformationControlVertices.size()); + + if (isPointOnSegment(vertex.originalPosition, control1.originalPosition, control2.originalPosition)) { + // 如果在边上,使用绝对安全的线性插值计算权重 + float distTotal = control1.originalPosition.distance(control2.originalPosition); + if (distTotal < 1e-6f) continue; + + float distToControl2 = vertex.originalPosition.distance(control2.originalPosition); + + float[] weights = new float[deformationControlVertices.size()]; + weights[i] = distToControl2 / distTotal; + weights[(i + 1) % deformationControlVertices.size()] = 1.0f - weights[i]; + + deformationWeights.put(vertex, weights); + isOnBoundary = true; + count++; + break; // 处理完这个顶点,跳出内层循环 + } + } + + // 如果顶点不在任何边上,再继续进行内部判断和MVC计算 + if (!isOnBoundary) { + if (deformationControlVertices.size() >= 3 && pointInPolygon(vertex.originalPosition, controlBoundary)) { + float[] weights = calculateMeanValueCoordinates(vertex.originalPosition, deformationControlVertices); + deformationWeights.put(vertex, weights); + count++; + } + } + } + } + logger.info("为 {} 个在范围内的顶点预计算了权重。", count); + } + + /** + * [HELPER] 检查点是否在线段上 (包含容差) + */ + private boolean isPointOnSegment(Vector2f p, Vector2f a, Vector2f b) { + float epsilon = 1e-4f; + float crossProduct = (p.y - a.y) * (b.x - a.x) - (p.x - a.x) * (b.y - a.y); + // 检查点是否共线 + if (Math.abs(crossProduct) > epsilon) { + return false; + } + + float dotProduct = (p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y); + // 检查点是否在A点“之后” + if (dotProduct < 0) { + return false; + } + + float squaredLengthBA = (b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y); + // 检查点是否在B点“之前” + return !(dotProduct > squaredLengthBA); + } + + + /** + * [NEW & FIXED] 使用平均值坐标(MVC)为点计算权重。 + */ + private float[] calculateMeanValueCoordinates(Vector2f point, List controls) { + int n = controls.size(); + float[] weights = new float[n]; + float totalWeight = 0; + float epsilon = 1e-7f; // 平滑因子 + + List tanHalfAngles = new ArrayList<>(n); + + for (int i = 0; i < n; i++) { + Vector2f v_i = controls.get(i).originalPosition; + Vector2f v_next = controls.get((i + 1) % n).originalPosition; + + if (point.distance(v_i) < epsilon) { + Arrays.fill(weights, 0); + weights[i] = 1.0f; + return weights; + } + + Vector2f vec1 = v_i.sub(point, new Vector2f()); + Vector2f vec2 = v_next.sub(point, new Vector2f()); + tanHalfAngles.add((float) Math.tan(angleBetween(vec1, vec2) / 2.0)); + } + + for (int i = 0; i < n; i++) { + float dist = point.distance(controls.get(i).originalPosition); + float tan_prev = tanHalfAngles.get((i - 1 + n) % n); + + float weight = (tan_prev + tanHalfAngles.get(i)) / (dist + epsilon); + weights[i] = weight; + totalWeight += weight; + } + + if (Math.abs(totalWeight) > epsilon) { + for (int i = 0; i < n; i++) { + weights[i] /= totalWeight; + } + } + return weights; + } + + + /** + * [HELPER] 检查一个点是否在一个多边形内部(射线法)。 + */ + private boolean pointInPolygon(Vector2f point, List polygon) { + if (polygon == null || polygon.size() < 3) { + return false; + } + + // 计算从第一条边 (p0 -> p1) 到 point 的叉积符号 + float sign = 0; + + for (int i = 0; i < polygon.size(); i++) { + Vector2f p1 = polygon.get(i); + Vector2f p2 = polygon.get((i + 1) % polygon.size()); + + // 计算边向量 (edge = p2 - p1) + float edge_x = p2.x - p1.x; + float edge_y = p2.y - p1.y; + + // 计算从 p1 到 point 的向量 + float point_vec_x = point.x - p1.x; + float point_vec_y = point.y - p1.y; + + // 计算二维叉积 (edge_x * point_vec_y - edge_y * point_vec_x) + float cross_product = edge_x * point_vec_y - edge_y * point_vec_x; + + if (i == 0) { + // 如果是第一条边,记录下叉积的符号 + sign = Math.signum(cross_product); + } else { + // 如果后续的边的叉积符号与第一个不同,那么点就在多边形外部 + if (Math.signum(cross_product) != sign && Math.abs(cross_product) > 1e-6f) { // 添加一个小的容差 + return false; + } + } + } + + // 如果所有边的叉积符号都相同,那么点就在内部(或边上) + return true; + } + + // [HELPER] 计算两个向量之间的有向夹角 + private float angleBetween(Vector2f v1, Vector2f v2) { + // atan2(v1.x*v2.y - v1.y*v2.x, v1.x*v2.x + v1.y*v2.y) + float cross = v1.x * v2.y - v1.y * v2.x; + float dot = v1.dot(v2); + return (float) Math.atan2(cross, dot); + } + + // ==================== 网格数据设置 ==================== + + /** + * 设置网格数据 + */ + public void setMeshData(float[] vertices, float[] uvs, int[] indices) { + if (vertices.length % 2 != 0) { + throw new IllegalArgumentException("Vertices array must have even length (x,y pairs)"); + } + if (uvs.length % 2 != 0) { + throw new IllegalArgumentException("UVs array must have even length (u,v pairs)"); + } + if (vertices.length / 2 != uvs.length / 2) { + throw new IllegalArgumentException("Vertices and UVs must have same number of points"); + } + if (this.activeVertexList == null) { + this.activeVertexList = new VertexList("KongFuZi"); + } + this.activeVertexList.clear(); + int vertexCount = vertices.length / 2; + for (int i = 0; i < vertexCount; i++) { + float x = vertices[i * 2]; + float y = vertices[i * 2 + 1]; + float u = uvs[i * 2]; + float v = uvs[i * 2 + 1]; + this.activeVertexList.add(new Vertex(x, y, u, v)); + } + this.indices = indices.clone(); + this.originalPivot.set(this.pivot); + markDirty(); + } + + /** + * 设置是否为渲染顶点 + */ + public void setRenderVertices(boolean renderVertices) { + isRenderVertices = renderVertices; + } + + public void setModelPart(ModelPart modelPart) { + this.modelPart = modelPart; + } + + /** + * 设置是否显示液化覆盖层 + */ + public void setShowLiquifyOverlay(boolean show) { + this.showLiquifyOverlay = show; + markDirty(); + } + + /** + * 绘制液化状态指示器(在不显示顶点时) + */ + private void drawLiquifyStatusIndicator(BufferBuilder bb) { + BoundingBox bounds = getBounds(); + if (bounds == null || !bounds.isValid()) return; + + float centerX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f; + float centerY = (bounds.getMinY() + bounds.getMaxY()) / 2.0f; + float width = bounds.getWidth(); + float height = bounds.getHeight(); + + // 计算指示器位置(放在网格右上方,不遮挡内容) + float indicatorX = bounds.getMaxX() + Math.max(width, height) * 0.2f; + float indicatorY = bounds.getMaxY() + Math.max(width, height) * 0.1f; + + // 1. 绘制简洁的液化状态圆点 + float dotRadius = Math.max(width, height) * 0.08f; + + // 外圈圆点 + bb.begin(GL11.GL_TRIANGLE_FAN, 16); + bb.setColor(new Vector4f(1.0f, 0.6f, 0.0f, 0.8f)); // 橙色圆点 + bb.vertex(indicatorX, indicatorY, 0f, 0f); // 中心点 + for (int i = 0; i <= 16; i++) { + float angle = (float) (i * 2 * Math.PI / 16); + float x = indicatorX + (float) Math.cos(angle) * dotRadius; + float y = indicatorY + (float) Math.sin(angle) * dotRadius; + bb.vertex(x, y, 0f, 0f); + } + bb.endImmediate(); + + // 内圈白色圆点 + float innerRadius = dotRadius * 0.5f; + bb.begin(GL11.GL_TRIANGLE_FAN, 12); + bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 0.9f)); // 白色内圆 + bb.vertex(indicatorX, indicatorY, 0f, 0f); // 中心点 + for (int i = 0; i <= 12; i++) { + float angle = (float) (i * 2 * Math.PI / 12); + float x = indicatorX + (float) Math.cos(angle) * innerRadius; + float y = indicatorY + (float) Math.sin(angle) * innerRadius; + bb.vertex(x, y, 0f, 0f); + } + bb.endImmediate(); + + // 2. 绘制简洁的画笔图标 + drawSimpleBrushIcon(bb, indicatorX, indicatorY, dotRadius); + + // 3. 绘制简洁的提示文字 + String liquifyText = "Liquify"; + String hintText = "Ctrl: Show Vertices"; + + TextRenderer textRenderer = ModelRender.getTextRenderer(); + if (textRenderer != null) { + float textY = indicatorY + dotRadius + 15f; + + // 主标题 + float titleWidth = textRenderer.getTextWidth(liquifyText); + float titleX = indicatorX - titleWidth / 2.0f; + + // 绘制主标题背景(简洁的圆角效果) + bb.begin(GL11.GL_TRIANGLES, 6); + bb.setColor(new Vector4f(0.0f, 0.0f, 0.0f, 0.6f)); // 半透明黑色背景 + bb.vertex(titleX - 6, textY - 12, 0f, 0f); + bb.vertex(titleX + titleWidth + 6, textY - 12, 0f, 0f); + bb.vertex(titleX + titleWidth + 6, textY + 2, 0f, 0f); + bb.vertex(titleX + titleWidth + 6, textY + 2, 0f, 0f); + bb.vertex(titleX - 6, textY + 2, 0f, 0f); + bb.vertex(titleX - 6, textY - 12, 0f, 0f); + bb.endImmediate(); + + // 绘制主标题 + ModelRender.renderText(liquifyText, titleX, textY, new Vector4f(1.0f, 0.8f, 0.0f, 1.0f)); + + // 提示文字(小字号) + float hintY = textY + 15f; + float hintWidth = textRenderer.getTextWidth(hintText); + float hintX = indicatorX - hintWidth / 2.0f; + + ModelRender.renderText(hintText, hintX, hintY, new Vector4f(0.8f, 0.8f, 0.8f, 0.7f)); + } + } + + /** + * 绘制简洁的画笔图标 + */ + private void drawSimpleBrushIcon(BufferBuilder bb, float centerX, float centerY, float size) { + float iconSize = size * 0.6f; + + // 画笔柄(简单的线条) + bb.begin(GL11.GL_LINES, 2); + bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 0.9f)); + bb.vertex(centerX - iconSize * 0.3f, centerY - iconSize * 0.5f, 0f, 0f); + bb.vertex(centerX, centerY + iconSize * 0.3f, 0f, 0f); + bb.endImmediate(); + + // 画笔头(小三角形) + bb.begin(GL11.GL_TRIANGLES, 3); + bb.setColor(new Vector4f(1.0f, 0.3f, 0.0f, 0.9f)); + bb.vertex(centerX, centerY + iconSize * 0.3f, 0f, 0f); + bb.vertex(centerX - iconSize * 0.2f, centerY + iconSize * 0.6f, 0f, 0f); + bb.vertex(centerX + iconSize * 0.2f, centerY + iconSize * 0.6f, 0f, 0f); + bb.endImmediate(); + } + + /** + * 设置中心点 + */ + public boolean setPivot(float x, float y) { + BoundingBox bounds = getBounds(); + if (x >= bounds.getMinX() && x <= bounds.getMaxX() && + y >= bounds.getMinY() && y <= bounds.getMaxY()) { + this.pivot.set(x, y); + return true; + } + return false; + } + + public Vector2f getOriginalPivot() { + return new Vector2f(originalPivot); + } + + public boolean setOriginalPivot(Vector2f p) { + if (p != null) { + BoundingBox bounds = getBounds(); + if (bounds != null && + p.x >= bounds.getMinX() && p.x <= bounds.getMaxX() && + p.y >= bounds.getMinY() && p.y <= bounds.getMaxY()) { + this.originalPivot.set(p); + markDirty(); + return true; + } + } + return false; + } + + + public boolean setPivot(Vector2f pivot) { + BoundingBox bounds = getBounds(); + if (pivot.x >= bounds.getMinX() && pivot.x <= bounds.getMaxX() && + pivot.y >= bounds.getMinY() && pivot.y <= bounds.getMaxY()) { + this.pivot.set(pivot); + return true; + } + return false; + } + + /** + * 获取中心点 + */ + public Vector2f getPivot() { + return new Vector2f(pivot); + } + + /** + * 移动中心点 + */ + public void movePivot(float dx, float dy) { + float newX = pivot.x + dx; + float newY = pivot.y + dy; + + BoundingBox b = getBounds(); + + if (b != null && newX >= b.getMinX() && newX <= b.getMaxX() + && newY >= b.getMinY() && newY <= b.getMaxY()) { + this.pivot.add(dx, dy); + // 同步原始局部 pivot —— 这里假设 originalPivot 与 pivot 的坐标系一致(多数场景下是这样) + this.originalPivot.add(dx, dy); + markDirty(); + } + } + + + /** + * 创建矩形网格 + */ + public static Mesh2D createQuad(String name, float width, float height) { + float hw = width / 2.0f; + float hh = height / 2.0f; + + float[] vertices = { + -hw, -hh, // 左下 + hw, -hh, // 右下 + hw, hh, // 右上 + -hw, hh // 左上 + }; + + float[] uvs = { + 0.0f, 1.0f, // 左下 + 1.0f, 1.0f, // 右下 + 1.0f, 0.0f, // 右上 + 0.0f, 0.0f // 左上 + }; + + int[] indices = { + 0, 1, 2, // 第一个三角形 + 0, 2, 3 // 第二个三角形 + }; + + return new Mesh2D(name, vertices, uvs, indices); + } + + /** + * 获取原始顶点数据的 float 数组 + * (来自当前激活的列表) + */ + public float[] getOriginalVertices() { + if (activeVertexList == null) return new float[0]; + float[] origVerts = new float[getVertexCount() * 2]; + for (int i = 0; i < getVertexCount(); i++) { + Vertex v = activeVertexList.get(i); // <-- MODIFIED + origVerts[i * 2] = v.originalPosition.x; + origVerts[i * 2 + 1] = v.originalPosition.y; + } + return origVerts; + } + + /** + * 设置原始顶点数据 + * (到当前激活的列表) + */ + public void setOriginalVertices(float[] originalVertices) { + // --- MODIFIED --- + if (activeVertexList == null) return; + if (originalVertices != null && originalVertices.length == getVertexCount() * 2) { + for (int i = 0; i < getVertexCount(); i++) { + Vertex v = activeVertexList.get(i); // <-- MODIFIED + v.originalPosition.set(originalVertices[i * 2], originalVertices[i * 2 + 1]); + } + } + // --- END MODIFIED --- + } + + /** + * 设置顶点数据(支持顶点数量变化) + * (到当前激活的列表) + */ + public void setVertices(float[] vertices) { + if (vertices == null) { + throw new IllegalArgumentException("Vertices array cannot be null"); + } + if (vertices.length % 2 != 0) { + throw new IllegalArgumentException("Vertices array must have even length (x,y pairs)"); + } + if (activeVertexList == null) return; // <-- MODIFIED + + // --- MODIFIED --- + int newVertexCount = vertices.length / 2; + int oldVertexCount = getVertexCount(); + + // 如果顶点数量变化 + if (newVertexCount != oldVertexCount) { + List newVertexCollection = new ArrayList<>(newVertexCount); + for (int i = 0; i < newVertexCount; i++) { + float x = vertices[i * 2]; + float y = vertices[i * 2 + 1]; + + if (i < oldVertexCount) { + // 保留旧UV + newVertexCollection.add(new Vertex(x, y, activeVertexList.get(i).uv.x, activeVertexList.get(i).uv.y)); + } else { + // 估算新UV (简单取 0.5, 0.5) + newVertexCollection.add(new Vertex(x, y, 0.5f, 0.5f)); + } + } + // 完全替换活动列表中的顶点 + activeVertexList.clear(); + for(Vertex v : newVertexCollection) { + activeVertexList.add(v); + } + + // 索引也需要重新生成 + regenerateIndicesForNewVertexCount(newVertexCount); + } else { + // 顶点数量不变,仅更新位置 + for (int i = 0; i < newVertexCount; i++) { + activeVertexList.get(i).position.set(vertices[i * 2], vertices[i * 2 + 1]); + } + } + // --- END MODIFIED --- + + markDirty(); + } + + /** + * 为新的顶点数量重新生成索引 + */ + private void regenerateIndicesForNewVertexCount(int newVertexCount) { + // 简单的三角形扇形索引生成 + if (newVertexCount >= 3) { + List newIndices = new ArrayList<>(); + + // 使用三角形扇形(适用于凸多边形) + for (int i = 1; i < newVertexCount - 1; i++) { + newIndices.add(0); + newIndices.add(i); + newIndices.add(i + 1); + } + + this.indices = new int[newIndices.size()]; + for (int i = 0; i < newIndices.size(); i++) { + this.indices[i] = newIndices.get(i); + } + } else { + // 顶点太少,清空索引 + this.indices = new int[0]; + } + } + + /** + * 设置顶点数据(带原始顶点同步) + * (到当前激活的列表) + */ + public void setVertices(float[] vertices, boolean updateOriginal) { + setVertices(vertices); + if (updateOriginal) { + this.saveAsOriginal(); + } + } + + /** + * 创建圆形网格 + */ + public static Mesh2D createCircle(String name, float radius, int segments) { + if (segments < 3) { + segments = 3; + } + + int vertexCount = segments + 1; // 中心点 + 边缘点 + float[] vertices = new float[vertexCount * 2]; + float[] uvs = new float[vertexCount * 2]; + int[] indices = new int[segments * 3]; + + // 中心点 (索引0) + vertices[0] = 0.0f; + vertices[1] = 0.0f; + uvs[0] = 0.5f; + uvs[1] = 0.5f; + + // 边缘点 + float angleStep = (float) (2.0f * Math.PI / segments); + for (int i = 0; i < segments; i++) { + float angle = i * angleStep; + int vertexIndex = (i + 1) * 2; + + vertices[vertexIndex] = (float) Math.cos(angle) * radius; + vertices[vertexIndex + 1] = (float) Math.sin(angle) * radius; + + uvs[vertexIndex] = (float) (Math.cos(angle) * 0.5f + 0.5f); + uvs[vertexIndex + 1] = (float) (Math.sin(angle) * 0.5f + 0.5f); + + // 三角形索引 + int triangleIndex = i * 3; + indices[triangleIndex] = 0; // 中心点 + indices[triangleIndex + 1] = i + 1; + indices[triangleIndex + 2] = (i + 1) % segments + 1; + } + + return new Mesh2D(name, vertices, uvs, indices); + } + + // ==================== 顶点操作 ==================== + + /** + * 添加新顶点到网格末尾 + */ + public void addVertex(float x, float y, float u, float v) { + if (activeVertexList != null) { + activeVertexList.add(new Vertex(x, y, u, v)); + markDirty(); + } + } + + /** + * 在指定位置插入顶点 + */ + public void insertVertex(int index, float x, float y, float u, float v) { + if (activeVertexList == null) return; // <-- MODIFIED + if (index < 0 || index > getVertexCount()) { + throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); + } + + // --- MODIFIED --- + activeVertexList.vertices.add(index, new Vertex(x, y, u, v)); + // --- END MODIFIED --- + + updateIndicesAfterVertexInsertion(index); + markDirty(); + } + + /** + * 移除指定顶点 + */ + public void removeVertex(int index) { + if (activeVertexList == null) return; // <-- MODIFIED + if (index < 0 || index >= getVertexCount()) { + throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); + } + + // --- MODIFIED --- + activeVertexList.remove(index); + // --- END MODIFIED --- + + updateIndicesAfterVertexRemoval(index); + markDirty(); + } + + /** + * 在插入顶点后更新索引数组 + */ + private void updateIndicesAfterVertexInsertion(int insertedIndex) { + if (indices == null) return; + + for (int i = 0; i < indices.length; i++) { + if (indices[i] >= insertedIndex) { + indices[i]++; + } + } + } + + /** + * 在移除顶点后更新索引数组 + */ + private void updateIndicesAfterVertexRemoval(int removedIndex) { + if (indices == null) return; + + // 创建新索引数组,移除引用被删除顶点的三角形 + List newIndicesList = new ArrayList<>(); + for (int i = 0; i < indices.length; i += 3) { + int i1 = indices[i]; + int i2 = indices[i + 1]; + int i3 = indices[i + 2]; + + // 如果三角形包含被删除的顶点,跳过这个三角形 + if (i1 == removedIndex || i2 == removedIndex || i3 == removedIndex) { + continue; + } + + // 调整索引编号 + if (i1 > removedIndex) i1--; + if (i2 > removedIndex) i2--; + if (i3 > removedIndex) i3--; + + newIndicesList.add(i1); + newIndicesList.add(i2); + newIndicesList.add(i3); + } + + // 转换回数组 + this.indices = new int[newIndicesList.size()]; + for (int i = 0; i < newIndicesList.size(); i++) { + this.indices[i] = newIndicesList.get(i); + } + } + + /** + * 获取顶点数量 + */ + public int getVertexCount() { + return (activeVertexList != null) ? activeVertexList.size() : 0; + } + + /** + * 获取顶点位置 + */ + public Vector2f getVertex(int index, Vector2f dest) { + if (index < 0 || index >= getVertexCount()) { + throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); + } + return dest.set(activeVertexList.get(index).position); + } + + public Vector2f getVertex(int index) { + return getVertex(index, new Vector2f()); + } + + /** + * 设置顶点位置 + */ + public void setVertex(int index, float x, float y) { + if (index < 0 || index >= getVertexCount()) { + throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); + } + activeVertexList.get(index).position.set(x, y); + markDirty(); + } + + public void setVertex(int index, Vector2f position) { + setVertex(index, position.x, position.y); + } + + public void setVertex(int index, Vertex vertex){ + activeVertexList.set(index, vertex); + } + + public Vertex getVertexInstance(int index){ + return activeVertexList.get(index); + } + + /** + * 设置该 Mesh 的选中状态(线程安全) + */ + public void setSelected(boolean sel) { + this.selected = sel; + } + + /** + * 查询选中状态 + */ + public boolean isSelected() { + return this.selected; + } + + /** + * 获取UV坐标 + */ + public Vector2f getUV(int index, Vector2f dest) { + if (index < 0 || index >= getVertexCount()) { + throw new IndexOutOfBoundsException("UV index out of bounds: " + index); + } + return dest.set(activeVertexList.get(index).uv); + } + + /** + * 设置UV坐标 + */ + public void setUV(int index, float u, float v) { + if (index < 0 || index >= getVertexCount()) { + throw new IndexOutOfBoundsException("UV index out of bounds: " + index); + } + activeVertexList.get(index).uv.set(u, v); + markDirty(); + } + + // ==================== 变形支持 ==================== + + /** + * 重置为原始顶点数据 + */ + public void resetToOriginal() { + if (activeVertexList != null) { + for (Vertex v : activeVertexList) { + v.resetToOriginal(); + } + markDirty(); + } + } + + /** + * 保存当前顶点为原始数据 + */ + public void saveAsOriginal() { + if (activeVertexList == null || modelPart == null) return; + Matrix3f inverseWorldTransform = modelPart.getWorldTransform().invert(new Matrix3f()); + for (Vertex v : activeVertexList) { + Vector2f currentWorldPos = v.position; + Vector2f newLocalPos = Matrix3fUtils.transformPoint(inverseWorldTransform, currentWorldPos); + v.originalPosition.set(newLocalPos); + // v.originalUv.set(v.uv); // 如果你有 originalUv 字段 + } + for (Vertex controlV : deformationControlVertices) { + Vector2f currentWorldPos = controlV.position; + Vector2f newLocalPos = Matrix3fUtils.transformPoint(inverseWorldTransform, currentWorldPos); + controlV.originalPosition.set(newLocalPos); + } + this.deformationWeightsDirty = true; + } + + + /** + * 应用变形到所有顶点 + */ + public void transformVertices(VertexTransformer transformer) { + if (activeVertexList != null) { + for (int i = 0; i < getVertexCount(); i++) { + Vertex v = activeVertexList.get(i); + transformer.transform(v.position, i); + } + markDirty(); + } + } + + public ModelPart getModelPart() { + return modelPart; + } + + public VertexList getActiveVertexList() { + return activeVertexList; + } + + /** + * 顶点变换器接口 + */ + public interface VertexTransformer { + void transform(Vector2f vertex, int index); + } + + // ==================== 边界计算 ==================== + + /** + * 更新边界框 + */ + public void updateBounds() { + bounds.reset(); + if (activeVertexList != null) { + for (Vertex v : activeVertexList) { + bounds.expand(v.position.x, v.position.y); + } + } + boundsDirty = false; + } + + /** + * 获取边界框 + */ + public BoundingBox getBounds() { + if (boundsDirty) { + updateBounds(); + } + return bounds; + } + + /** + * 检查点是否在网格内(使用边界框近似) + */ + public boolean containsPoint(float x, float y) { + return containsPoint(x, y, false); + } + + /** + * 检查点是否在网格内(可选择精确检测) + */ + public boolean containsPoint(float x, float y, boolean precise) { + //if (isInMultiSelection()) { + // BoundingBox multiBounds = getMultiSelectionBounds(); + // boolean inBounds = x >= multiBounds.getMinX() && x <= multiBounds.getMaxX() && + // y >= multiBounds.getMinY() && y <= multiBounds.getMaxY(); +// + // if (precise && inBounds) { + // // 在多选边界框内时,进一步检查是否在任意选中的网格几何形状内 + // return isPointInAnySelectedMesh(x, y); + // } + // return inBounds; + //} + + BoundingBox b = getBounds(); + boolean inBounds = x >= b.getMinX() && x <= b.getMaxX() && y >= b.getMinY() && y <= b.getMaxY(); + + if (precise && inBounds) { + // 精确检测点是否在网格几何形状内 + return isPointInMeshGeometry(x, y); + } + return inBounds; + } + + /** + * 检查点是否在任意选中的网格几何形状内 + */ + private boolean isPointInAnySelectedMesh(float x, float y) { + // 检查自己 + if (isPointInMeshGeometry(x, y)) { + return true; + } + + // 检查多选列表中的其他网格 + for (Mesh2D mesh : multiSelectedParts) { + if (mesh.isPointInMeshGeometry(x, y)) { + return true; + } + } + return false; + } + + /** + * 精确检测点是否在网格几何形状内(使用射线法) + * (基于当前激活的列表) + */ + private boolean isPointInMeshGeometry(float x, float y) { + // 简单的边界框检测先过滤掉明显不在的 + BoundingBox b = getBounds(); + if (x < b.getMinX() || x > b.getMaxX() || y < b.getMinY() || y > b.getMaxY()) { + return false; + } + + // 使用射线法进行精确检测 + return isPointInPolygon(x, y); // <-- MODIFIED + } + + /** + * 使用射线法判断点是否在多边形内 (新版本) + * (基于当前激活的列表) + */ + private boolean isPointInPolygon(float x, float y) { + // --- MODIFIED --- + if (getVertexCount() < 3) { // 至少需要3个点组成三角形 + return false; + } + + boolean inside = false; + int n = getVertexCount(); + + for (int i = 0, j = n - 1; i < n; j = i++) { + Vertex vi = activeVertexList.get(i); // <-- MODIFIED + Vertex vj = activeVertexList.get(j); // <-- MODIFIED + + float xi = vi.position.x; + float yi = vi.position.y; + float xj = vj.position.x; + float yj = vj.position.y; + if (isPointOnLineSegment(x, y, xi, yi, xj, yj)) { + return true; + } + if (((yi > y) != (yj > y)) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) { + inside = !inside; + } + } + return inside; + } + + /** + * 检查点是否在线段上 + */ + private boolean isPointOnLineSegment(float x, float y, float x1, float y1, float x2, float y2) { + float cross = (x - x1) * (y2 - y1) - (y - y1) * (x2 - x1); + if (Math.abs(cross) > 1e-6) { + return false; // 不在直线上 + } + + float dot = (x - x1) * (x2 - x1) + (y - y1) * (y2 - y1); + if (dot < 0) { + return false; // 在线段起点之前 + } + + float squaredLength = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1); + if (dot > squaredLength) { + return false; // 在线段终点之后 + } + + return true; + } + + public boolean containsPoint(Vector2f point) { + return containsPoint(point.x, point.y); + } + + // ==================== 缓冲区支持 ==================== + + /** + * 获取顶点缓冲区数据 + * (基于当前激活的列表) + */ + public FloatBuffer getVertexBuffer(FloatBuffer buffer) { + int floatCount = getVertexCount() * 2; + if (buffer == null || buffer.remaining() < floatCount) { + throw new IllegalArgumentException("Buffer is null or too small"); + } + buffer.clear(); + if (activeVertexList != null) { + for (Vertex v : activeVertexList) { + buffer.put(v.position.x); + buffer.put(v.position.y); + } + } + buffer.flip(); + return buffer; + } + + public float getX(int index) { + if (index < 0 || index >= getVertexCount()) { + throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); + } + return activeVertexList.get(index).position.x; + } + + public float getY(int index) { + if (index < 0 || index >= getVertexCount()) { + throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); + } + return activeVertexList.get(index).position.y; + } + + /** + * 获取索引缓冲区数据 + */ + public IntBuffer getIndexBuffer(IntBuffer buffer) { + if (buffer == null || buffer.capacity() < indices.length) { + throw new IllegalArgumentException("Buffer is null or too small"); + } + buffer.clear(); + buffer.put(indices); + buffer.flip(); + return buffer; + } + + /** + * 获取交错的顶点+UV数据(用于VBO) + * (基于当前激活的列表) + */ + public FloatBuffer getInterleavedBuffer(FloatBuffer buffer) { + int vertexCount = getVertexCount(); + int floatCount = vertexCount * 4; // 每个顶点:x, y, u, v + if (buffer == null || buffer.remaining() < floatCount) { + throw new IllegalArgumentException("Buffer is null or too small"); + } + buffer.clear(); + if (activeVertexList != null) { + for (int i = 0; i < vertexCount; i++) { + Vertex v = activeVertexList.get(i); + buffer.put(v.position.x); buffer.put(v.position.y); + buffer.put(v.uv.x); buffer.put(v.uv.y); + } + } + buffer.flip(); + return buffer; + } + + // ==================== 状态管理 ==================== + /** + * 标记数据已修改 + */ + public void markDirty() { + this.dirty = true; + this.boundsDirty = true; + this.multiSelectionDirty = true; // 新增:标记多选边界框需要更新 + } + + /** + * 设置当前的“悬停状态” + */ + public void setSuspension(boolean suspension) { + isSuspension = suspension; + } + + /** + * 清除脏标记 + */ + public void markClean() { + this.dirty = false; + } + + /** + * 检查数据是否已修改 + */ + public boolean isDirty() { + return dirty; + } + + /** + * 将网格数据上传到 GPU(生成 VAO/VBO/EBO) + */ + public void uploadToGPU() { + if (uploaded) return; + + RenderSystem.assertOnRenderThread(); + + int vertexCount = getVertexCount(); + if (vertexCount == 0 || indices.length == 0) { + return; + } + + FloatBuffer interleaved = MemoryUtil.memAllocFloat(vertexCount * 4); + IntBuffer ib = MemoryUtil.memAllocInt(indices.length); + try { + getInterleavedBuffer(interleaved); + getIndexBuffer(ib); + + vaoId = RenderSystem.glGenVertexArrays(); + RenderSystem.glBindVertexArray(() -> vaoId); + + vboId = RenderSystem.glGenBuffers(); + RenderSystem.glBindBuffer(RenderSystem.GL_ARRAY_BUFFER, () -> vboId); + RenderSystem.glBufferData(RenderSystem.GL_ARRAY_BUFFER, interleaved, RenderSystem.GL_STATIC_DRAW); + + eboId = RenderSystem.glGenBuffers(); + RenderSystem.glBindBuffer(RenderSystem.GL_ELEMENT_ARRAY_BUFFER, () -> eboId); + RenderSystem.glBufferData(RenderSystem.GL_ELEMENT_ARRAY_BUFFER, ib, RenderSystem.GL_STATIC_DRAW); + + int stride = 4 * Float.BYTES; + RenderSystem.enableVertexAttribArray(0); + RenderSystem.vertexAttribPointer(0, 2, RenderSystem.GL_FLOAT, false, stride, 0); + RenderSystem.enableVertexAttribArray(1); + RenderSystem.vertexAttribPointer(1, 2, RenderSystem.GL_FLOAT, false, stride, 2 * Float.BYTES); + RenderSystem.glBindVertexArray(() -> 0); + + indexCount = indices.length; + uploaded = true; + markClean(); // 上传完成后,将数据标记为干净 + } finally { + MemoryUtil.memFree(interleaved); + MemoryUtil.memFree(ib); + RenderSystem.glBindBuffer(RenderSystem.GL_ARRAY_BUFFER, () -> 0); + } + } + + /** + * 绘制网格(会在第一次绘制时自动上传到 GPU) + */ + public void draw(int shaderProgram, Matrix3f modelMatrix) { + if (!visible) return; + if (indices == null || indices.length == 0) return; + + if (dirty) { + deleteGPU(); + uploadToGPU(); + } else if (!uploaded) { + uploadToGPU(); + } + + // 保存当前 program,必要时恢复 + int prevProgram = RenderSystem.getCurrentProgram(); + boolean switchedProgram = false; + if (shaderProgram != 0 && prevProgram != shaderProgram) { + RenderSystem.useProgram(shaderProgram); + switchedProgram = true; + } + + // 确保纹理绑定到纹理单元0,并通知 shader 使用纹理单元0 + RenderSystem.activeTexture(GL13.GL_TEXTURE0); + if (texture != null) { + texture.bind(); + } else { + RenderSystem.bindTexture(0); + } + if (shaderProgram != 0) { + int texLoc = RenderSystem.getUniformLocation(shaderProgram, "uTexture"); + if (texLoc != -1) RenderSystem.uniform1i(texLoc, 0); + } + + if (shaderProgram != 0) { + int loc = RenderSystem.getUniformLocation(shaderProgram, "uModelMatrix"); + if (loc == -1) loc = RenderSystem.getUniformLocation(shaderProgram, "uModel"); + if (loc != -1) RenderSystem.uniformMatrix3(loc, modelMatrix); + } + + // 绑定 VAO 并绘制 + RenderSystem.glBindVertexArray(() -> vaoId); + RenderSystem.drawElements(RenderSystem.DRAW_TRIANGLES, indexCount, + RenderSystem.GL_UNSIGNED_INT, 0); + RenderSystem.glBindVertexArray(() -> 0); + + // 解绑纹理(恢复到单元0的绑定0) + if (texture != null) { + texture.unbind(); + } else { + RenderSystem.bindTexture(0); + } + + // 恢复之前的 program(如果我们切换过) + if (switchedProgram) { + RenderSystem.useProgram(prevProgram); + } + + // 选中框绘制(需要切换到固色 shader) + if (selected && !isRenderVertices) { + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + RenderSystem.pushState(); + try { + setSolidShader(modelMatrix); + + if (isInMultiSelection()) { + drawMultiSelectionBox(); + } else { + drawSelectBox(); + } + } finally { + RenderSystem.popState(); + } + } + + RanderToolsManager.getInstance().renderAllTools(modelMatrix, this); + + if (isSuspension && !selected) { + RenderSystem.pushState(); + + setSolidShader(modelMatrix); + + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + Tesselator t = Tesselator.getInstance(); + BufferBuilder bb = t.getBuilder(); + + BoundingBox bbox = getBounds(); + if (bbox != null && bbox.isValid()) { + bb.begin(GL11.GL_LINE_LOOP, 4); + bb.setColor(new Vector4f(1f, 0f, 0f, 1f)); + bb.vertex(bbox.getMinX(), bbox.getMinY(), 0f, 0f); + bb.vertex(bbox.getMaxX(), bbox.getMinY(), 0f, 0f); + bb.vertex(bbox.getMaxX(), bbox.getMaxY(), 0f, 0f); + bb.vertex(bbox.getMinX(), bbox.getMaxY(), 0f, 0f); + t.end(); + + String hoverText = getName(); + float textX = bbox.getMaxX() + 5f; + float textY = bbox.getMaxY(); + Vector4f bgColor = new Vector4f(1f, 0f, 0f, 0.8f); + Vector4f fgColor = new Vector4f(1f, 1f, 1f, 1f); + + float lineHeight = 18f; + + List lines = splitLines(hoverText, 30); + + float textHeight = lines.size() * lineHeight; + float textWidth = 0f; + for (String line : lines) { + textWidth = Math.max(textWidth, ModelRender.getTextRenderer().getTextWidth(line)); + } + + bb.begin(GL11.GL_TRIANGLES, 6); + bb.setColor(bgColor); + bb.vertex(textX, textY, 0f, 0f); + bb.vertex(textX + textWidth, textY, 0f, 0f); + bb.vertex(textX + textWidth, textY + textHeight, 0f, 0f); + bb.vertex(textX + textWidth, textY + textHeight, 0f, 0f); + bb.vertex(textX, textY + textHeight, 0f, 0f); + bb.vertex(textX, textY, 0f, 0f); + t.end(); + + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + ModelRender.renderText(line, textX, textY + (i + 1) * lineHeight - 5, fgColor); + } + } + + RenderSystem.popState(); + } + } + + private void setSolidShader(Matrix3f modelMatrix) { + ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader"); + if (solidShader != null && solidShader.programId != 0) { + solidShader.use(); + int modelLoc = solidShader.getUniformLocation("uModelMatrix"); + if (modelLoc != -1) { + RenderSystem.uniformMatrix3(modelLoc, modelMatrix); + } + int colorLoc = solidShader.getUniformLocation("uColor"); + if (colorLoc != -1) { + RenderSystem.uniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); + } + } + } + + public static List splitLines(String text, int maxCharsPerLine) { + List lines = new ArrayList<>(); + StringBuilder line = new StringBuilder(); + + for (String word : text.split(" ")) { + if (line.length() + word.length() + 1 > maxCharsPerLine) { + if (!line.isEmpty()) { + lines.add(line.toString()); + line = new StringBuilder(); + } + if (word.length() > maxCharsPerLine) { + int start = 0; + while (start < word.length()) { + int end = Math.min(start + maxCharsPerLine, word.length()); + lines.add(word.substring(start, end)); + start = end; + } + continue; + } + } + if (!line.isEmpty()) { + line.append(" "); + } + line.append(word); + } + if (!line.isEmpty()) { + lines.add(line.toString()); + } + return lines; + } + + private void drawSelectBox() { + BoundingBox bounds = getBounds(); + MultiSelectionBoxRenderer.drawSelectBox(bounds, pivot); + } + + /** + * 添加网格到多选列表 + */ + public void addToMultiSelection(Mesh2D mesh) { + if (mesh != null && !multiSelectedParts.contains(mesh)) { + multiSelectedParts.add(mesh); + multiSelectionDirty = true; + markDirty(); + } + } + + /** + * 从多选列表移除网格 + */ + public void removeFromMultiSelection(Mesh2D mesh) { + if (multiSelectedParts.remove(mesh)) { + multiSelectionDirty = true; + markDirty(); + } + } + + /** + * 清空多选列表 + */ + public void clearMultiSelection() { + if (!multiSelectedParts.isEmpty()) { + multiSelectedParts.clear(); + multiSelectionDirty = true; + markDirty(); + } + } + + /** + * 获取多选列表 + */ + public List getMultiSelectedParts() { + return new ArrayList<>(multiSelectedParts); + } + + /** + * 检查是否在多选状态 + */ + public boolean isInMultiSelection() { + return !multiSelectedParts.isEmpty(); + } + + /** + * 获取多选状态下的组合边界框 + */ + public BoundingBox getMultiSelectionBounds() { + if (multiSelectionDirty) { + updateMultiSelectionBounds(); + } + return multiSelectionBounds; + } + + /** + * 更新多选边界框 + */ + private void updateMultiSelectionBounds() { + multiSelectionBounds.reset(); + + // 首先包含自己的边界(应用变换后的边界) + BoundingBox selfBounds = getBounds(); + if (selfBounds.isValid()) { + multiSelectionBounds.expand(selfBounds); + } + + // 然后包含所有多选部分的边界(应用它们各自的变换) + for (Mesh2D mesh : multiSelectedParts) { + // 确保其他网格的边界也是最新的 + mesh.updateBounds(); + BoundingBox meshBounds = mesh.getBounds(); + if (meshBounds.isValid()) { + multiSelectionBounds.expand(meshBounds); + } + } + + multiSelectionDirty = false; + } + + /** + * 强制更新多选边界框(在外部变换操作后调用) + */ + public void forceUpdateMultiSelectionBounds() { + multiSelectionDirty = true; + updateMultiSelectionBounds(); + } + + /** + * 检查点是否在多选边界框内 + */ + public boolean multiSelectionContainsPoint(float x, float y) { + if (!isInMultiSelection()) { + return containsPoint(x, y); + } + + BoundingBox multiBounds = getMultiSelectionBounds(); + return multiBounds.contains(x, y); + } + + /** + * 在多选状态下绘制组合边界框 + */ + private void drawMultiSelectionBox() { + BoundingBox multiBounds = getMultiSelectionBounds(); + MultiSelectionBoxRenderer.drawMultiSelectionBox(multiBounds); + } + + /** + * 计算模型的边界框 [minX, minY, maxX, maxY] + */ + public float[] calculateBoundingBox() { + // 使用现有的边界计算功能 + BoundingBox bounds = getBounds(); + return new float[]{ + bounds.getMinX(), + bounds.getMinY(), + bounds.getMaxX(), + bounds.getMaxY() + }; + } + + /** + * 计算带扩展的边界框 [minX, minY, maxX, maxY] + */ + public float[] calculateBoundingBox(float expand) { + float[] bounds = calculateBoundingBox(); + return new float[]{ + bounds[0] - expand, + bounds[1] - expand, + bounds[2] + expand, + bounds[3] + expand + }; + } + public void draw() { + if (!visible) return; + if (indices == null || indices.length == 0) return; + if (!uploaded) { + uploadToGPU(); + } + if (texture != null) { + texture.bind(); + } + GL30.glBindVertexArray(vaoId); + GL11.glDrawElements(GL11.GL_TRIANGLES, indexCount, GL11.GL_UNSIGNED_INT, 0); + GL30.glBindVertexArray(0); + if (texture != null) { + texture.unbind(); + } + } + + /** + * 从 GPU 删除本网格相关的 VAO/VBO/EBO + */ + public void deleteGPU() { + if (!uploaded) return; + // 禁用属性并删除缓冲 + try { + GL30.glBindVertexArray(0); + if (vboId != -1) { + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + GL15.glDeleteBuffers(vboId); + vboId = -1; + } + if (eboId != -1) { + GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0); + GL15.glDeleteBuffers(eboId); + eboId = -1; + } + if (vaoId != -1) { + GL30.glDeleteVertexArrays(vaoId); + vaoId = -1; + } + } catch (Exception ignored) { + // 在某些上下文销毁阶段 GL 调用可能不可用 + } finally { + uploaded = false; + } + } + + // ==================== Getter/Setter ==================== + + public String getName() { + if (modelPart != null){ + return modelPart.getName(); + } + return name; + } + + public void setName(String name) { + this.name = name; + } + + public float[] getVertices() { + if (activeVertexList == null) return new float[0]; + float[] verts = new float[getVertexCount() * 2]; + for (int i = 0; i < getVertexCount(); i++) { + Vertex v = activeVertexList.get(i); + verts[i * 2] = v.position.x; + verts[i * 2 + 1] = v.position.y; + } + return verts; + } + + public float[] getUVs() { + if (activeVertexList == null) return new float[0]; + float[] uvs = new float[getVertexCount() * 2]; + for (int i = 0; i < getVertexCount(); i++) { + Vertex v = activeVertexList.get(i); + uvs[i * 2] = v.uv.x; + uvs[i * 2 + 1] = v.uv.y; + } + return uvs; + } + + public int[] getIndices() { + return indices.clone(); + } + + public Texture getTexture() { + return texture; + } + + public void setTexture(Texture texture) { + this.texture = texture; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public int getDrawMode() { + return drawMode; + } + + public void setDrawMode(int drawMode) { + if (drawMode < POINTS || drawMode > TRIANGLE_FAN) { + throw new IllegalArgumentException("Invalid draw mode: " + drawMode); + } + this.drawMode = drawMode; + } + + public int getIndexCount() { + return indices.length; + } + + // ==================== 工具方法 ==================== + + /** + * 创建网格的深拷贝 + */ + public Mesh2D copy() { + Mesh2D copy = new Mesh2D(name + "_copy"); + copy.indices = this.indices != null ? this.indices.clone() : new int[0]; + + copy.pivot = new Vector2f(this.pivot); + copy.originalPivot = new Vector2f(this.originalPivot); + copy.texture = this.texture; + copy.visible = this.visible; + copy.drawMode = this.drawMode; + copy.bakedToWorld = this.bakedToWorld; + copy.dirty = true; + copy.boundsDirty = true; + copy.selected = this.selected; + return copy; + } + + + public int getVaoId() { + return vaoId; + } + + /** + * 获取绘制模式字符串 + */ + public String getDrawModeString() { + switch (drawMode) { + case POINTS: return "POINTS"; + case LINES: return "LINES"; + case LINE_STRIP: return "LINE_STRIP"; + case TRIANGLES: return "TRIANGLES"; + case TRIANGLE_STRIP: return "TRIANGLE_STRIP"; + case TRIANGLE_FAN: return "TRIANGLE_FAN"; + default: return "UNKNOWN"; + } + } + + /** 标记或查询网格顶点是否已经被烘焙到世界坐标 */ + public void setBakedToWorld(boolean baked) { + this.bakedToWorld = baked; + } + + public boolean isBakedToWorld() { + return bakedToWorld; + } + + + // ==================== Object 方法 ==================== + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Mesh2D mesh2D = (Mesh2D) o; + return visible == mesh2D.visible && + drawMode == mesh2D.drawMode && + Objects.equals(name, mesh2D.name) && + java.util.Arrays.equals(indices, mesh2D.indices) && + Objects.equals(pivot, mesh2D.pivot); + } + + @Override + public int hashCode() { + int result = Objects.hash(name, + pivot, + visible, drawMode); + result = 31 * result + java.util.Arrays.hashCode(indices); + return result; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Mesh2D{") + .append("name='").append(name).append('\'') + .append(", activeList='").append(activeVertexList != null ? activeVertexList.getName() : "null").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(", selected=").append(selected) + .append(", inMultiSelection=").append(isInMultiSelection()) + .append(", multiSelectionCount=").append(multiSelectedParts.size()) + .append(", drawMode=").append(getDrawModeString()) + .append(", bounds=").append(getBounds()); + if (isInMultiSelection()) { + sb.append(", multiSelectionBounds=").append(getMultiSelectionBounds()); + } + if (activeVertexList != null && !activeVertexList.isEmpty()) { + sb.append(", coordinates=["); + for (int i = 0; i < activeVertexList.size(); i++) { + if (i > 0) sb.append(", "); + Vertex v = activeVertexList.get(i); + sb.append("(") + .append(String.format("%.2f", v.position.x)) + .append(", ") + .append(String.format("%.2f", v.position.y)) + .append(")"); + } + sb.append("]"); + } + + sb.append('}'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java index 9cd44eb..6ed1046 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java @@ -1,6 +1,5 @@ package com.chuangzhou.vivid2D.render.model; -import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData; import com.chuangzhou.vivid2D.render.model.data.ModelData; import com.chuangzhou.vivid2D.render.model.data.ModelMetadata; import com.chuangzhou.vivid2D.render.model.util.*; @@ -8,8 +7,6 @@ import com.chuangzhou.vivid2D.util.ModelDataJsonConverter; import org.joml.Matrix3f; import javax.swing.tree.DefaultMutableTreeNode; -import java.io.FileOutputStream; -import java.io.ObjectOutputStream; import java.util.*; /** 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 5d4af1d..d9b0ddf 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -1,16 +1,17 @@ package com.chuangzhou.vivid2D.render.model; import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal; -import com.chuangzhou.vivid2D.render.model.util.*; +import com.chuangzhou.vivid2D.render.model.util.BoundingBox; +import com.chuangzhou.vivid2D.render.model.util.Deformer; +import com.chuangzhou.vivid2D.render.model.util.Vertex; import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; import org.joml.Matrix3f; import org.joml.Vector2f; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.tree.DefaultMutableTreeNode; import java.util.*; -import java.util.concurrent.CopyOnWriteArrayList; +import javax.swing.tree.DefaultMutableTreeNode; /** * 2D模型部件,支持层级变换和变形器 @@ -44,15 +45,16 @@ public class ModelPart { // ==================== 变形系统 ==================== private final List deformers; private final List liquifyStrokes = new ArrayList<>(); - private final Map parameters; // ==================== 状态标记 ==================== private boolean transformDirty; private boolean boundsDirty; private boolean pivotInitialized; - private final List events = new CopyOnWriteArrayList<>(); + private final List events = new LinkedList<>(); private boolean inMultiSelectionOperation = false; + private boolean startLiquefy =false; + private final Map parameters = new LinkedHashMap<>(); // ====== 液化模式枚举 ====== public enum LiquifyMode { @@ -66,31 +68,6 @@ public class ModelPart { TURBULENCE // 湍流(噪声扰动) } - public AnimationParameter createParameter(String id, float min, float max, float defaultValue) { - AnimationParameter param = new AnimationParameter(id, min, max, defaultValue); - parameters.put(id, param); - return param; - } - - public AnimationParameter getParameter(String id) { - return parameters.get(id); - } - - public Map getParameters() { - return parameters; - } - - public void addParameter(AnimationParameter param) { - parameters.put(param.getId(), param); - } - - public void setParameterValue(String paramId, float value) { - AnimationParameter param = parameters.get(paramId); - if (param != null) { - param.setValue(value); - markTransformDirty(); - } - } // ==================== 构造器 ==================== @@ -103,18 +80,25 @@ public class ModelPart { this.children = new ArrayList<>(); this.meshes = new ArrayList<>(); this.deformers = new ArrayList<>(); + + // 初始化变换属性 this.position = new Vector2f(); this.rotation = 0.0f; this.scale = new Vector2f(1.0f, 1.0f); this.localTransform = new Matrix3f(); this.worldTransform = new Matrix3f(); + + // 初始化渲染属性 this.visible = true; this.blendMode = BlendMode.NORMAL; this.opacity = 1.0f; - parameters = new HashMap<>(); + + // 标记需要更新 this.transformDirty = true; this.boundsDirty = true; + updateLocalTransform(); + // 初始时 worldTransform = localTransform(无父节点时) recomputeWorldTransformRecursive(); } @@ -128,7 +112,7 @@ public class ModelPart { private void triggerEvent(String eventName) { for (ModelEvent event : events) { - event.trigger(eventName, this); + event.trigger(eventName,this); } } @@ -136,6 +120,7 @@ public class ModelPart { * 设置液化状态 */ public void setStartLiquefy(boolean startLiquefy) { + this.startLiquefy = startLiquefy; // 同步到所有网格 for (Mesh2D mesh : meshes) { @@ -424,7 +409,6 @@ public class ModelPart { child.markTransformDirty(); // 确保子节点的 worldTransform 立即更新 child.recomputeWorldTransformRecursive(); - triggerEvent("children"); } /** @@ -437,7 +421,6 @@ public class ModelPart { child.markTransformDirty(); child.recomputeWorldTransformRecursive(); } - triggerEvent("children"); return removed; } @@ -544,20 +527,20 @@ public class ModelPart { if (stroke == null || stroke.points == null) return; LiquifyMode mode = stroke.mode != null ? stroke.mode : LiquifyMode.PUSH; for (LiquifyPoint p : stroke.points) { - applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, mode, stroke.iterations, true); + applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, mode, stroke.iterations,true); } } /** * 对当前部件下所有网格应用液化笔效果(类似 Photoshop 的液化工具)。 * 请在使用前注册在使用addLiquifyStroke方法注册LiquifyStroke - *

+ * * 注意: * - brushCenter 使用世界坐标(与 ModelPart 的世界坐标体系一致)。 * - radius 为画笔半径(像素),strength 为强度(建议范围 0.0 - 1.0,数值越大效果越强)。 * - mode 选择液化操作类型。 * - iterations 为迭代次数(>0),可用来让效果更平滑,默认 1 次即可。 - *

+ * * 该方法会直接修改 mesh 的顶点并更新其边界(mesh.updateBounds)。 */ public void applyLiquify(Vector2f brushCenter, float radius, float strength, LiquifyMode mode, int iterations, boolean createVertices) { @@ -945,7 +928,7 @@ public class ModelPart { float brushArea = brushAreaWidth * brushAreaHeight; // 根据画笔大小和网格复杂度计算顶点密度 - int targetVertexCount = Math.max(4, Math.min(20, (int) (brushArea / (localRadius * localRadius * 0.5f)))); + int targetVertexCount = Math.max(4, Math.min(20, (int)(brushArea / (localRadius * localRadius * 0.5f)))); // 在画笔区域内均匀添加顶点 boolean addedVertices = addUniformVerticesInArea(mesh, newVertices, newUVs, newIndices, @@ -1569,9 +1552,7 @@ public class ModelPart { public void setPosition(float x, float y) { // 防止递归调用 if (inMultiSelectionOperation) { - float deltaX = x - position.x; - float deltaY = y - position.y; - + // 直接执行单选择辑,避免递归 position.set(x, y); markTransformDirty(); updateLocalTransform(); @@ -1582,42 +1563,28 @@ public class ModelPart { mesh.setPivot(worldPivot.x, worldPivot.y); } - syncSecondaryVerticesForPart(this, deltaX, deltaY); + updateMeshVertices(); triggerEvent("position"); return; } - float oldX = position.x; - float oldY = position.y; - - // 多选状态下移动 + // 如果是多选状态下的移动,使用多选移动方法 if (isInMultiSelection() && !getSelectedMeshes().isEmpty()) { - float dx = x - oldX; - float dy = y - oldY; + Vector2f currentPos = getPosition(); + float dx = x - currentPos.x; + float dy = y - currentPos.y; + // 设置标志防止递归 inMultiSelectionOperation = true; try { - moveSelectedMeshes(dx, dy); // 这里会调用 setPosition,但被 inMultiSelectionOperation 拦截 + moveSelectedMeshes(dx, dy); } finally { inMultiSelectionOperation = false; } - - // 更新自身位置 - position.set(x, y); - markTransformDirty(); - updateLocalTransform(); - recomputeWorldTransformRecursive(); - - for (Mesh2D mesh : meshes) { - Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, mesh.getOriginalPivot()); - mesh.setPivot(worldPivot.x, worldPivot.y); - } - - triggerEvent("position"); return; } - // 单选逻辑 + // 原有单选择辑 position.set(x, y); markTransformDirty(); updateLocalTransform(); @@ -1628,157 +1595,44 @@ public class ModelPart { mesh.setPivot(worldPivot.x, worldPivot.y); } - float deltaX = x - oldX; - float deltaY = y - oldY; - - syncSecondaryVerticesForPart(this, deltaX, deltaY); + updateMeshVertices(); triggerEvent("position"); } - /** - * 同步部件下所有网格的二级顶点位置 - */ - private void syncSecondaryVerticesForPart(ModelPart part, float deltaX, float deltaY) { - if (part == null) return; - - List meshes = part.getMeshes(); - if (meshes == null) return; - - for (Mesh2D mesh : meshes) { - if (mesh != null && mesh.isVisible() && mesh.getSecondaryVertexCount() > 0) { - - List 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()) { - syncSecondaryVerticesForPart(child, deltaX, deltaY); - } - } - /** * 更新所有网格的顶点位置以反映当前变换 */ public void updateMeshVertices() { - // 确保世界变换是最新的 - if (transformDirty) { - updateLocalTransform(); - recomputeWorldTransformRecursive(); - } - - // 对每个网格应用当前的世界变换 + recomputeWorldTransformRecursive(); for (Mesh2D mesh : meshes) { - updateMeshVertices(mesh); + if (mesh == null) continue; + for (Vertex vertex : mesh.getActiveVertexList()) { + Vector2f localPoint = vertex.originalPosition; + Vector2f worldPoint = Matrix3fUtils.transformPoint(this.worldTransform, localPoint); + vertex.position.set(worldPoint); + } + mesh.applyDeformation(); + mesh.markDirty(); } - - // 递归更新子部件的网格 for (ModelPart child : children) { child.updateMeshVertices(); } } + /** * 强制更新单个网格的顶点位置 */ private void updateMeshVertices(Mesh2D mesh) { if (mesh == null) return; - // 确保 worldTransform 是最新的 - if (transformDirty) { - updateLocalTransform(); - recomputeWorldTransformRecursive(); + + // 获取原始顶点数据(局部坐标) + float[] originalVertices = mesh.getOriginalVertices(); + if (originalVertices == null || originalVertices.length == 0) { + logger.warn("网格 {} 没有原始顶点数据,无法更新变换", mesh.getName()); + return; } - // 1) 让 mesh 自己把局部顶点一次性转换成渲染缓存(世界坐标) - mesh.syncRenderVerticesFromLocal(this.worldTransform); - // 2) 同步 pivot(不改原始局部数据) - try { - Vector2f origPivot = mesh.getOriginalPivot(); - if (origPivot != null) { - Vector2f worldPivot = Matrix3fUtils.transformPoint(this.worldTransform, origPivot); - mesh.setPivot(worldPivot.x, worldPivot.y); - } - } catch (Exception e) { - logger.warn("更新网格pivot时出错: {}", e.getMessage()); - } - // 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 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; - } - - /** - * 更新木偶控制点的位置 - */ - private void updatePuppetPinsPosition(Mesh2D mesh) { - if (mesh == null) return; - - List puppetPins = mesh.getPuppetPins(); - if (puppetPins.isEmpty()) return; // 确保世界变换是最新的 if (transformDirty) { @@ -1786,21 +1640,38 @@ public class ModelPart { recomputeWorldTransformRecursive(); } - for (PuppetPin pin : puppetPins) { - // 获取控制点的原始局部位置(保持不变!) - Vector2f originalLocalPos = pin.getOriginalPosition(); + int vertexCount = originalVertices.length / 2; - // 将原始局部位置变换到世界坐标(用于显示) - Vector2f worldPos = Matrix3fUtils.transformPoint(worldTransform, originalLocalPos); + // 应用当前世界变换到每个顶点 - 添加边界检查 + for (int i = 0; i < vertexCount; i++) { + if (i * 2 + 1 >= originalVertices.length) { + logger.warn("顶点索引 {} 超出原始顶点数组范围", i); + continue; + } - // 只更新控制点的显示位置,保持原始局部位置不变 - pin.setPosition(worldPos.x, worldPos.y); + Vector2f localPoint = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]); + Vector2f worldPoint = Matrix3fUtils.transformPoint(worldTransform, localPoint); - logger.trace("更新木偶控制点显示位置: 局部({}, {}) -> 世界({}, {})", - originalLocalPos.x, originalLocalPos.y, worldPos.x, worldPos.y); + // 检查目标索引是否有效 + if (i < mesh.getVertexCount()) { + mesh.setVertex(i, worldPoint.x, worldPoint.y); + } else { + logger.warn("顶点索引 {} 超出网格顶点范围 (总顶点数: {})", i, mesh.getVertexCount()); + } } - logger.debug("同步更新了 {} 个木偶控制点的显示位置", puppetPins.size()); + // 同步 mesh 的原始局部 pivot -> 当前世界 pivot + try { + Vector2f origPivot = mesh.getOriginalPivot(); + Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, origPivot); + mesh.setPivot(worldPivot.x, worldPivot.y); + } catch (Exception e) { + logger.warn("更新网格pivot时出错: {}", e.getMessage()); + } + + // 标记网格需要更新 + mesh.markDirty(); + mesh.setBakedToWorld(true); } public void setPosition(Vector2f pos) { @@ -1828,6 +1699,7 @@ 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); } @@ -1836,37 +1708,6 @@ public class ModelPart { updateMeshVertices(); } - /** - * 更新木偶控制点的原始位置 - */ - private void updatePuppetPinsOriginalPosition(Mesh2D mesh, Matrix3f oldWorldTransform, float dx, float dy) { - if (mesh == null) return; - - List puppetPins = mesh.getPuppetPins(); - if (puppetPins.isEmpty()) return; - - for (PuppetPin pin : puppetPins) { - // 获取控制点的当前原始位置(局部坐标) - Vector2f currentOriginalPos = pin.getOriginalPosition(); - - // 将原始位置变换到旧的世界坐标系 - Vector2f oldWorldPos = Matrix3fUtils.transformPoint(oldWorldTransform, currentOriginalPos); - - // 在世界坐标系中应用位移 - Vector2f newWorldPos = new Vector2f(oldWorldPos.x + dx, oldWorldPos.y + dy); - - // 将新的世界位置逆变换回新的局部坐标系 - Vector2f newLocalOriginalPos = Matrix3fUtils.transformPointInverse(this.worldTransform, newWorldPos); - - // 更新控制点的原始位置 - pin.setOriginalPosition(newLocalOriginalPos.x, newLocalOriginalPos.y); - - logger.trace("更新木偶控制点原始位置: 旧局部({}, {}) -> 新局部({}, {})", - currentOriginalPos.x, currentOriginalPos.y, newLocalOriginalPos.x, newLocalOriginalPos.y); - } - } - - /** * 移动部件 */ @@ -1896,7 +1737,7 @@ public class ModelPart { return; } - // 原有单选择辑 - 修复:确保网格顶点被更新 + // 原有单选择辑 Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); this.rotation = radians; markTransformDirty(); @@ -1908,10 +1749,6 @@ public class ModelPart { Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot); mesh.setOriginalPivot(newLocalOriginalPivot); mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); - - // 更新木偶控制点和二级顶点 - updatePuppetPinsOriginalPositionForTransform(mesh, oldWorldTransform); - updateSecondaryVerticesWorldPosition(mesh); } updateMeshVertices(); @@ -1923,16 +1760,9 @@ 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"); } @@ -1950,7 +1780,7 @@ public class ModelPart { return; } - // 原有单选择辑 - 修复:确保网格顶点被更新 + // 原有单选择辑 Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); this.scaleX = sx; this.scaleY = sy; @@ -1964,40 +1794,12 @@ public class ModelPart { Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot); mesh.setOriginalPivot(newLocalOriginalPivot); mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); - - // 更新木偶控制点和二级顶点 - updatePuppetPinsOriginalPositionForTransform(mesh, oldWorldTransform); - updateSecondaryVerticesWorldPosition(mesh); } updateMeshVertices(); triggerEvent("scale"); } - /** - * 为变换操作更新木偶控制点的原始位置 - */ - private void updatePuppetPinsOriginalPositionForTransform(Mesh2D mesh, Matrix3f oldWorldTransform) { - if (mesh == null) return; - - List puppetPins = mesh.getPuppetPins(); - if (puppetPins.isEmpty()) return; - - for (PuppetPin pin : puppetPins) { - // 获取控制点的当前原始位置(局部坐标) - Vector2f currentOriginalPos = pin.getOriginalPosition(); - - // 将原始位置变换到旧的世界坐标系 - Vector2f oldWorldPos = Matrix3fUtils.transformPoint(oldWorldTransform, currentOriginalPos); - - // 将旧的世界位置逆变换回新的局部坐标系 - Vector2f newLocalOriginalPos = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPos); - - // 更新控制点的原始位置 - pin.setOriginalPosition(newLocalOriginalPos.x, newLocalOriginalPos.y); - } - } - public void setScale(float uniformScale) { // 记录旧的世界变换,用于计算 pivot 的相对位置 @@ -2018,7 +1820,6 @@ public class ModelPart { mesh.setOriginalPivot(newLocalOriginalPivot); // 同时更新 mesh 的当前 pivot 到新的世界坐标 mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); - updateSecondaryVerticesWorldPosition(mesh); } updateMeshVertices(); @@ -2069,90 +1870,40 @@ public class ModelPart { public void addMesh(Mesh2D mesh) { if (mesh == null) return; + // 确保拷贝保留原始的纹理引用(copy() 已处理) + //mesh.setTexture(mesh.getTexture()); mesh.setModelPart(this); + // 确保本节点的 worldTransform 是最新的 recomputeWorldTransformRecursive(); - // 1. 保存局部顶点到 originalVertices - float[] localVertices = mesh.getVertices().clone(); - mesh.setOriginalVertices(localVertices); - // 2. 计算世界坐标并写入 *renderVertices*,而不是 + // 保存拷贝的原始(局部)顶点供后续重算 world 顶点使用 + float[] originalVertices = mesh.getVertices().clone(); + mesh.setOriginalVertices(originalVertices); + // 把 originalPivot 保存在 mesh 中(setMeshData 已经初始化 originalPivot) + // 将每个顶点从本地空间变换到世界空间(烘焙到 world) int vc = mesh.getVertexCount(); for (int i = 0; i < vc; i++) { - Vector2f local = new Vector2f(localVertices[i * 2], localVertices[i * 2 + 1]); + Vector2f local = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]); Vector2f worldPt = Matrix3fUtils.transformPoint(this.worldTransform, local); - // mesh.setVertex(i, worldPt.x, worldPt.y); - mesh.setRenderVertex(i, worldPt.x, worldPt.y); + mesh.setVertex(i, worldPt.x, worldPt.y); } - // 3. 同步 pivot + + // 同步 originalPivot -> world pivot(如果 originalPivot 有意义) try { Vector2f origPivot = mesh.getOriginalPivot(); Vector2f worldPivot = Matrix3fUtils.transformPoint(this.worldTransform, origPivot); - mesh.setPivot(worldPivot.x, worldPivot.y); // 现在这个会成功(因为步骤1的修复) - } catch (Exception ignored) { - } - initializePuppetPinsPosition(mesh); + mesh.setPivot(worldPivot.x, worldPivot.y); + } catch (Exception ignored) { } + + // 标记为已烘焙到世界坐标(语义上明确),并确保 bounds/dirty 状态被正确刷新 mesh.setBakedToWorld(true); + + // 确保 GPU 数据在下一次绘制时会被上传(如果当前在渲染线程,也可以直接 uploadToGPU) mesh.markDirty(); + + // 将拷贝加入到本部件 meshes.add(mesh); boundsDirty = true; - triggerEvent("meshes"); - } - - /** - * 更新木偶控制点的位置(修复移动问题) - */ - private void updatePuppetPinsForMovement(Mesh2D mesh, Matrix3f oldWorldTransform, float dx, float dy) { - if (mesh == null) return; - - List puppetPins = mesh.getPuppetPins(); - if (puppetPins.isEmpty()) return; - - for (PuppetPin pin : puppetPins) { - // 获取控制点的当前位置(世界坐标) - Vector2f currentWorldPos = pin.getPosition(); - - // 应用相同的位移到控制点 - Vector2f newWorldPos = new Vector2f(currentWorldPos.x + dx, currentWorldPos.y + dy); - - // 更新控制点的位置 - pin.setPosition(newWorldPos.x, newWorldPos.y); - - // 同时更新控制点的原始位置,保持一致性 - Vector2f currentOriginalPos = pin.getOriginalPosition(); - Vector2f newOriginalPos = new Vector2f(currentOriginalPos.x + dx, currentOriginalPos.y + dy); - pin.setOriginalPosition(newOriginalPos.x, newOriginalPos.y); - - logger.trace("移动木偶控制点: ({}, {}) -> ({}, {})", - currentWorldPos.x, currentWorldPos.y, newWorldPos.x, newWorldPos.y); - } - - logger.debug("移动时同步更新了 {} 个木偶控制点", puppetPins.size()); - } - - /** - * 初始化木偶控制点的位置 - */ - private void initializePuppetPinsPosition(Mesh2D mesh) { - if (mesh == null) return; - - List puppetPins = mesh.getPuppetPins(); - if (puppetPins.isEmpty()) return; - - for (PuppetPin pin : puppetPins) { - // 获取控制点的原始局部位置 - Vector2f originalLocalPos = pin.getOriginalPosition(); - - // 将原始局部位置变换到世界坐标 - Vector2f worldPos = Matrix3fUtils.transformPoint(this.worldTransform, originalLocalPos); - - // 设置控制点的当前位置 - pin.setPosition(worldPos.x, worldPos.y); - - // 同时保存当前位置为原始位置(确保一致性) - pin.saveAsOriginal(); - } - - logger.debug("初始化了 {} 个木偶控制点的位置", puppetPins.size()); } /** @@ -2169,7 +1920,7 @@ public class ModelPart { // 由于 Mesh2D 的 originalPivot 已经存储了其在 ModelPart 局部坐标系中的相对位置, // 我们可以直接将 ModelPart 的新 pivot 赋值给 Mesh2D 的 originalPivot // 然后再通过变换更新 Mesh2D 的实际 pivot - if (!mesh.setOriginalPivot(new Vector2f(x, y))) { + if (!mesh.setOriginalPivot(new Vector2f(x, y))){ return false; } // Mesh2D 的实际 pivot 应该根据 ModelPart 的世界变换来计算 @@ -2219,6 +1970,33 @@ public class ModelPart { return new ArrayList<>(meshes); } + // ==================== 参数管理 ==================== + public AnimationParameter createParameter(String id, float min, float max, float defaultValue) { + AnimationParameter param = new AnimationParameter(id, min, max, defaultValue); + parameters.put(id, param); + return param; + } + + public AnimationParameter getParameter(String id) { + return parameters.get(id); + } + + public void addParameter(AnimationParameter param) { + parameters.put(param.getId(), param); + } + + public void setParameterValue(String paramId, float value) { + AnimationParameter param = parameters.get(paramId); + if (param != null) { + param.setValue(value); + } + } + + public float getParameterValue(String paramId) { + AnimationParameter param = parameters.get(paramId); + return param != null ? param.getValue() : 0.0f; + } + // ==================== 变形器管理 ==================== /** @@ -2283,27 +2061,19 @@ public class ModelPart { * 获取世界空间中的包围盒 */ public BoundingBox getWorldBounds() { - BoundingBox worldBounds = new BoundingBox(); - - for (Mesh2D mesh : meshes) { - // 确保网格的世界边界是最新的 - BoundingBox meshWorldBounds = mesh.getWorldBounds(); - if (meshWorldBounds != null && meshWorldBounds.isValid()) { - worldBounds.expand(meshWorldBounds); - } + if (boundsDirty) { + updateBounds(); } - // 如果没有有效边界,使用局部边界作为备选 - 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); - } + 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); } } @@ -2373,7 +2143,6 @@ public class ModelPart { public void setVisible(boolean visible) { this.visible = visible; - triggerEvent("visible"); } public BlendMode getBlendMode() { @@ -2382,24 +2151,19 @@ public class ModelPart { public void setBlendMode(BlendMode blendMode) { this.blendMode = blendMode; - triggerEvent("blendMode"); } public float getOpacity() { return opacity; } - public float getScaleX() { - return scaleX; + public float getScaleX() { return scaleX; } + public float getScaleY() { return scaleY; } + public Map getParameters() { + return Collections.unmodifiableMap(parameters); } - - public float getScaleY() { - return scaleY; - } - public void setOpacity(float opacity) { this.opacity = Math.max(0.0f, Math.min(1.0f, opacity)); - triggerEvent("opacity"); } public List getDeformers() { @@ -2412,8 +2176,7 @@ public class ModelPart { public float y; public float pressure = 1.0f; - public LiquifyPoint() { - } + public LiquifyPoint() {} public LiquifyPoint(float x, float y) { this.x = x; @@ -2426,17 +2189,9 @@ public class ModelPart { this.pressure = pressure; } - public float getX() { - return x; - } - - public float getY() { - return y; - } - - public float getPressure() { - return pressure; - } + public float getX() { return x; } + public float getY() { return y; } + public float getPressure() { return pressure; } } // ====== 液化笔划数据结构(包含点序列与笔划参数),提供 getter 以便反射读取 ====== @@ -2447,8 +2202,7 @@ public class ModelPart { public int iterations = 1; public List points = new ArrayList<>(); - public LiquifyStroke() { - } + public LiquifyStroke() {} public LiquifyStroke(LiquifyMode mode, float radius, float strength, int iterations) { this.mode = mode; @@ -2457,25 +2211,11 @@ public class ModelPart { this.iterations = iterations; } - public String getMode() { - return mode.name(); - } // PartData 反射时读取字符串也可 - - public float getRadius() { - return radius; - } - - public float getStrength() { - return strength; - } - - public int getIterations() { - return iterations; - } - - public List getPoints() { - return points; - } + public String getMode() { return mode.name(); } // PartData 反射时读取字符串也可 + public float getRadius() { return radius; } + public float getStrength() { return strength; } + public int getIterations() { return iterations; } + public List getPoints() { return points; } public void addPoint(float x, float y, float pressure) { this.points.add(new LiquifyPoint(x, y, pressure)); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java index 75c1f37..3ca54b1 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java @@ -1,25 +1,14 @@ package com.chuangzhou.vivid2D.render.model.data; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.PuppetPin; -import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex; -import org.joml.Vector2f; -import org.joml.Vector4f; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import java.io.Serial; import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -/** - * @author tzdwindows 7 - */ public class MeshData implements Serializable { + @Serial private static final long serialVersionUID = 1L; - // 基础网格数据 public String name; public float[] vertices; public float[] uvs; @@ -28,49 +17,9 @@ public class MeshData implements Serializable { public boolean visible; public int drawMode; - // 原始顶点数据(用于变形恢复) - public float[] originalVertices; - public float[] renderVertices = null; - public String modelPartName = null; // 关联的ModelPart名称 - public boolean isSuspension = false; - public Vector2f previewPoint = null; - - // 变换相关 - public Vector2f pivot; - public Vector2f originalPivot; - - // 选择状态 - public boolean selected; - public boolean bakedToWorld; - - // ==================== 二级顶点支持 ==================== - public List secondaryVertices = new ArrayList<>(); - public boolean showSecondaryVertices = false; - public Vector4f secondaryVertexColor = new Vector4f(0.0f, 1.0f, 0.0f, 1.0f); - public Vector4f selectedSecondaryVertexColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); - public float secondaryVertexSize = 6.0f; - public Integer selectedSecondaryVertexId = null; - - // ==================== 木偶工具支持 ==================== - public List puppetPins = new ArrayList<>(); - public Integer selectedPuppetPinId = null; - public boolean showPuppetPins = true; - public Vector4f puppetPinColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); - public Vector4f selectedPuppetPinColor = new Vector4f(1.0f, 1.0f, 0.0f, 1.0f); - public float puppetPinSize = 8.0f; - - // ==================== 液化状态支持 ==================== - public boolean showLiquifyOverlay = false; - public Vector4f liquifyOverlayColor = new Vector4f(1.0f, 0.5f, 0.0f, 0.3f); - - // ==================== 渲染状态 ==================== - public boolean isRenderVertices = false; - public MeshData() { this.visible = true; this.drawMode = Mesh2D.TRIANGLES; - this.pivot = new Vector2f(0, 0); - this.originalPivot = new Vector2f(0, 0); } public MeshData(Mesh2D mesh) { @@ -81,164 +30,19 @@ public class MeshData implements Serializable { this.indices = mesh.getIndices(); this.visible = mesh.isVisible(); this.drawMode = mesh.getDrawMode(); - this.selected = mesh.isSelected(); - this.bakedToWorld = mesh.isBakedToWorld(); - this.isRenderVertices = mesh.isRenderVertices(); - - // 保存原始顶点数据 - this.originalVertices = mesh.getOriginalVertices(); - this.renderVertices = mesh.getRenderVertices(); // 获取渲染顶点副本 - this.isSuspension = mesh.isSuspension(); - this.previewPoint = mesh.getPreviewPoint() != null ? new Vector2f(mesh.getPreviewPoint()) : null; - if (mesh.getModelPart() != null) { - this.modelPartName = mesh.getModelPart().getName(); - } - - // 保存变换数据 - this.pivot = new Vector2f(mesh.getPivot()); - this.originalPivot = new Vector2f(mesh.getOriginalPivot()); - - // 保存二级顶点数据 - saveSecondaryVertices(mesh); - - // 保存木偶控制点数据 - savePuppetPins(mesh); - - // 保存液化状态 - this.showLiquifyOverlay = mesh.showLiquifyOverlay; - this.liquifyOverlayColor = new Vector4f(mesh.getLiquifyOverlayColor()); if (mesh.getTexture() != null) { this.textureName = mesh.getTexture().getName(); } } - /** - * 保存二级顶点数据 - */ - private void saveSecondaryVertices(Mesh2D mesh) { - List vertices = mesh.getSecondaryVertices(); - SecondaryVertex selectedVertex = mesh.getSelectedSecondaryVertex(); - - for (SecondaryVertex vertex : vertices) { - SecondaryVertexData data = new SecondaryVertexData(vertex); - secondaryVertices.add(data); - - // 记录选中的二级顶点ID - if (selectedVertex != null && selectedVertex.getId() == vertex.getId()) { - selectedSecondaryVertexId = vertex.getId(); - } - } - - this.showSecondaryVertices = mesh.isShowSecondaryVertices(); - this.secondaryVertexColor = new Vector4f(mesh.secondaryVertexColor); - this.selectedSecondaryVertexColor = new Vector4f(mesh.selectedSecondaryVertexColor); - this.secondaryVertexSize = mesh.secondaryVertexSize; - } - - /** - * 保存木偶控制点数据 - */ - private void savePuppetPins(Mesh2D mesh) { - List pins = mesh.getPuppetPins(); - PuppetPin selectedPin = mesh.getSelectedPuppetPin(); - - for (PuppetPin pin : pins) { - PuppetPinData data = new PuppetPinData(pin); - puppetPins.add(data); - - // 记录选中的木偶控制点ID - if (selectedPin != null && selectedPin.getId() == pin.getId()) { - selectedPuppetPinId = pin.getId(); - } - } - - this.showPuppetPins = mesh.getShowPuppetPins(); - this.puppetPinColor = new Vector4f(mesh.getPuppetPinColor()); - this.selectedPuppetPinColor = new Vector4f(mesh.getSelectedPuppetPinColor()); - this.puppetPinSize = mesh.getPuppetPinSize(); - } - public Mesh2D toMesh2D() { Mesh2D mesh = new Mesh2D(name, vertices, uvs, indices); mesh.setVisible(visible); mesh.setDrawMode(drawMode); - mesh.setSelected(selected); - mesh.setBakedToWorld(bakedToWorld); - mesh.setRenderVertices(isRenderVertices); - - // 恢复原始顶点数据 - if (originalVertices != null) { - mesh.setOriginalVertices(originalVertices); - } - - // 恢复变换数据 - mesh.setPivot(pivot); - mesh.setOriginalPivot(originalPivot); - - // 恢复二级顶点 - restoreSecondaryVertices(mesh); - - mesh.setSuspension(this.isSuspension); - if (this.previewPoint != null) { - mesh.setPreviewPoint(new Vector2f(this.previewPoint)); - } - - // 恢复木偶控制点 - restorePuppetPins(mesh); - - // 恢复液化状态 - mesh.setShowLiquifyOverlay(showLiquifyOverlay); - mesh.getLiquifyOverlayColor().set(liquifyOverlayColor); - return mesh; } - /** - * 恢复二级顶点数据 - */ - private void restoreSecondaryVertices(Mesh2D mesh) { - for (SecondaryVertexData data : secondaryVertices) { - SecondaryVertex vertex = mesh.addSecondaryVertex(data.position.x, data.position.y, data.uv.x, data.uv.y); - vertex.setId(data.id); - vertex.setOriginalPosition(data.originalPosition); - - // 恢复选中状态 - if (selectedSecondaryVertexId != null && data.id == selectedSecondaryVertexId) { - mesh.setSelectedSecondaryVertex(vertex); - } - } - - mesh.setShowSecondaryVertices(showSecondaryVertices); - mesh.setSecondaryVertexColor(secondaryVertexColor); - mesh.setSelectedSecondaryVertexColor(selectedSecondaryVertexColor); - mesh.setSecondaryVertexSize(secondaryVertexSize); - } - - /** - * 恢复木偶控制点数据 - */ - private void restorePuppetPins(Mesh2D mesh) { - for (PuppetPinData data : puppetPins) { - PuppetPin pin = mesh.addPuppetPin(data.position.x, data.position.y, data.uv.x, data.uv.y); - pin.setId(data.id); - pin.setOriginalPosition(data.originalPosition); - pin.setInfluenceRadius(data.influenceRadius); - pin.setName(data.name); - - // 恢复权重映射 - pin.getWeightMap().putAll(data.weightMap); - - // 恢复选中状态 - if (selectedPuppetPinId != null && data.id == selectedPuppetPinId) { - mesh.setSelectedPuppetPin(pin); - } - } - - mesh.setShowPuppetPins(showPuppetPins); - // 注意:木偶控制点颜色等设置需要在Mesh2D中添加相应的setter方法 - } - public MeshData copy() { MeshData copy = new MeshData(); copy.name = this.name; @@ -248,187 +52,6 @@ public class MeshData implements Serializable { copy.textureName = this.textureName; copy.visible = this.visible; copy.drawMode = this.drawMode; - copy.selected = this.selected; - copy.bakedToWorld = this.bakedToWorld; - copy.isRenderVertices = this.isRenderVertices; - - copy.renderVertices = this.renderVertices != null ? this.renderVertices.clone() : null; - copy.modelPartName = this.modelPartName; - copy.isSuspension = this.isSuspension; - copy.previewPoint = this.previewPoint != null ? new Vector2f(this.previewPoint) : null; - - // 复制原始顶点数据 - copy.originalVertices = this.originalVertices != null ? this.originalVertices.clone() : null; - - // 复制变换数据 - copy.pivot = new Vector2f(this.pivot); - copy.originalPivot = new Vector2f(this.originalPivot); - - // 复制二级顶点数据 - copy.secondaryVertices = new ArrayList<>(); - for (SecondaryVertexData data : this.secondaryVertices) { - copy.secondaryVertices.add(data.copy()); - } - copy.showSecondaryVertices = this.showSecondaryVertices; - copy.secondaryVertexColor = new Vector4f(this.secondaryVertexColor); - copy.selectedSecondaryVertexColor = new Vector4f(this.selectedSecondaryVertexColor); - copy.secondaryVertexSize = this.secondaryVertexSize; - copy.selectedSecondaryVertexId = this.selectedSecondaryVertexId; - - // 复制木偶控制点数据 - copy.puppetPins = new ArrayList<>(); - for (PuppetPinData data : this.puppetPins) { - copy.puppetPins.add(data.copy()); - } - copy.selectedPuppetPinId = this.selectedPuppetPinId; - copy.showPuppetPins = this.showPuppetPins; - copy.puppetPinColor = new Vector4f(this.puppetPinColor); - copy.selectedPuppetPinColor = new Vector4f(this.selectedPuppetPinColor); - copy.puppetPinSize = this.puppetPinSize; - - // 复制液化状态 - copy.showLiquifyOverlay = this.showLiquifyOverlay; - copy.liquifyOverlayColor = new Vector4f(this.liquifyOverlayColor); - return copy; } - - // ==================== 内部数据类 ==================== - - /** - * 二级顶点数据类(可序列化) - */ - public static class SecondaryVertexData implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - - public int id; - public Vector2f position; - public Vector2f originalPosition; - public Vector2f uv; - public boolean selected; - public ControlShapeData controlShape; - - public SecondaryVertexData() { - this.controlShape = new ControlShapeData(); - } - public SecondaryVertexData(SecondaryVertex vertex) { - this.id = vertex.getId(); - this.position = vertex.getPosition(); - this.originalPosition = vertex.getOriginalPosition(); - this.uv = vertex.getUV(); - this.selected = vertex.isSelected(); - this.controlShape = createControlShapeData(vertex.getControlShape()); - } - - private static ControlShapeData createControlShapeData(SecondaryVertex.ControlShape controlShape) { - return new ControlShapeData(controlShape); - } - - - public SecondaryVertexData copy() { - SecondaryVertexData copy = new SecondaryVertexData(); - copy.id = this.id; - copy.position = new Vector2f(this.position); - copy.originalPosition = new Vector2f(this.originalPosition); - copy.uv = new Vector2f(this.uv); - copy.selected = this.selected; - copy.controlShape = this.controlShape.copy(); - return copy; - } - - /** - * 控制点形状和约束数据类(可序列化),用于封装二级顶点控制属性。 - */ - public static class ControlShapeData implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - public int shapeId; - public boolean pinned; - public boolean locked; - public Vector2f minControlPoint; - public Vector2f maxControlPoint; - - public ControlShapeData() { - this.minControlPoint = new Vector2f(0, 0); - this.maxControlPoint = new Vector2f(0, 0); - } - - /** - * 构造函数:从 SecondaryVertex.ControlShape 对象创建数据 - */ - public ControlShapeData(SecondaryVertex.ControlShape controlShape) { - this.shapeId = controlShape.getShapeId(); - this.pinned = controlShape.isPinned(); - this.locked = controlShape.isLocked(); - this.minControlPoint = controlShape.getMinControlPoint(); - this.maxControlPoint = controlShape.getMaxControlPoint(); - } - - /** - * 创建 ControlShapeData 的深拷贝 - */ - public ControlShapeData copy() { - ControlShapeData copy = new ControlShapeData(); - copy.shapeId = this.shapeId; - copy.pinned = this.pinned; - copy.locked = this.locked; - copy.minControlPoint = new Vector2f(this.minControlPoint); - copy.maxControlPoint = new Vector2f(this.maxControlPoint); - return copy; - } - - @Override - public String toString() { - return String.format("ControlShapeData{id=%d, pinned=%s, locked=%s, min=(%.2f, %.2f), max=(%.2f, %.2f)}", - shapeId, pinned, locked, minControlPoint.x, minControlPoint.y, maxControlPoint.x, maxControlPoint.y); - } - } - } - - /** - * 木偶控制点数据类(可序列化) - */ - public static class PuppetPinData implements Serializable { - @Serial - private static final long serialVersionUID = 1L; - - public int id; - public Vector2f position; - public Vector2f originalPosition; - public Vector2f uv; - public float influenceRadius; - public boolean selected; - public String name; - public Map weightMap; - - public PuppetPinData() { - this.weightMap = new HashMap<>(); - } - - public PuppetPinData(PuppetPin pin) { - this(); - this.id = pin.getId(); - this.position = new Vector2f(pin.getPosition()); - this.originalPosition = new Vector2f(pin.getOriginalPosition()); - this.uv = new Vector2f(pin.getUV()); - this.influenceRadius = pin.getInfluenceRadius(); - this.selected = pin.isSelected(); - this.name = pin.getName(); - this.weightMap = new HashMap<>(pin.getWeightMap()); - } - - public PuppetPinData copy() { - PuppetPinData copy = new PuppetPinData(); - copy.id = this.id; - copy.position = new Vector2f(this.position); - copy.originalPosition = new Vector2f(this.originalPosition); - copy.uv = new Vector2f(this.uv); - copy.influenceRadius = this.influenceRadius; - copy.selected = this.selected; - copy.name = this.name; - copy.weightMap = new HashMap<>(this.weightMap); - return copy; - } - } -} \ No newline at end of file +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelData.java index 4bf9b3b..de74c87 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelData.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/data/ModelData.java @@ -1,6 +1,7 @@ package com.chuangzhou.vivid2D.render.model.data; import com.chuangzhou.vivid2D.render.model.AnimationParameter; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.*; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartData.java index 8ed0620..9b1ece6 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartData.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartData.java @@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.render.model.data; import com.chuangzhou.vivid2D.render.model.AnimationParameter; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import org.joml.Vector2f; import java.io.Serial; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/RotationDeformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/RotationDeformer.java index 1234b81..b70cef5 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/RotationDeformer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/RotationDeformer.java @@ -1,7 +1,7 @@ package com.chuangzhou.vivid2D.render.model.transform; import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import org.joml.Vector2f; import java.util.Map; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/ScaleDeformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/ScaleDeformer.java index aec4de4..0be693d 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/ScaleDeformer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/ScaleDeformer.java @@ -1,7 +1,7 @@ package com.chuangzhou.vivid2D.render.model.transform; import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.SaveVector2f; import org.joml.Vector2f; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/VertexDeformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/VertexDeformer.java index 1e1c69f..73b83ef 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/VertexDeformer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/VertexDeformer.java @@ -1,7 +1,7 @@ package com.chuangzhou.vivid2D.render.model.transform; import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import org.joml.Vector2f; import java.util.ArrayList; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/WaveDeformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/WaveDeformer.java index ca0117a..88a2fb4 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/transform/WaveDeformer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/transform/WaveDeformer.java @@ -1,7 +1,7 @@ package com.chuangzhou.vivid2D.render.model.transform; import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import org.joml.Vector2f; import java.util.Map; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Deformer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Deformer.java index f039855..5a95b9d 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Deformer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Deformer.java @@ -1,5 +1,6 @@ package com.chuangzhou.vivid2D.render.model.util; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import org.joml.Vector2f; import java.util.*; 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 deleted file mode 100644 index b82765f..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java +++ /dev/null @@ -1,3845 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.MultiSelectionBoxRenderer; -import com.chuangzhou.vivid2D.render.model.ModelPart; -import com.chuangzhou.vivid2D.render.model.util.manager.RanderToolsManager; -import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; -import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; -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.Vector4f; -import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL13; -import org.lwjgl.opengl.GL15; -import org.lwjgl.opengl.GL30; -import org.lwjgl.system.MemoryUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.FloatBuffer; -import java.nio.IntBuffer; -import java.util.*; - -/** - * 2D网格类,用于存储和管理2D模型的几何数据 - * 支持顶点、UV坐标、索引和变形操作 - * - * @author tzdwindows 7 - */ -public class Mesh2D { - private static final Logger logger = LoggerFactory.getLogger(Mesh2D.class); - // ==================== 网格数据 ==================== - private String name; - private float[] vertices; // 顶点数据 [x0, y0, x1, y1, ...] - private float[] uvs; // UV坐标 [u0, v0, u1, v1, ...] - private int[] indices; // 索引数据 - private float[] originalVertices; // 原始顶点数据(用于变形恢复) - private ModelPart modelPart; - private float[] renderVertices; - - // ==================== 二级顶点支持 ==================== - private final List secondaryVertices = new ArrayList<>(); - private boolean showSecondaryVertices = false; - public Vector4f secondaryVertexColor = new Vector4f(0.0f, 1.0f, 0.0f, 1.0f); // 绿色二级顶点 - public Vector4f selectedSecondaryVertexColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); // 红色选中的二级顶点 - public float secondaryVertexSize = 6.0f; - private SecondaryVertex selectedSecondaryVertex = null; - - // ==================== 渲染属性 ==================== - private Texture texture; - private boolean visible = true; - private int drawMode = TRIANGLES; // 绘制模式 - private int vaoId = -1; - private int vboId = -1; - private int eboId = -1; - private int indexCount = 0; - private boolean uploaded = false; - - // ==================== 状态管理 ==================== - private boolean dirty = true; // 数据是否已修改 - private BoundingBox bounds; - private boolean boundsDirty = true; - private boolean bakedToWorld = false; - private volatile boolean selected = false; - private Vector2f pivot = new Vector2f(0, 0); - private Vector2f originalPivot = new Vector2f(0, 0); - private boolean isSuspension = false; - private boolean isRenderVertices = false; - - // ==================== 多选支持 ==================== - private final List multiSelectedParts = new ArrayList<>(); - private final BoundingBox multiSelectionBounds = new BoundingBox(); - private boolean multiSelectionDirty = true; - - // ==================== 液化状态渲染 ==================== - public boolean showLiquifyOverlay = false; - private final Vector4f liquifyOverlayColor = new Vector4f(1.0f, 0.5f, 0.0f, 0.3f); // 半透明橙色 - private Vector2f previewPoint = null; - - // ==================== 木偶工具 ==================== - private final List puppetPins = new ArrayList<>(); - private PuppetPin selectedPuppetPin = null; - private boolean showPuppetPins = true; - private final Vector4f puppetPinColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); // 红色控制点 - private final Vector4f selectedPuppetPinColor = new Vector4f(1.0f, 1.0f, 0.0f, 1.0f); // 黄色选中的控制点 - - // ==================== 常量 ==================== - public static final int POINTS = 0; - public static final int LINES = 1; - public static final int LINE_STRIP = 2; - public static final int TRIANGLES = 3; - public static final int TRIANGLE_STRIP = 4; - public static final int TRIANGLE_FAN = 5; - private static final float ROTATION_HANDLE_DISTANCE = 30.0f; - // ==================== 构造器 ==================== - - private static final float SNAP_THRESHOLD = 0.01f; // 靠近判定阈值(按需要调大,比如 0.1f) - public Mesh2D() { - this.name = "unnamed"; - this.vertices = new float[0]; - this.uvs = new float[0]; - this.indices = new int[0]; - this.originalVertices = new float[0]; - this.renderVertices = null; - this.bounds = new BoundingBox(); - this.pivot = new Vector2f(0f, 0f); - this.originalPivot = new Vector2f(0f, 0f); - this.bakedToWorld = false; - this.dirty = true; - } - - public Mesh2D(String name) { - this.name = name; - this.vertices = new float[0]; - this.uvs = new float[0]; - this.indices = new int[0]; - this.bounds = new BoundingBox(); - } - - public Mesh2D(String name, float[] vertices, float[] uvs, int[] indices) { - this(name); - setMeshData(vertices, uvs, indices); - } - - public Vector4f getPuppetPinColor() { - return puppetPinColor; - } - - public PuppetPin getSelectedPuppetPin() { - return selectedPuppetPin; - } - - public Vector4f getSelectedPuppetPinColor() { - return selectedPuppetPinColor; - } - - public float getPuppetPinSize() { - return 8.0f; - } - - public Vector4f getLiquifyOverlayColor() { - return liquifyOverlayColor; - } - - /** - * 设置是否显示木偶控制点 - */ - public void setShowPuppetPins(boolean show) { - this.showPuppetPins = show; - markDirty(); - } - - /** - * 获取渲染用顶点(返回副本以防外部修改) - */ - public float[] getRenderVertices() { - ensureRenderVerticesInitialized(); - return renderVertices != null ? renderVertices.clone() : null; - } - - /** - * 渲染上传器/渲染线程调用此接口获取用于上传到 GPU 的顶点数组。 - * 优先返回 renderVertices(已是世界坐标),否则返回局部顶点副本(兼容旧渲染路径)。 - */ - public float[] getVerticesForUpload() { - ensureRenderVerticesInitialized(); - if (renderVertices != null) return renderVertices; - return vertices != null ? vertices.clone() : null; - } - - /** - * 确保 renderVertices 已初始化并与局部 vertices 长度一致 - */ - private void ensureRenderVerticesInitialized() { - if (this.vertices == null) return; - if (this.renderVertices == null || this.renderVertices.length != this.vertices.length) { - this.renderVertices = this.vertices.clone(); - } - } - - /** - * 设置渲染用顶点(索引以顶点序号计) - * 注意:此方法仅修改渲染缓存,不触碰局部 vertices 或 originalVertices。 - */ - public void setRenderVertex(int index, float x, float y) { - ensureRenderVerticesInitialized(); - if (renderVertices == null) return; - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("Render vertex index out of bounds: " + index); - } - int base = index * 2; - renderVertices[base] = x; - renderVertices[base + 1] = y; - markDirty(); - } - - /** - * 将局部顶点数组一次性转换为世界坐标并写入 renderVertices(由 ModelPart 调用) - * 注意:不修改局部 vertices/originalVertices,只修改 renderVertices(渲染缓存)。 - */ - // Mesh2D.java - - public void syncRenderVerticesFromLocal(Matrix3f worldTransform) { - if (this.vertices == null || this.vertices.length == 0) return; - ensureRenderVerticesInitialized(); - - // 【关键新增】获取 ModelPart 的 Pivot。这是解决偏移的关键。 - ModelPart part = getModelPart(); // 假设 Mesh2D 提供了 getModelPart() 方法 - if (part == null) { - logger.warn("Mesh {} 找不到关联的 ModelPart,无法获取 Pivot。", this.name); - // 如果找不到 Part,则继续使用默认 (0,0) 局部坐标 - } - // 默认 Pivot 补偿为 (0, 0) - Vector2f pivotCompensation = (part != null) ? part.getPivot() : new Vector2f(0, 0); // 假设 ModelPart 有 getPivot() - - try { - int vc = this.vertices.length / 2; - for (int i = 0; i < vc; i++) { - int ix = i * 2; - Vector2f local = new Vector2f(this.vertices[ix], this.vertices[ix + 1]); - - // ---------------------------------------------------------------------------------- - // 【修正 1:形变网格顶点】 - // 如果 ModelPart 变换是绕 Pivot 进行的,那么网格顶点需要相对于 Pivot 进行平移补偿 - Vector2f adjustedLocal = local.sub(pivotCompensation.x, pivotCompensation.y, new Vector2f()); - - Vector2f world = Matrix3fUtils.transformPoint(worldTransform, adjustedLocal); - // ---------------------------------------------------------------------------------- - - renderVertices[ix] = world.x; - renderVertices[ix + 1] = world.y; - } - this.bakedToWorld = true; - this.boundsDirty = false; - - // 额外同步 SecondaryVertex 的世界坐标 - for (SecondaryVertex sv : secondaryVertices) { - Vector2f local = sv.getPosition(); - - // ---------------------------------------------------------------------------------- - // 【修正 2:SecondaryVertex 渲染位置】 - // SecondaryVertex 的局部位置也需要相对于 Pivot 进行平移补偿 - Vector2f adjustedLocal = local.sub(pivotCompensation.x, pivotCompensation.y, new Vector2f()); - - Vector2f world = Matrix3fUtils.transformPoint(worldTransform, adjustedLocal); - // ---------------------------------------------------------------------------------- - - sv.setWorldPosition(world); - } - - markDirty(); - } catch (Exception e) { - logger.error("syncRenderVerticesFromLocal failed for mesh {}: {}", this.name, e.getMessage(), e); - } - } - - /** - * 获取是否渲染顶点模式 - */ - public boolean isRenderVertices() { - return isRenderVertices; - } - - public void setPreviewPoint(Vector2f p) { - if (p == null) this.previewPoint = null; - else this.previewPoint = new Vector2f(p); - } - - public Vector2f getPreviewPoint() { - return previewPoint == null ? null : new Vector2f(previewPoint); - } - - /** - * 添加木偶控制点 - */ - public PuppetPin addPuppetPin(float x, float y, float u, float v) { - PuppetPin pin = new PuppetPin(x, y, u, v); - puppetPins.add(pin); - - // 预计算权重 - pin.precomputeWeights(this); - - markDirty(); - return pin; - } - - /** - * 移除木偶控制点 - */ - public boolean removePuppetPin(PuppetPin pin) { - boolean removed = puppetPins.remove(pin); - if (removed) { - if (selectedPuppetPin == pin) { - selectedPuppetPin = null; - } - markDirty(); - updateVerticesFromPuppetPins(); // 更新顶点位置 - } - return removed; - } - - /** - * 设置选中的木偶控制点 - */ - public void setSelectedPuppetPin(PuppetPin pin) { - if (selectedPuppetPin != null) { - selectedPuppetPin.setSelected(false); - } - - selectedPuppetPin = pin; - - if (selectedPuppetPin != null) { - selectedPuppetPin.setSelected(true); - } - } - - /** - * 通过位置选择木偶控制点 - */ - public PuppetPin selectPuppetPinAt(float x, float y, float tolerance) { - for (int i = puppetPins.size() - 1; i >= 0; i--) { - PuppetPin pin = puppetPins.get(i); - Vector2f pos = pin.getPosition(); - - if (Math.abs(pos.x - x) <= tolerance && Math.abs(pos.y - y) <= tolerance) { - setSelectedPuppetPin(pin); - return pin; - } - } - setSelectedPuppetPin(null); - return null; - } - - /** - * 移动选中的木偶控制点 - 修复版 - */ - public boolean moveSelectedPuppetPin(float dx, float dy) { - if (selectedPuppetPin != null) { - // 如果有活跃的二级顶点变形,提示用户或自动解决冲突 - if (hasActiveSecondaryVertexDeformation()) { - logger.warn("检测到变形冲突:木偶控制点移动时存在活跃的二级顶点变形"); - // 可以选择自动解决冲突或提示用户 - resolveDeformationConflict(true); // 优先使用木偶变形 - } - - selectedPuppetPin.move(dx, dy); - updateVerticesFromPuppetPins(); - return true; - } - return false; - } - - /** - * 保存当前的木偶变形状态 - */ - public void savePuppetDeformationState() { - if (originalVertices == null || originalVertices.length != vertices.length) { - originalVertices = vertices.clone(); - } else { - // 更新原始顶点为当前状态,这样后续的变形会基于当前状态 - System.arraycopy(vertices, 0, originalVertices, 0, vertices.length); - } - - // 同时保存控制点的原始位置 - for (PuppetPin pin : puppetPins) { - pin.saveAsOriginal(); - } - } - - /** - * 检查是否有活跃的木偶变形 - */ - public boolean hasActivePuppetDeformation() { - for (PuppetPin pin : puppetPins) { - Vector2f currentPos = pin.getPosition(); - Vector2f originalPos = pin.getOriginalPosition(); - if (currentPos.distanceSquared(originalPos) > 0.001f) { - return true; - } - } - return false; - } - - /** - * 检查是否有活跃的二级顶点变形 - */ - public boolean hasActiveSecondaryVertexDeformation() { - for (SecondaryVertex vertex : secondaryVertices) { - Vector2f currentPos = vertex.getPosition(); - Vector2f originalPos = vertex.getOriginalPosition(); - if (currentPos.distanceSquared(originalPos) > 0.001f) { - return true; - } - } - return false; - } - - /** - * 基于木偶控制点更新顶点位置(核心算法) - */ - public void updateVerticesFromPuppetPins() { - if (puppetPins.isEmpty() || originalVertices == null) { - return; - } - - // 只有在没有活跃变形时才重置到原始位置 - if (!hasActivePuppetDeformation()) { - System.arraycopy(originalVertices, 0, vertices, 0, vertices.length); - } - - // 应用所有控制点的变形 - boolean hasDeformation = false; - for (PuppetPin pin : puppetPins) { - if (applyPuppetPinDeformation(pin)) { - hasDeformation = true; - } - } - - if (hasDeformation) { - markDirtyForPuppet(); - } - } - - /** - * 应用单个控制点的变形 - 返回是否实际应用了变形 - */ - private boolean applyPuppetPinDeformation(PuppetPin pin) { - Vector2f pinDelta = new Vector2f(pin.getPosition()).sub(pin.getOriginalPosition()); - - // 如果控制点没有移动,跳过 - if (pinDelta.lengthSquared() < 0.001f) { - return false; - } - - Map weightMap = pin.getWeightMap(); - - // 应用变形到每个受影响的顶点 - for (Map.Entry entry : weightMap.entrySet()) { - int vertexIndex = entry.getKey(); - float weight = entry.getValue(); - - // 计算该顶点应该移动的距离 - Vector2f vertexDelta = new Vector2f(pinDelta).mul(weight); - - // 应用变形 - int baseIndex = vertexIndex * 2; - vertices[baseIndex] += vertexDelta.x; - vertices[baseIndex + 1] += vertexDelta.y; - } - - return true; - } - - private void markDirtyForPuppet() { - // 不调用 updateVerticesFromSecondaryVertices(),避免覆盖木偶变形 - deleteGPU(); - this.dirty = true; - this.boundsDirty = true; - this.multiSelectionDirty = true; - } - - /** - * 预计算所有控制点的权重 - */ - public void precomputeAllPuppetWeights() { - for (PuppetPin pin : puppetPins) { - pin.precomputeWeights(this); - } - } - - public boolean getShowPuppetPins() { - return showPuppetPins; - } - - - - // ==================== 网格数据设置 ==================== - - /** - * 设置网格数据 - */ - public void setMeshData(float[] vertices, float[] uvs, int[] indices) { - if (vertices.length % 2 != 0) { - throw new IllegalArgumentException("Vertices array must have even length (x,y pairs)"); - } - if (uvs.length % 2 != 0) { - throw new IllegalArgumentException("UVs array must have even length (u,v pairs)"); - } - if (vertices.length / 2 != uvs.length / 2) { - throw new IllegalArgumentException("Vertices and UVs must have same number of points"); - } - - this.vertices = vertices.clone(); // 局部顶点(用于变形) - this.uvs = uvs.clone(); - this.indices = indices.clone(); - this.originalVertices = vertices.clone(); // 记录 original - - // 初始化渲染缓存(初始与局部顶点一致) - this.renderVertices = vertices.clone(); - - // 将当前 pivot 视为原始(局部)pivot 的初始值 - this.originalPivot.set(this.pivot); - - markDirty(); - } - - /** - * 设置是否为渲染顶点 - */ - public void setRenderVertices(boolean renderVertices) { - isRenderVertices = renderVertices; - } - - public void setModelPart(ModelPart modelPart) { - this.modelPart = modelPart; - } - - /** - * 设置是否显示液化覆盖层 - */ - public void setShowLiquifyOverlay(boolean show) { - this.showLiquifyOverlay = show; - markDirty(); - } - - public boolean isShowLiquifyOverlay() { - return showLiquifyOverlay; - } - - /** - * 设置中心点 - */ - public boolean setPivot(float x, float y) { - BoundingBox bounds = getBounds(); - if (x >= bounds.getMinX() && x <= bounds.getMaxX() && - y >= bounds.getMinY() && y <= bounds.getMaxY()) { - this.pivot.set(x, y); - return true; - } - return false; - } - - public Vector2f getOriginalPivot() { - return new Vector2f(originalPivot); - } - - public boolean setOriginalPivot(Vector2f p) { - if (p != null) { - BoundingBox bounds = calculateOriginalBounds(); - if (bounds != null && - p.x >= bounds.getMinX() && p.x <= bounds.getMaxX() && - p.y >= bounds.getMinY() && p.y <= bounds.getMaxY()) { - this.originalPivot.set(p); - markDirty(); - return true; - } - } - return false; - } - - - public boolean setPivot(Vector2f pivot) { - BoundingBox bounds = getBounds(); - if (pivot.x >= bounds.getMinX() && pivot.x <= bounds.getMaxX() && - pivot.y >= bounds.getMinY() && pivot.y <= bounds.getMaxY()) { - this.pivot.set(pivot); - return true; - } - return false; - } - - /** - * 获取中心点 - */ - public Vector2f getPivot() { - return new Vector2f(pivot); - } - - /** - * 移动中心点 - */ - public void movePivot(float dx, float dy) { - float newX = pivot.x + dx; - float newY = pivot.y + dy; - - BoundingBox b = getBounds(); - - if (b != null && newX >= b.getMinX() && newX <= b.getMaxX() - && newY >= b.getMinY() && newY <= b.getMaxY()) { - this.pivot.add(dx, dy); - // 同步原始局部 pivot —— 这里假设 originalPivot 与 pivot 的坐标系一致(多数场景下是这样) - this.originalPivot.add(dx, dy); - markDirty(); - } - } - - - /** - * 创建矩形网格 - */ - public static Mesh2D createQuad(String name, float width, float height) { - float hw = width / 2.0f; - float hh = height / 2.0f; - - float[] vertices = { - -hw, -hh, // 左下 - hw, -hh, // 右下 - hw, hh, // 右上 - -hw, hh // 左上 - }; - - float[] uvs = { - 0.0f, 1.0f, // 左下 - 1.0f, 1.0f, // 右下 - 1.0f, 0.0f, // 右上 - 0.0f, 0.0f // 左上 - }; - - int[] indices = { - 0, 1, 2, // 第一个三角形 - 0, 2, 3 // 第二个三角形 - }; - - return new Mesh2D(name, vertices, uvs, indices); - } - - public float[] getOriginalVertices() { - return (originalVertices != null) ? originalVertices.clone() : (vertices != null ? vertices.clone() : new float[0]); - } - - /** - * 设置原始顶点数据 - */ - public void setOriginalVertices(float[] originalVertices) { - this.originalVertices = originalVertices != null ? originalVertices.clone() : null; - - // 修改原始数据意味着之前的 renderVertices/烘焙不再可信 - this.renderVertices = null; - this.bakedToWorld = false; - - // 标记脏,触发后续重新计算/上传 - markDirty(); - } - - /** - * 设置顶点数据(支持顶点数量变化) - */ - public void setVertices(float[] vertices) { - if (vertices == null) { - throw new IllegalArgumentException("Vertices array cannot be null"); - } - if (vertices.length % 2 != 0) { - throw new IllegalArgumentException("Vertices array must have even length (x,y pairs)"); - } - - // 允许顶点数量变化,但需要相应地调整UV数组 - if (this.vertices.length != vertices.length) { - // 顶点数量发生了变化,需要调整UV数组 - adjustUVsForNewVertexCount(vertices.length / 2); - } - - this.vertices = vertices.clone(); - markDirty(); - } - - /** - * 根据新的顶点数量调整UV数组 - */ - private void adjustUVsForNewVertexCount(int newVertexCount) { - int currentVertexCount = getVertexCount(); - - if (newVertexCount == currentVertexCount) { - return; // 顶点数量没有变化 - } - - float[] newUVs = new float[newVertexCount * 2]; - - if (newVertexCount > currentVertexCount) { - // 顶点数量增加,复制现有UV并估算新增顶点的UV - System.arraycopy(uvs, 0, newUVs, 0, uvs.length); - - // 为新增的顶点估算UV坐标 - for (int i = currentVertexCount; i < newVertexCount; i++) { - int uvIndex = i * 2; - // 使用边界框来估算UV - BoundingBox bounds = getBounds(); - if (bounds != null && bounds.isValid()) { - float u = 0.5f; // 默认值 - float v = 0.5f; // 默认值 - newUVs[uvIndex] = u; - newUVs[uvIndex + 1] = v; - } else { - newUVs[uvIndex] = 0.5f; - newUVs[uvIndex + 1] = 0.5f; - } - } - } else { - // 顶点数量减少,截断UV数组 - System.arraycopy(uvs, 0, newUVs, 0, newUVs.length); - } - - this.uvs = newUVs; - - // 如果顶点数量变化很大,可能需要重新生成索引 - if (Math.abs(newVertexCount - currentVertexCount) > 2) { - regenerateIndicesForNewVertexCount(newVertexCount); - } - } - - /** - * 为新的顶点数量重新生成索引 - */ - private void regenerateIndicesForNewVertexCount(int newVertexCount) { - // 简单的三角形扇形索引生成 - if (newVertexCount >= 3) { - List newIndices = new ArrayList<>(); - - // 使用三角形扇形(适用于凸多边形) - for (int i = 1; i < newVertexCount - 1; i++) { - newIndices.add(0); - newIndices.add(i); - newIndices.add(i + 1); - } - - this.indices = new int[newIndices.size()]; - for (int i = 0; i < newIndices.size(); i++) { - this.indices[i] = newIndices.get(i); - } - } else { - // 顶点太少,清空索引 - this.indices = new int[0]; - } - } - - /** - * 设置顶点数据(带原始顶点同步) - */ - public void setVertices(float[] vertices, boolean updateOriginal) { - setVertices(vertices); - if (updateOriginal && originalVertices != null && originalVertices.length == vertices.length) { - this.originalVertices = vertices.clone(); - } - } - - /** - * 创建圆形网格 - */ - public static Mesh2D createCircle(String name, float radius, int segments) { - if (segments < 3) { - segments = 3; - } - - int vertexCount = segments + 1; // 中心点 + 边缘点 - float[] vertices = new float[vertexCount * 2]; - float[] uvs = new float[vertexCount * 2]; - int[] indices = new int[segments * 3]; - - // 中心点 (索引0) - vertices[0] = 0.0f; - vertices[1] = 0.0f; - uvs[0] = 0.5f; - uvs[1] = 0.5f; - - // 边缘点 - float angleStep = (float) (2.0f * Math.PI / segments); - for (int i = 0; i < segments; i++) { - float angle = i * angleStep; - int vertexIndex = (i + 1) * 2; - - vertices[vertexIndex] = (float) Math.cos(angle) * radius; - vertices[vertexIndex + 1] = (float) Math.sin(angle) * radius; - - uvs[vertexIndex] = (float) (Math.cos(angle) * 0.5f + 0.5f); - uvs[vertexIndex + 1] = (float) (Math.sin(angle) * 0.5f + 0.5f); - - // 三角形索引 - int triangleIndex = i * 3; - indices[triangleIndex] = 0; // 中心点 - indices[triangleIndex + 1] = i + 1; - indices[triangleIndex + 2] = (i + 1) % segments + 1; - } - - return new Mesh2D(name, vertices, uvs, indices); - } - - // ==================== 二级顶点操作方法 ==================== - - /** - * 添加二级顶点 - */ - public SecondaryVertex addSecondaryVertex(float x, float y, float u, float v) { - SecondaryVertex vertex = new SecondaryVertex(x, y, u, v); - // 初始化当前位置为原始位置,避免新建时 position 默认为 (0,0) 导致网格瞬移至 (0,0) - Vector2f orig = vertex.getOriginalPosition(); - if (orig != null) { - vertex.setPosition(new Vector2f(orig.x, orig.y)); - } - secondaryVertices.add(vertex); - markDirty(); - updateVerticesFromSecondaryVertices(); - return vertex; - } - - public SecondaryVertex addSecondaryVertex(Vector2f position, Vector2f uv) { - return addSecondaryVertex(position.x, position.y, uv.x, uv.y); - } - - public void createDefaultSecondaryVertices() { - updateBounds(); - BoundingBox bounds = getBounds(); - if (bounds == null || !bounds.isValid()) { - logger.warn("无法创建默认二级顶点:边界框无效"); - return; - } - float minX = bounds.getMinX(); - float minY = bounds.getMinY(); - float maxX = bounds.getMaxX(); - float maxY = bounds.getMaxY(); - // 1. 左上角 (0, 0) - addSecondaryVertex(minX, minY, 0.0f, 0.0f); - // 2. 右上角 (1, 0) - addSecondaryVertex(maxX, minY, 1.0f, 0.0f); - // 3. 右下角 (1, 1) - addSecondaryVertex(maxX, maxY, 1.0f, 1.0f); - // 4. 左下角 (0, 1) - addSecondaryVertex(minX, maxY, 0.0f, 1.0f); - logger.info("为网格 {} 创建了四个默认二级顶点", getName()); - markDirty(); - } - - /** - * 在指定位置插入二级顶点 - */ - public SecondaryVertex insertSecondaryVertex(int index, float x, float y, float u, float v) { - if (index < 0 || index > secondaryVertices.size()) { - throw new IndexOutOfBoundsException("Secondary vertex index out of bounds: " + index); - } - SecondaryVertex vertex = new SecondaryVertex(x, y, u, v); - secondaryVertices.add(index, vertex); - markDirty(); - return vertex; - } - - /** - * 移动某个二级顶点(已改动:增加调用 RegionOptimizer,重新分配 controlRadius) - */ - public void moveSecondaryVertex(SecondaryVertex v, float newX, float newY) { - if (v == null) return; - if (v.isLocked()) { - logger.debug("Secondary vertex {} is locked, move ignored", v.getId()); - return; - } - - // 【关键修改:移除或禁用 pinned 状态下的 applyDeltaToMesh 调用】 - // 在工具中,如果拖拽的钉子是 pinned 状态,我们应该阻止它进行任何操作, - // 或者允许它移动但不再触发整个网格的平移。 - if (v.isPinned()) { - // 允许 pinned 钉子移动,但不再触发 applyDeltaToMesh。 - // ModelPart 的平移应该由 SelectionTool/ModelPart 自身更新其 position 字段来驱动。 - v.setPosition(newX, newY); - logger.debug("Moved pinned vertex {} to ({}, {}) without translating whole mesh", v.getId(), newX, newY); - // 即使 pinned 移动,也需要更新形变网格,因为 position 变了 - updateVerticesFromSecondaryVertices(); - markDirty(); - return; - } - - // 否则我们尝试正常移动,同时检查是否碰撞到其他顶点(snap) - // 优先检测是否靠近其他 existing 顶点位置 - Vector2f oldPos = v.getPosition(); // 用于计算 delta,但在 pin 状态已处理 - - for (SecondaryVertex other : secondaryVertices) { - if (other == v) continue; - Vector2f otherPos = other.getPosition(); - if (isClose(newX, newY, otherPos.x, otherPos.y, SNAP_THRESHOLD)) { - // Snap 到 other - v.setPosition(otherPos); - // 把被靠近的那个当作钉子 - other.setPinned(true); - // 把移动到其上的顶点锁定 - v.setLocked(true); - logger.info("SecondaryVertex {} snapped to {}. {} pinned, {} locked.", v.getId(), other.getId(), other.getId(), v.getId()); - - updateVerticesFromSecondaryVertices(); - markDirty(); - return; - } - } - - // 没有 snap,正常移动该点(只移动当前顶点,不影响整块) - v.setPosition(newX, newY); - logger.debug("Moved secondary vertex {} to ({}, {})", v.getId(), newX, newY); - - // 确保非 snap 移动后,形变网格和渲染状态立即更新 - updateVerticesFromSecondaryVertices(); - markDirty(); - } - - /** - * 判断两点是否接近(用于 snap 判定) - */ - private boolean isClose(float ax, float ay, float bx, float by, float thresh) { - float dx = ax - bx; - float dy = ay - by; - return dx * dx + dy * dy <= thresh * thresh; - } - - /** - * 取消所有 pinned/locked 状态(比如用户按下某个解锁命令) - */ - public void unpinAll() { - for (SecondaryVertex sv : secondaryVertices) { - sv.setPinned(false); - sv.setLocked(false); - } - logger.info("All secondary vertices unpinned/unlocked"); - } - - /** - * 把整个网格按 dx,dy 平移(用于移动 pinned 顶点时) - * 会移动: - * - 所有 secondaryVertices 的 position & originalPosition - * - 顶点数组 vertices 与 originalVertices - */ - private void applyDeltaToMesh(float dx, float dy) { - if (dx == 0f && dy == 0f) return; - - // 移动 secondary vertices(当前 pos 和 original pos 都平移) - for (SecondaryVertex sv : secondaryVertices) { - Vector2f p = sv.getPosition(); - p.add(dx, dy); - sv.setPosition(p); - - Vector2f op = sv.getOriginalPosition(); - op.add(dx, dy); - sv.setOriginalPosition(op); - } - - // 移动顶点数组(当前和原始) - if (vertices != null) { - for (int i = 0; i < vertices.length; i += 2) { - vertices[i] += dx; - vertices[i + 1] += dy; - } - } - if (originalVertices != null) { - for (int i = 0; i < originalVertices.length; i += 2) { - originalVertices[i] += dx; - originalVertices[i + 1] += dy; - } - } - - logger.debug("applyDeltaToMesh dx={}, dy={} applied to mesh", dx, dy); - } - - /** - * 新增:添加二级顶点(调用优化器确保半径分配合理) - */ - public void addSecondaryVertex(SecondaryVertex newV) { - if (secondaryVertices == null) return; - secondaryVertices.add(newV); - updateVerticesFromSecondaryVertices(); - } - - - /** - * 移除二级顶点 - */ - public boolean removeSecondaryVertex(SecondaryVertex vertex) { - boolean removed = secondaryVertices.remove(vertex); - if (removed) { - if (selectedSecondaryVertex == vertex) { - selectedSecondaryVertex = null; - } - markDirty(); - } - return removed; - } - - /** - * 通过ID移除二级顶点 - */ - public boolean removeSecondaryVertex(int id) { - return secondaryVertices.removeIf(vertex -> { - if (vertex.getId() == id) { - if (selectedSecondaryVertex != null && selectedSecondaryVertex.getId() == id) { - selectedSecondaryVertex = null; - } - return true; - } - return false; - }); - } - - /** - * 通过索引移除二级顶点 - */ - public SecondaryVertex removeSecondaryVertexAt(int index) { - if (index < 0 || index >= secondaryVertices.size()) { - throw new IndexOutOfBoundsException("Secondary vertex index out of bounds: " + index); - } - SecondaryVertex removed = secondaryVertices.remove(index); - if (selectedSecondaryVertex == removed) { - selectedSecondaryVertex = null; - } - markDirty(); - return removed; - } - - /** - * 清空所有二级顶点 - */ - public void clearSecondaryVertices() { - secondaryVertices.clear(); - selectedSecondaryVertex = null; - markDirty(); - } - - /** - * 获取二级顶点列表 - */ - public List getSecondaryVertices() { - return new ArrayList<>(secondaryVertices); - } - - /** - * 获取二级顶点数量 - */ - public int getSecondaryVertexCount() { - return secondaryVertices.size(); - } - - /** - * 通过ID查找二级顶点 - */ - public SecondaryVertex getSecondaryVertex(int id) { - return secondaryVertices.stream() - .filter(vertex -> vertex.getId() == id) - .findFirst() - .orElse(null); - } - - /** - * 通过索引获取二级顶点 - */ - public SecondaryVertex getSecondaryVertexAt(int index) { - if (index < 0 || index >= secondaryVertices.size()) { - throw new IndexOutOfBoundsException("Secondary vertex index out of bounds: " + index); - } - return secondaryVertices.get(index); - } - - /** - * 设置选中的二级顶点 - */ - public void setSelectedSecondaryVertex(SecondaryVertex vertex) { - // 取消之前选中的顶点 - if (selectedSecondaryVertex != null) { - selectedSecondaryVertex.setSelected(false); - } - - selectedSecondaryVertex = vertex; - - if (selectedSecondaryVertex != null) { - selectedSecondaryVertex.setSelected(true); - } - } - - /** - * 获取选中的二级顶点 - */ - public SecondaryVertex getSelectedSecondaryVertex() { - return selectedSecondaryVertex; - } - - /** - * 通过位置选择二级顶点 - */ - public SecondaryVertex selectSecondaryVertexAt(float x, float y, float tolerance) { - for (int i = secondaryVertices.size() - 1; i >= 0; i--) { - SecondaryVertex vertex = secondaryVertices.get(i); - Vector2f pos = vertex.getPosition(); - - if (Math.abs(pos.x - x) <= tolerance && Math.abs(pos.y - y) <= tolerance) { - setSelectedSecondaryVertex(vertex); - return vertex; - } - } - setSelectedSecondaryVertex(null); - return null; - } - - /** - * 移动选中的二级顶点 - */ - public boolean moveSelectedSecondaryVertex(float dx, float dy) { - if (selectedSecondaryVertex != null) { - selectedSecondaryVertex.move(dx, dy); - markDirty(); - return true; - } - return false; - } - - /** - * 设置是否显示二级顶点 - */ - public void setShowSecondaryVertices(boolean show) { - this.showSecondaryVertices = show; - markDirty(); - } - - /** - * 移动所有二级顶点(与网格同步移动) - */ - public void moveSecondaryVertices(float dx, float dy) { - for (SecondaryVertex vertex : secondaryVertices) { - vertex.move(dx, dy); - } - markDirty(); - } - - /** - * 设置所有二级顶点的位置(绝对位置) - */ - public void setSecondaryVerticesPosition(float x, float y) { - for (SecondaryVertex vertex : secondaryVertices) { - vertex.setPosition(x, y); - } - markDirty(); - } - - /** - * 同步二级顶点到网格边界(当网格移动或变形时调用) - */ - public void syncSecondaryVerticesToBounds() { - if (secondaryVertices.isEmpty()) return; - - BoundingBox bounds = getBounds(); - if (bounds == null || !bounds.isValid()) return; - - // 如果有4个二级顶点(默认的四个角点),将它们同步到边界框的四个角 - if (secondaryVertices.size() == 4) { - float minX = bounds.getMinX(); - float minY = bounds.getMinY(); - float maxX = bounds.getMaxX(); - float maxY = bounds.getMaxY(); - - // 更新四个角点的位置 - secondaryVertices.get(0).setPosition(minX, minY); // 左下 - secondaryVertices.get(1).setPosition(maxX, minY); // 右下 - secondaryVertices.get(2).setPosition(maxX, maxY); // 右上 - secondaryVertices.get(3).setPosition(minX, maxY); // 左上 - } else { - // 对于其他数量的二级顶点,使用相对位置保持 - syncSecondaryVerticesByRelativePosition(); - } - markDirty(); - } - - /** - * 根据相对位置同步二级顶点 - */ - private void syncSecondaryVerticesByRelativePosition() { - BoundingBox currentBounds = getBounds(); - BoundingBox originalBounds = calculateOriginalBounds(); - - if (currentBounds == null || originalBounds == null || - !currentBounds.isValid() || !originalBounds.isValid()) { - return; - } - - float currentWidth = currentBounds.getWidth(); - float currentHeight = currentBounds.getHeight(); - float originalWidth = originalBounds.getWidth(); - float originalHeight = originalBounds.getHeight(); - - if (originalWidth <= 0 || originalHeight <= 0) return; - - // 计算缩放比例 - float scaleX = currentWidth / originalWidth; - float scaleY = currentHeight / originalHeight; - - // 计算平移量 - float translateX = currentBounds.getMinX() - originalBounds.getMinX(); - float translateY = currentBounds.getMinY() - originalBounds.getMinY(); - - // 应用变换到所有二级顶点(**注意:不要修改 secondary 的 originalPosition**, - // 否则会改变用于计算 delta 的基准位置,导致一级顶点整体偏移) - for (SecondaryVertex vertex : secondaryVertices) { - Vector2f originalPos = vertex.getOriginalPosition(); - float newX = originalPos.x * scaleX + translateX; - float newY = originalPos.y * scaleY + translateY; - vertex.setPosition(newX, newY); - } - } - - - /** - * 保存当前二级顶点位置为原始位置(在网格移动后调用) - */ - public void saveCurrentPositionsAsOriginal() { - for (SecondaryVertex vertex : secondaryVertices) { - vertex.saveAsOriginal(); - } - } - - /** - * 根据二级顶点位置更新网格顶点(实现变形效果) - */ - public void updateVerticesFromSecondaryVertices() { - if (secondaryVertices == null || secondaryVertices.isEmpty() || vertices == null || originalVertices == null) { - return; - } - - // 确保顶点数组长度匹配 - if (originalVertices.length != vertices.length) { - logger.warn("原始顶点和当前顶点数组长度不匹配: {} != {}", originalVertices.length, vertices.length); - return; - } - - // 计算原始顶点质心(用于后续抵消整体平移) - int vertCount = originalVertices.length / 2; - if (vertCount == 0) return; - float origCx = 0f, origCy = 0f; - for (int i = 0; i < originalVertices.length; i += 2) { - origCx += originalVertices[i]; - origCy += originalVertices[i + 1]; - } - origCx /= vertCount; - origCy /= vertCount; - - // 根据控制点数量选择策略 - updateVerticesUsingMLS(); - - // 计算变形后顶点的质心 - float newCx = 0f, newCy = 0f; - for (int i = 0; i < vertices.length; i += 2) { - newCx += vertices[i]; - newCy += vertices[i + 1]; - } - newCx /= vertCount; - newCy /= vertCount; - - // 抵消全局平移:把新质心移回原始质心位置 - float tx = origCx - newCx; - float ty = origCy - newCy; - - // 只有在有显著偏移时才应用修正,避免数值抖动 - if (Math.abs(tx) > 1e-6f || Math.abs(ty) > 1e-6f) { - for (int i = 0; i < vertices.length; i += 2) { - vertices[i] += tx; - vertices[i + 1] += ty; - } - logger.debug("纠正整体位移,平移量 ({}, {})", tx, ty); - } - } - - private void updateVerticesUsingMLS() { - try { - int secCount = secondaryVertices.size(); - if (secCount == 0) return; - - // 收集合格控制点:p(original)来自 originalPosition,q(current)来自 position - java.util.List allP_list = new java.util.ArrayList<>(); - java.util.List allQ_list = new java.util.ArrayList<>(); - java.util.List isSelected_list = new java.util.ArrayList<>(); - - for (int i = 0; i < secCount; i++) { - SecondaryVertex sv = secondaryVertices.get(i); - // 仅允许未锁定且未 pinned 的点参与变形(视为合格) - if (sv == null) continue; - if (sv.isLocked() || sv.isPinned()) continue; - - // 注意:getOriginalPosition() 和 getPosition() 返回新的 Vector2f 副本,不要修改它们 - allP_list.add(sv.getOriginalPosition()); - allQ_list.add(sv.getPosition()); - isSelected_list.add(sv.isSelected()); - } - - int eligibleCount = allP_list.size(); - if (eligibleCount == 0) return; - - Vector2f[] allP = allP_list.toArray(new Vector2f[0]); - Vector2f[] allQ = allQ_list.toArray(new Vector2f[0]); - - // 构造用户选区索引(基于当前位置 allQ):优先使用被选中的合格点 - java.util.List selIdx = new java.util.ArrayList<>(); - for (int i = 0; i < eligibleCount; i++) if (isSelected_list.get(i)) selIdx.add(i); - - boolean explicitSelection = !selIdx.isEmpty(); - boolean applyGlobal = !explicitSelection; - if (!explicitSelection) { - for (int i = 0; i < eligibleCount; i++) selIdx.add(i); - } - - // 用当前位置 allQ 构造去重后的 ptsQ 与索引映射 - java.util.List ptsQ = new java.util.ArrayList<>(); - java.util.List idxMap = new java.util.ArrayList<>(); - java.util.Set seen = new java.util.HashSet<>(); - for (int idx : selIdx) { - Vector2f cur = allQ[idx]; - String key = String.format("%.6f_%.6f", cur.x, cur.y); - if (seen.contains(key)) continue; - seen.add(key); - ptsQ.add(new Vector2f(cur.x, cur.y)); - idxMap.add(idx); - } - - // 若显式选择且点小于3,尽量扩充到可用合格点(基于当前位置) - if (explicitSelection && ptsQ.size() < 3) { - for (int i = 0; i < eligibleCount && ptsQ.size() < 3; i++) { - String key = String.format("%.6f_%.6f", allQ[i].x, allQ[i].y); - if (seen.contains(key)) continue; - seen.add(key); - ptsQ.add(new Vector2f(allQ[i].x, allQ[i].y)); - idxMap.add(i); - } - } - - // 构建 polygon(按极角排序),若自交则退到凸包(均基于当前位置) - java.util.List poly = new java.util.ArrayList<>(); - java.util.List polyIdx = new java.util.ArrayList<>(); - if (explicitSelection && ptsQ.size() >= 3) { - Vector2f cen = new Vector2f(0f, 0f); - for (Vector2f v : ptsQ) { cen.x += v.x; cen.y += v.y; } - cen.x /= ptsQ.size(); cen.y /= ptsQ.size(); - - java.util.List order = new java.util.ArrayList<>(); - for (int i = 0; i < ptsQ.size(); i++) order.add(i); - order.sort((a, b) -> { - double anga = Math.atan2(ptsQ.get(a).y - cen.y, ptsQ.get(a).x - cen.x); - double angb = Math.atan2(ptsQ.get(b).y - cen.y, ptsQ.get(b).x - cen.x); - return Double.compare(anga, angb); - }); - - for (int oi : order) { - poly.add(ptsQ.get(oi)); - polyIdx.add(idxMap.get(oi)); // 这些索引对应 allP/allQ 数组 - } - - if (polygonIsSelfIntersecting(poly)) { - java.util.List hull = convexHull(ptsQ); - java.util.List hullIdx = new java.util.ArrayList<>(); - for (Vector2f hp : hull) { - int found = -1; - for (int i = 0; i < ptsQ.size(); i++) { - if (Math.abs(ptsQ.get(i).x - hp.x) < 1e-6f && Math.abs(ptsQ.get(i).y - hp.y) < 1e-6f) { - found = idxMap.get(i); - break; - } - } - hullIdx.add(found); - } - poly = hull; - polyIdx = hullIdx; - } - } - - // 构造 MLS 控制点集合:p 必须来自 allP(original),q 来自 allQ(current) - java.util.List mlsPList = new java.util.ArrayList<>(); - java.util.List mlsQList = new java.util.ArrayList<>(); - if (applyGlobal) { - for (int i = 0; i < eligibleCount; i++) { - mlsPList.add(new Vector2f(allP[i].x, allP[i].y)); - mlsQList.add(new Vector2f(allQ[i].x, allQ[i].y)); - } - } else { - for (Integer id : polyIdx) { - if (id == null || id < 0) continue; - mlsPList.add(new Vector2f(allP[id].x, allP[id].y)); - mlsQList.add(new Vector2f(allQ[id].x, allQ[id].y)); - } - // 防护:若意外为空则退回全局 - if (mlsPList.isEmpty()) { - for (int i = 0; i < eligibleCount; i++) { - mlsPList.add(new Vector2f(allP[i].x, allP[i].y)); - mlsQList.add(new Vector2f(allQ[i].x, allQ[i].y)); - } - applyGlobal = true; - } - } - Vector2f[] p = mlsPList.toArray(new Vector2f[0]); - Vector2f[] q = mlsQList.toArray(new Vector2f[0]); - - // 固定羽化宽度(可调) - final float feather = 20f; - - // 如果没有实际位移且是显式选择模式,则直接返回(避免把 rest 覆盖成 q) - boolean anyDelta = false; - for (int i = 0; i < Math.min(p.length, q.length); i++) { - float dx = q[i].x - p[i].x, dy = q[i].y - p[i].y; - if (Math.abs(dx) > 1e-5f || Math.abs(dy) > 1e-5f) { anyDelta = true; break; } - } - if (!anyDelta && !applyGlobal) return; - - // MLS 参数 - final float alpha = 1.0f; - final float eps = 1e-6f; - - // 主循环:遍历原始顶点并应用 MLS(或按羽化混合) - if (originalVertices == null || vertices == null) return; - int vertCount = originalVertices.length / 2; - if (vertCount <= 0) return; - - for (int vi = 0; vi < originalVertices.length; vi += 2) { - float ox = originalVertices[vi]; - float oy = originalVertices[vi + 1]; - - float blend = 1f; - if (!applyGlobal) { - boolean inside = pointInPolygonRayCast(ox, oy, poly); - blend = inside ? 1f : 0f; - if (!inside && feather > 1e-6f) { - float distToEdge = (float) pointToPolygonDistance(ox, oy, poly); - if (distToEdge <= feather) { - blend = 1f - (distToEdge / feather); - blend = Math.max(0f, Math.min(1f, blend)); - } - } - } - - if (blend <= 0f) { - vertices[vi] = ox; - vertices[vi + 1] = oy; - continue; - } - - int n = p.length; - if (n == 0) { - vertices[vi] = ox; - vertices[vi + 1] = oy; - continue; - } - - // 计算权重与加权质心 - float wSum = 0f; - float px = 0f, py = 0f, qx = 0f, qy = 0f; - float[] weights = new float[n]; - boolean directMapped = false; - for (int k = 0; k < n; k++) { - float dx = ox - p[k].x; - float dy = oy - p[k].y; - float distSq = dx * dx + dy * dy; - if (distSq < 1e-8f) { - vertices[vi] = q[k].x; - vertices[vi + 1] = q[k].y; - directMapped = true; - break; - } - float w = 1.0f / (float) Math.pow(distSq + eps, alpha); - weights[k] = w; - wSum += w; - px += w * p[k].x; - py += w * p[k].y; - qx += w * q[k].x; - qy += w * q[k].y; - } - - if (directMapped) { - if (blend < 1f) { - float dx = vertices[vi] - ox; - float dy = vertices[vi + 1] - oy; - vertices[vi] = ox + dx * blend; - vertices[vi + 1] = oy + dy * blend; - } - continue; - } - - if (wSum <= 0f) { - vertices[vi] = ox; - vertices[vi + 1] = oy; - continue; - } - - px /= wSum; py /= wSum; qx /= wSum; qy /= wSum; - - // mu - float mu = 0f; - for (int k = 0; k < n; k++) { - float wk = weights[k]; - if (wk == 0f) continue; - float rx = p[k].x - px, ry = p[k].y - py; - mu += wk * (rx * rx + ry * ry); - } - if (mu < eps) { - // 退化 -> 最近点回退 - int nearest = 0; float nd = Float.POSITIVE_INFINITY; - for (int k = 0; k < n; k++) { - float dx = ox - p[k].x; float dy = oy - p[k].y; - float d = dx * dx + dy * dy; - if (d < nd) { nd = d; nearest = k; } - } - float tx = q[nearest].x - p[nearest].x; - float ty = q[nearest].y - p[nearest].y; - float nx = ox + tx, ny = oy + ty; - vertices[vi] = ox * (1f - blend) + nx * blend; - vertices[vi + 1] = oy * (1f - blend) + ny * blend; - continue; - } - - // 计算 a,b 并应用相似变换 - float a = 0f, b = 0f; - for (int k = 0; k < n; k++) { - float wk = weights[k]; - if (wk == 0f) continue; - float pxk = p[k].x - px, pyk = p[k].y - py; - float qxk = q[k].x - qx, qyk = q[k].y - qy; - a += wk * (pxk * qxk + pyk * qyk); - b += wk * (pxk * qyk - pyk * qxk); - } - - float vxRel = ox - px, vyRel = oy - py; - float tx = (a * vxRel - b * vyRel) / mu; - float ty = (b * vxRel + a * vyRel) / mu; - float dx = qx + tx, dy = qy + ty; - - // 混合结果(羽化) - vertices[vi] = ox * (1f - blend) + dx * blend; - vertices[vi + 1] = oy * (1f - blend) + dy * blend; - } // end for vertices - - logger.debug("MLS 应用完成 (explicitSelection={}, applyGlobal={}), eligibleControl={}, mlsControl={}", - explicitSelection, applyGlobal, eligibleCount, p.length); - } catch (Exception e) { - logger.error("区域化 MLS 变形失败,回退 IDW", e); - updateVerticesUsingInverseDistanceWeighting(); - } - } - - - /* --------------------- 辅助函数:多边形/几何 --------------------- */ - - /** 射线法点在多边形内判定,poly 为顺序顶点(闭合不需要重复首点) */ - private boolean pointInPolygonRayCast(float x, float y, java.util.List poly) { - boolean inside = false; - int n = poly.size(); - for (int i = 0, j = n - 1; i < n; j = i++) { - Vector2f vi = poly.get(i); - Vector2f vj = poly.get(j); - boolean intersect = ((vi.y > y) != (vj.y > y)) && - (x < (vj.x - vi.x) * (y - vi.y) / (vj.y - vi.y + 1e-12f) + vi.x); - if (intersect) inside = !inside; - } - return inside; - } - - /** 计算点到多边形边界的最小距离(若 poly 为空返回 +inf) */ - private double pointToPolygonDistance(float x, float y, java.util.List poly) { - if (poly == null || poly.isEmpty()) return Double.POSITIVE_INFINITY; - double best = Double.POSITIVE_INFINITY; - int n = poly.size(); - for (int i = 0; i < n; i++) { - Vector2f a = poly.get(i); - Vector2f b = poly.get((i + 1) % n); - double dist = pointToSegmentDistance(x, y, a.x, a.y, b.x, b.y); - if (dist < best) best = dist; - } - return best; - } - - private double pointToSegmentDistance(double px, double py, double x1, double y1, double x2, double y2) { - double vx = x2 - x1, vy = y2 - y1; - double wx = px - x1, wy = py - y1; - double c1 = vx * wx + vy * wy; - if (c1 <= 0) return Math.hypot(px - x1, py - y1); - double c2 = vx * vx + vy * vy; - if (c2 <= c1) return Math.hypot(px - x2, py - y2); - double t = c1 / c2; - double projx = x1 + t * vx; - double projy = y1 + t * vy; - return Math.hypot(px - projx, py - projy); - } - - /** 判断多边形是否自交(朴素 O(n^2) 检查边相交) */ - private boolean polygonIsSelfIntersecting(java.util.List poly) { - int n = poly.size(); - if (n < 4) return false; - for (int i = 0; i < n; i++) { - Vector2f a1 = poly.get(i); - Vector2f a2 = poly.get((i + 1) % n); - for (int j = i + 1; j < n; j++) { - // skip adjacent edges - if (Math.abs(i - j) <= 1 || (i == 0 && j == n - 1) || (j == 0 && i == n - 1)) continue; - Vector2f b1 = poly.get(j); - Vector2f b2 = poly.get((j + 1) % n); - if (segmentsIntersect(a1.x, a1.y, a2.x, a2.y, b1.x, b1.y, b2.x, b2.y)) return true; - } - } - return false; - } - - /** 线段相交测试(闭包含端点) */ - private boolean segmentsIntersect(double x1, double y1, double x2, double y2, - double x3, double y3, double x4, double y4) { - // 快速边界排除 - if (Math.max(x1,x2) < Math.min(x3,x4) || Math.max(x3,x4) < Math.min(x1,x2) || - Math.max(y1,y2) < Math.min(y3,y4) || Math.max(y3,y4) < Math.min(y1,y2)) - return false; - double d1 = orient(x3,y3,x4,y4,x1,y1); - double d2 = orient(x3,y3,x4,y4,x2,y2); - double d3 = orient(x1,y1,x2,y2,x3,y3); - double d4 = orient(x1,y1,x2,y2,x4,y4); - if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) - return true; - if (Math.abs(d1) < 1e-10 && onSegment(x3,y3,x4,y4,x1,y1)) return true; - if (Math.abs(d2) < 1e-10 && onSegment(x3,y3,x4,y4,x2,y2)) return true; - if (Math.abs(d3) < 1e-10 && onSegment(x1,y1,x2,y2,x3,y3)) return true; - if (Math.abs(d4) < 1e-10 && onSegment(x1,y1,x2,y2,x4,y4)) return true; - return false; - } - private double orient(double ax,double ay,double bx,double by,double cx,double cy) { - return (bx-ax)*(cy-ay) - (by-ay)*(cx-ax); - } - private boolean onSegment(double ax,double ay,double bx,double by,double px,double py) { - return Math.min(ax,bx)-1e-8 <= px && px <= Math.max(ax,bx)+1e-8 && - Math.min(ay,by)-1e-8 <= py && py <= Math.max(ay,by)+1e-8; - } - - /** 凸包(Monotone chain),返回逆时针 hull 顶点 */ - private java.util.List convexHull(java.util.List points) { - java.util.List pts = new java.util.ArrayList<>(points); - pts.sort((a,b) -> { - if (a.x == b.x) return Float.compare(a.y,b.y); - return Float.compare(a.x,b.x); - }); - java.util.List lower = new java.util.ArrayList<>(); - for (Vector2f p : pts) { - while (lower.size() >= 2) { - Vector2f p1 = lower.get(lower.size()-2), p2 = lower.get(lower.size()-1); - if (orient(p1.x,p1.y,p2.x,p2.y,p.x,p.y) <= 0) lower.remove(lower.size()-1); - else break; - } - lower.add(p); - } - java.util.List upper = new java.util.ArrayList<>(); - for (int i = pts.size()-1; i >= 0; i--) { - Vector2f p = pts.get(i); - while (upper.size() >= 2) { - Vector2f p1 = upper.get(upper.size()-2), p2 = upper.get(upper.size()-1); - if (orient(p1.x,p1.y,p2.x,p2.y,p.x,p.y) <= 0) upper.remove(upper.size()-1); - else break; - } - upper.add(p); - } - lower.remove(lower.size()-1); - upper.remove(upper.size()-1); - lower.addAll(upper); - return lower; - } - - /** - * 基于反距离加权(IDW)但对“位移 delta”加权计算结果。 - * 输入为控制点原始位置 secOrig 和每点的 delta(current - original)。 - * 返回最终的绝对坐标(orig + weighted_delta) - */ - private Vector2f computeIDWForPointUsingDeltas(float ox, float oy, Vector2f[] secOrig, Vector2f[] deltas) { - // Live2D 弯曲变形的影响力衰减可能更快,使用 power = 3.0f 或 4.0f 可能会更自然地模拟局部性。 - // 这里采用 power = 3.0f 作为改进尝试。 - final float power = 3.0f; // 增加衰减速度 - final float eps = 1e-5f; - - float sumWX = 0f; - float sumWY = 0f; - float weightSum = 0f; - - for (int k = 0; k < secOrig.length; k++) { - Vector2f sO = secOrig[k]; - - float dx = ox - sO.x; - float dy = oy - sO.y; - float distSq = dx * dx + dy * dy; - - // 如果非常接近某个控制点,直接使用该控制点的 delta,避免数值不稳定 - if (distSq < eps * eps) { - return new Vector2f(ox + deltas[k].x, oy + deltas[k].y); - } - - // --- 核心改进:权重计算 --- - float dist = (float) Math.sqrt(distSq); - // Live2D 可能使用径向基函数(RBF)或平滑的权重, - // 但这里保持 IDW 结构,仅增加 power 使影响更局部。 - float w = 1.0f / (float) Math.pow(dist, power); - - sumWX += w * deltas[k].x; - sumWY += w * deltas[k].y; - weightSum += w; - } - - if (weightSum > 0f) { - float dxFinal = sumWX / weightSum; - float dyFinal = sumWY / weightSum; - return new Vector2f(ox + dxFinal, oy + dyFinal); - } else { - return new Vector2f(ox, oy); - } - } - - - /** - * 基于反距离加权(IDW)但对“位移 delta”加权计算结果,保证不会把整个网格搬到 (0,0)。 - * 返回最终的绝对坐标(orig + weighted_delta) - */ - private Vector2f computeIDWForPoint(float ox, float oy, Vector2f[] secOrig, Vector2f[] secCurr) { - final float power = 2.0f; - final float eps = 1e-5f; - - float sumWX = 0f; - float sumWY = 0f; - float weightSum = 0f; - - for (int k = 0; k < secOrig.length; k++) { - Vector2f sO = secOrig[k]; - Vector2f sC = secCurr[k]; - - float dx = ox - sO.x; - float dy = oy - sO.y; - float distSq = dx * dx + dy * dy; - - // 如果非常接近某个控制点,直接使用该控制点的位移,避免数值不稳定 - if (distSq < eps * eps) { - float deltaX = sC.x - sO.x; - float deltaY = sC.y - sO.y; - return new Vector2f(ox + deltaX, oy + deltaY); - } - - float dist = (float) Math.sqrt(distSq); - float w = 1.0f / (float) Math.pow(dist, power); - - float deltaX = sC.x - sO.x; - float deltaY = sC.y - sO.y; - - sumWX += w * deltaX; - sumWY += w * deltaY; - weightSum += w; - } - - if (weightSum > 0f) { - float dxFinal = sumWX / weightSum; - float dyFinal = sumWY / weightSum; - return new Vector2f(ox + dxFinal, oy + dyFinal); - } else { - return new Vector2f(ox, oy); - } - } - - /** - * 使用反距离加权插值(适用于任意数量的控制点) - */ - private void updateVerticesUsingInverseDistanceWeighting() { - try { - float power = 2.0f; // 距离的幂次,通常为2 - float epsilon = 0.0001f; // 避免除零的小值 - - for (int i = 0; i < originalVertices.length; i += 2) { - float origX = originalVertices[i]; - float origY = originalVertices[i + 1]; - - float weightSum = 0.0f; - float newX = 0.0f; - float newY = 0.0f; - - for (SecondaryVertex secVertex : secondaryVertices) { - Vector2f secOrigPos = secVertex.getOriginalPosition(); - Vector2f secCurrPos = secVertex.getPosition(); - - // 计算原始位置到控制点原始位置的距离 - float dx = origX - secOrigPos.x; - float dy = origY - secOrigPos.y; - float distance = (float) Math.sqrt(dx * dx + dy * dy); - - // 避免除零 - if (distance < epsilon) { - // 如果顶点正好在控制点上,直接使用控制点的新位置 - newX = secCurrPos.x; - newY = secCurrPos.y; - weightSum = 1.0f; - break; - } - - // 计算权重:1 / distance^power - float weight = 1.0f / (float) Math.pow(distance, power); - - // 计算该控制点对顶点位置的贡献 - float deltaX = secCurrPos.x - secOrigPos.x; - float deltaY = secCurrPos.y - secOrigPos.y; - - newX += weight * (origX + deltaX); - newY += weight * (origY + deltaY); - weightSum += weight; - } - - if (weightSum > 0) { - vertices[i] = newX / weightSum; - vertices[i + 1] = newY / weightSum; - } else { - // 保持原始位置 - vertices[i] = origX; - vertices[i + 1] = origY; - } - } - - logger.debug("应用反距离加权变形,使用 {} 个控制点", secondaryVertices.size()); - - } catch (Exception e) { - logger.error("反距离加权变形失败", e); - } - } - - /** - * 计算原始顶点的边界框 - */ - private BoundingBox calculateOriginalBounds() { - if (originalVertices == null || originalVertices.length < 2) { - return null; - } - - BoundingBox bounds = new BoundingBox(); - for (int i = 0; i < originalVertices.length; i += 2) { - bounds.expand(originalVertices[i], originalVertices[i + 1]); - } - return bounds; - } - - /** - * 使用双线性插值更新顶点(适用于4个角点的情况) - */ - private void updateVerticesUsingBilinearInterpolation() { - // 获取四个角点的二级顶点 - SecondaryVertex bottomLeft = secondaryVertices.get(0); // 左下 - SecondaryVertex bottomRight = secondaryVertices.get(1); // 右下 - SecondaryVertex topRight = secondaryVertices.get(2); // 右上 - SecondaryVertex topLeft = secondaryVertices.get(3); // 左上 - - // 获取原始边界框(用于UV计算) - BoundingBox originalBounds = new BoundingBox(); - for (int i = 0; i < originalVertices.length; i += 2) { - originalBounds.expand(originalVertices[i], originalVertices[i + 1]); - } - - float minX = originalBounds.getMinX(); - float minY = originalBounds.getMinY(); - float maxX = originalBounds.getMaxX(); - float maxY = originalBounds.getMaxY(); - float width = maxX - minX; - float height = maxY - minY; - - if (width <= 0 || height <= 0) return; - - // 对每个顶点进行双线性插值 - for (int i = 0; i < originalVertices.length; i += 2) { - float origX = originalVertices[i]; - float origY = originalVertices[i + 1]; - - // 计算UV坐标(在原始边界框中的相对位置) - float u = (origX - minX) / width; - float v = (origY - minY) / height; - - // 双线性插值计算新位置 - Vector2f newPos = bilinearInterpolation( - bottomLeft.getPosition(), bottomRight.getPosition(), - topRight.getPosition(), topLeft.getPosition(), - u, v - ); - - // 更新顶点位置 - vertices[i] = newPos.x; - vertices[i + 1] = newPos.y; - } - } - - /** - * 双线性插值计算 - */ - private Vector2f bilinearInterpolation(Vector2f p00, Vector2f p10, Vector2f p11, Vector2f p01, - float u, float v) { - // 水平插值 - Vector2f bottom = new Vector2f( - p00.x + u * (p10.x - p00.x), - p00.y + u * (p10.y - p00.y) - ); - - Vector2f top = new Vector2f( - p01.x + u * (p11.x - p01.x), - p01.y + u * (p11.y - p01.y) - ); - - // 垂直插值 - return new Vector2f( - bottom.x + v * (top.x - bottom.x), - bottom.y + v * (top.y - bottom.y) - ); - } - - /** - * 使用复杂插值方法(适用于任意数量的二级顶点) - */ - private void updateVerticesUsingComplexInterpolation() { - // 这里可以实现更复杂的插值算法,如重心坐标插值 - // 目前先使用简单的最近邻插值 - - for (int i = 0; i < originalVertices.length; i += 2) { - float origX = originalVertices[i]; - float origY = originalVertices[i + 1]; - - // 找到最近的二级顶点 - SecondaryVertex closest = findClosestSecondaryVertex(origX, origY); - if (closest != null) { - Vector2f closestPos = closest.getPosition(); - // 简单地将顶点移动到最近的二级顶点位置 - vertices[i] = closestPos.x; - vertices[i + 1] = closestPos.y; - } - } - } - - /** - * 找到距离指定位置最近的二级顶点 - */ - private SecondaryVertex findClosestSecondaryVertex(float x, float y) { - if (secondaryVertices.isEmpty()) return null; - - SecondaryVertex closest = secondaryVertices.get(0); - float minDistance = distance(x, y, closest.getPosition().x, closest.getPosition().y); - - for (int i = 1; i < secondaryVertices.size(); i++) { - SecondaryVertex vertex = secondaryVertices.get(i); - float dist = distance(x, y, vertex.getPosition().x, vertex.getPosition().y); - if (dist < minDistance) { - minDistance = dist; - closest = vertex; - } - } - - return closest; - } - - private float distance(float x1, float y1, float x2, float y2) { - float dx = x2 - x1; - float dy = y2 - y1; - return (float) Math.sqrt(dx * dx + dy * dy); - } - - /** - * 获取是否显示二级顶点 - */ - public boolean isShowSecondaryVertices() { - return showSecondaryVertices; - } - - /** - * 设置二级顶点颜色 - */ - public void setSecondaryVertexColor(Vector4f color) { - this.secondaryVertexColor.set(color); - markDirty(); - } - - /** - * 设置选中的二级顶点颜色 - */ - public void setSelectedSecondaryVertexColor(Vector4f color) { - this.selectedSecondaryVertexColor.set(color); - markDirty(); - } - - /** - * 设置二级顶点大小 - */ - public void setSecondaryVertexSize(float size) { - this.secondaryVertexSize = size; - markDirty(); - } - - // ==================== 二级顶点变形支持 ==================== - - /** - * 对二级顶点应用变形 - */ - public void transformSecondaryVertices(VertexTransformer transformer) { - for (SecondaryVertex vertex : secondaryVertices) { - Vector2f position = vertex.getPosition(); - transformer.transform(position, -1); // 使用负索引标识二级顶点 - vertex.setPosition(position); - } - markDirty(); - } - - /** - * 重置所有二级顶点到原始位置 - */ - public void resetSecondaryVerticesToOriginal() { - for (SecondaryVertex vertex : secondaryVertices) { - vertex.resetToOriginal(); - } - markDirty(); - } - - /** - * 保存当前二级顶点位置为原始位置 - */ - public void saveSecondaryVerticesAsOriginal() { - for (SecondaryVertex vertex : secondaryVertices) { - vertex.saveAsOriginal(); - } - } - - // ==================== 顶点操作 ==================== - - /** - * 添加新顶点到网格末尾 - */ - public void addVertex(float x, float y, float u, float v) { - // 扩展顶点数组 - float[] newVertices = new float[vertices.length + 2]; - System.arraycopy(vertices, 0, newVertices, 0, vertices.length); - newVertices[vertices.length] = x; - newVertices[vertices.length + 1] = y; - - // 扩展UV数组 - float[] newUVs = new float[uvs.length + 2]; - System.arraycopy(uvs, 0, newUVs, 0, uvs.length); - newUVs[uvs.length] = u; - newUVs[uvs.length + 1] = v; - - // 更新数据 - this.vertices = newVertices; - this.uvs = newUVs; - - // 注意:需要手动更新索引数组来包含新顶点 - markDirty(); - } - - /** - * 在指定位置插入顶点 - */ - public void insertVertex(int index, float x, float y, float u, float v) { - if (index < 0 || index > getVertexCount()) { - throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); - } - - // 扩展顶点数组 - float[] newVertices = new float[vertices.length + 2]; - System.arraycopy(vertices, 0, newVertices, 0, index * 2); - newVertices[index * 2] = x; - newVertices[index * 2 + 1] = y; - System.arraycopy(vertices, index * 2, newVertices, index * 2 + 2, vertices.length - index * 2); - - // 扩展UV数组 - float[] newUVs = new float[uvs.length + 2]; - System.arraycopy(uvs, 0, newUVs, 0, index * 2); - newUVs[index * 2] = u; - newUVs[index * 2 + 1] = v; - System.arraycopy(uvs, index * 2, newUVs, index * 2 + 2, uvs.length - index * 2); - - // 更新数据 - this.vertices = newVertices; - this.uvs = newUVs; - - // 注意:需要更新索引数组来反映顶点位置的变化 - updateIndicesAfterVertexInsertion(index); - - markDirty(); - } - - /** - * 移除指定顶点 - */ - public void removeVertex(int index) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); - } - - // 缩减顶点数组 - float[] newVertices = new float[vertices.length - 2]; - System.arraycopy(vertices, 0, newVertices, 0, index * 2); - System.arraycopy(vertices, index * 2 + 2, newVertices, index * 2, vertices.length - index * 2 - 2); - - // 缩减UV数组 - float[] newUVs = new float[uvs.length - 2]; - System.arraycopy(uvs, 0, newUVs, 0, index * 2); - System.arraycopy(uvs, index * 2 + 2, newUVs, index * 2, uvs.length - index * 2 - 2); - - // 更新数据 - this.vertices = newVertices; - this.uvs = newUVs; - - // 更新索引数组 - updateIndicesAfterVertexRemoval(index); - - markDirty(); - } - - /** - * 在插入顶点后更新索引数组 - */ - private void updateIndicesAfterVertexInsertion(int insertedIndex) { - if (indices == null) return; - - for (int i = 0; i < indices.length; i++) { - if (indices[i] >= insertedIndex) { - indices[i]++; - } - } - } - - /** - * 在移除顶点后更新索引数组 - */ - private void updateIndicesAfterVertexRemoval(int removedIndex) { - if (indices == null) return; - - // 创建新索引数组,移除引用被删除顶点的三角形 - List newIndicesList = new ArrayList<>(); - for (int i = 0; i < indices.length; i += 3) { - int i1 = indices[i]; - int i2 = indices[i + 1]; - int i3 = indices[i + 2]; - - // 如果三角形包含被删除的顶点,跳过这个三角形 - if (i1 == removedIndex || i2 == removedIndex || i3 == removedIndex) { - continue; - } - - // 调整索引编号 - if (i1 > removedIndex) i1--; - if (i2 > removedIndex) i2--; - if (i3 > removedIndex) i3--; - - newIndicesList.add(i1); - newIndicesList.add(i2); - newIndicesList.add(i3); - } - - // 转换回数组 - this.indices = new int[newIndicesList.size()]; - for (int i = 0; i < newIndicesList.size(); i++) { - this.indices[i] = newIndicesList.get(i); - } - } - - /** - * 获取顶点数量 - */ - public int getVertexCount() { - return (vertices != null) ? (vertices.length / 2) : 0; - } - - /** - * 获取顶点位置 - */ - public Vector2f getVertex(int index, Vector2f dest) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); - } - return dest.set(getX(index), getY(index)); - } - - public Vector2f getVertex(int index) { - return getVertex(index, new Vector2f()); - } - - /** - * 设置顶点位置 - */ - public void setVertex(int index, float x, float y) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); - } - int baseIndex = index * 2; - // 只写局部顶点(供变形使用) - vertices[baseIndex] = x; - vertices[baseIndex + 1] = y; - // 不直接把局部坐标写到 renderVertices(render 由 ModelPart 更新世界变换时写入) - markDirty(); - } - - - public void setVertex(int index, Vector2f position) { - setVertex(index, position.x, position.y); - } - - /** - * 设置该 Mesh 的选中状态(线程安全) - */ - public void setSelected(boolean sel) { - this.selected = sel; - } - - /** - * 查询选中状态 - */ - public boolean isSelected() { - return this.selected; - } - - /** - * 获取UV坐标 - */ - public Vector2f getUV(int index, Vector2f dest) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("UV index out of bounds: " + index); - } - int baseIndex = index * 2; - return dest.set(uvs[baseIndex], uvs[baseIndex + 1]); - } - - /** - * 设置UV坐标 - */ - public void setUV(int index, float u, float v) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("UV index out of bounds: " + index); - } - int baseIndex = index * 2; - uvs[baseIndex] = u; - uvs[baseIndex + 1] = v; - markDirty(); - } - - // ==================== 变形支持 ==================== - - /** - * 重置为原始顶点数据 - */ - public void resetToOriginal() { - if (originalVertices != null && originalVertices.length == vertices.length) { - System.arraycopy(originalVertices, 0, vertices, 0, vertices.length); - markDirty(); - } - } - - /** - * 保存当前顶点为原始数据 - */ - public void saveAsOriginal() { - originalVertices = vertices.clone(); - } - - /** - * 应用变形到所有顶点 - */ - public void transformVertices(VertexTransformer transformer) { - for (int i = 0; i < getVertexCount(); i++) { - Vector2f vertex = getVertex(i); - transformer.transform(vertex, i); - setVertex(i, vertex); - } - markDirty(); - } - - /** - * 获取木偶控制点数量 - */ - public int getPuppetPinCount() { - return puppetPins.size(); - } - - public boolean isSuspension() { - return isSuspension; - } - - /** - * 顶点变换器接口 - */ - public interface VertexTransformer { - void transform(Vector2f vertex, int index); - } - - // ==================== 边界计算 ==================== - - /** - * 更新边界 - */ - public void updateBounds() { - // 优先使用渲染顶点(世界坐标) - if (this.renderVertices != null && this.bakedToWorld && this.renderVertices.length >= 2) { - float minX = Float.POSITIVE_INFINITY, minY = Float.POSITIVE_INFINITY; - float maxX = Float.NEGATIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY; - boolean hasValidPoints = false; - - for (int i = 0; i < renderVertices.length; i += 2) { - float x = renderVertices[i]; - float y = renderVertices[i + 1]; - if (Float.isNaN(x) || Float.isNaN(y) || Float.isInfinite(x) || Float.isInfinite(y)) continue; - - hasValidPoints = true; - if (x < minX) minX = x; - if (y < minY) minY = y; - if (x > maxX) maxX = x; - if (y > maxY) maxY = y; - } - - if (hasValidPoints) { - if (this.bounds == null) this.bounds = new BoundingBox(); - this.bounds.set(minX, minY, maxX, maxY); - this.boundsDirty = false; - return; - } - } - - // 回退到局部顶点计算 - 关键修复:只使用原始网格顶点,不包含二级顶点 - float[] src = null; - String srcType = "null"; - - if (this.vertices != null && this.vertices.length > 0) { - src = this.vertices; - srcType = "vertices"; - } else if (this.originalVertices != null) { - src = this.originalVertices; - srcType = "originalVertices"; - } else { - srcType = "null"; - } - - logger.debug("Mesh2D.updateBounds [{}] - 使用的数据源: {}, 长度: {}", - this.name, srcType, (src != null ? src.length : 0)); - - if (src == null || src.length < 2) { - if (this.bounds == null) this.bounds = new BoundingBox(); - this.bounds.set(0f, 0f, 0f, 0f); - this.boundsDirty = false; - return; - } - - float minX = Float.POSITIVE_INFINITY, minY = Float.POSITIVE_INFINITY; - float maxX = Float.NEGATIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY; - boolean hasValidPoints = false; - - // 关键:只计算原始网格顶点的边界,不包含二级顶点 - for (int i = 0; i < src.length; i += 2) { - float x = src[i]; - float y = src[i + 1]; - if (Float.isNaN(x) || Float.isNaN(y) || Float.isInfinite(x) || Float.isInfinite(y)) continue; - - hasValidPoints = true; - if (x < minX) minX = x; - if (y < minY) minY = y; - if (x > maxX) maxX = x; - if (y > maxY) maxY = y; - } - - if (!hasValidPoints || minX == Float.POSITIVE_INFINITY) { - minX = minY = maxX = maxY = 0f; - } - - if (this.bounds == null) this.bounds = new BoundingBox(); - this.bounds.set(minX, minY, maxX, maxY); - this.boundsDirty = false; - - logger.debug("边界更新完成 - 使用{}坐标: [{}, {}, {}, {}] (不包含二级顶点)", - (this.renderVertices != null && this.bakedToWorld) ? "世界" : "局部", - minX, minY, maxX, maxY); - } - - /** - * 获取边界框 - */ - public BoundingBox getBounds() { - if (this.boundsDirty) { - updateBounds(); - } - return this.bounds; - } - - - public BoundingBox getWorldBounds() { - BoundingBox wb = new BoundingBox(); - - // 1) 优先使用 renderVertices(已是世界坐标) - if (this.renderVertices != null && this.bakedToWorld && this.renderVertices.length >= 2) { - float minX = Float.POSITIVE_INFINITY, minY = Float.POSITIVE_INFINITY; - float maxX = Float.NEGATIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY; - for (int i = 0; i + 1 < renderVertices.length; i += 2) { - float x = renderVertices[i]; - float y = renderVertices[i + 1]; - if (Float.isNaN(x) || Float.isInfinite(x) || Float.isNaN(y) || Float.isInfinite(y)) continue; - if (x < minX) minX = x; - if (y < minY) minY = y; - if (x > maxX) maxX = x; - if (y > maxY) maxY = y; - } - if (minX == Float.POSITIVE_INFINITY) { // no valid points - wb.set(0f, 0f, 0f, 0f); - } else { - wb.set(minX, minY, maxX, maxY); - } - return wb; - } - - // 2) 否则使用局部 vertices(不包含二级顶点) - float[] src = (this.vertices != null && this.vertices.length > 0) ? this.vertices - : (this.originalVertices != null ? this.originalVertices : null); - - if (src == null || src.length < 2) { - wb.set(0f, 0f, 0f, 0f); - return wb; - } - - // 如果没有 modelPart,直接把局部 bounds 返回(视为 world) - if (this.modelPart == null) { - // 直接使用局部 bounds - float minX = Float.POSITIVE_INFINITY, minY = Float.POSITIVE_INFINITY; - float maxX = Float.NEGATIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY; - for (int i = 0; i + 1 < src.length; i += 2) { - float x = src[i], y = src[i + 1]; - if (Float.isNaN(x) || Float.isNaN(y) || Float.isInfinite(x) || Float.isInfinite(y)) continue; - if (x < minX) minX = x; - if (y < minY) minY = y; - if (x > maxX) maxX = x; - if (y > maxY) maxY = y; - } - if (minX == Float.POSITIVE_INFINITY) wb.set(0f, 0f, 0f, 0f); - else wb.set(minX, minY, maxX, maxY); - return wb; - } - - // 确保 modelPart 的 worldTransform 是最新的 - Matrix3f wt = new Matrix3f(modelPart.getWorldTransform()); - - float minX = Float.POSITIVE_INFINITY, minY = Float.POSITIVE_INFINITY; - float maxX = Float.NEGATIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY; - - Vector2f tmp = new Vector2f(); - for (int i = 0; i + 1 < src.length; i += 2) { - tmp.set(src[i], src[i + 1]); - Vector2f worldPt = Matrix3fUtils.transformPoint(wt, tmp); - float x = worldPt.x, y = worldPt.y; - if (Float.isNaN(x) || Float.isNaN(y) || Float.isInfinite(x) || Float.isInfinite(y)) continue; - if (x < minX) minX = x; - if (y < minY) minY = y; - if (x > maxX) maxX = x; - if (y > maxY) maxY = y; - } - - if (minX == Float.POSITIVE_INFINITY) wb.set(0f, 0f, 0f, 0f); - else wb.set(minX, minY, maxX, maxY); - - return wb; - } - - /** - * 检查点是否在网格内(使用边界框近似) - */ - public boolean containsPoint(float x, float y) { - return containsPoint(x, y, false); - } - - /** - * 检查点是否在网格内(可选择精确检测) - */ - public boolean containsPoint(float x, float y, boolean precise) { - //if (isInMultiSelection()) { - // BoundingBox multiBounds = getMultiSelectionBounds(); - // boolean inBounds = x >= multiBounds.getMinX() && x <= multiBounds.getMaxX() && - // y >= multiBounds.getMinY() && y <= multiBounds.getMaxY(); -// - // if (precise && inBounds) { - // // 在多选边界框内时,进一步检查是否在任意选中的网格几何形状内 - // return isPointInAnySelectedMesh(x, y); - // } - // return inBounds; - //} - - BoundingBox b = getBounds(); - boolean inBounds = x >= b.getMinX() && x <= b.getMaxX() && y >= b.getMinY() && y <= b.getMaxY(); - - if (precise && inBounds) { - // 精确检测点是否在网格几何形状内 - return isPointInMeshGeometry(x, y); - } - return inBounds; - } - - /** - * 检查点是否在任意选中的网格几何形状内 - */ - private boolean isPointInAnySelectedMesh(float x, float y) { - // 检查自己 - if (isPointInMeshGeometry(x, y)) { - return true; - } - - // 检查多选列表中的其他网格 - for (Mesh2D mesh : multiSelectedParts) { - if (mesh.isPointInMeshGeometry(x, y)) { - return true; - } - } - return false; - } - - /** - * 精确检测点是否在网格几何形状内(使用射线法) - */ - private boolean isPointInMeshGeometry(float x, float y) { - // 简单的边界框检测先过滤掉明显不在的 - BoundingBox b = getBounds(); - if (x < b.getMinX() || x > b.getMaxX() || y < b.getMinY() || y > b.getMaxY()) { - return false; - } - - // 使用射线法进行精确检测 - return isPointInPolygon(x, y, vertices); - } - - /** - * 使用射线法判断点是否在多边形内 - */ - private boolean isPointInPolygon(float x, float y, float[] vertices) { - if (vertices.length < 6) { // 至少需要3个点组成三角形 - return false; - } - - boolean inside = false; - int n = vertices.length / 2; - - for (int i = 0, j = n - 1; i < n; j = i++) { - float xi = vertices[i * 2]; - float yi = vertices[i * 2 + 1]; - float xj = vertices[j * 2]; - float yj = vertices[j * 2 + 1]; - - // 检查点是否在多边形的边上 - if (isPointOnLineSegment(x, y, xi, yi, xj, yj)) { - return true; - } - - // 射线法核心逻辑 - if (((yi > y) != (yj > y)) && - (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) { - inside = !inside; - } - } - - return inside; - } - - /** - * 检查点是否在线段上 - */ - private boolean isPointOnLineSegment(float x, float y, float x1, float y1, float x2, float y2) { - float cross = (x - x1) * (y2 - y1) - (y - y1) * (x2 - x1); - if (Math.abs(cross) > 1e-6) { - return false; // 不在直线上 - } - - float dot = (x - x1) * (x2 - x1) + (y - y1) * (y2 - y1); - if (dot < 0) { - return false; // 在线段起点之前 - } - - float squaredLength = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1); - return !(dot > squaredLength); // 在线段终点之后 - } - - public boolean containsPoint(Vector2f point) { - return containsPoint(point.x, point.y); - } - - // ==================== 缓冲区支持 ==================== - - /** - * 获取顶点缓冲区数据 - */ - public FloatBuffer getVertexBuffer(FloatBuffer buffer) { - if (buffer == null || buffer.remaining() < vertices.length) { - throw new IllegalArgumentException("Buffer is null or too small"); - } - buffer.clear(); - buffer.put(vertices); - buffer.flip(); - return buffer; - } - - /** - * 获取UV缓冲区数据 - */ - public FloatBuffer getUVBuffer(FloatBuffer buffer) { - if (buffer == null || buffer.capacity() < uvs.length) { - throw new IllegalArgumentException("Buffer is null or too small"); - } - buffer.clear(); - buffer.put(uvs); - buffer.flip(); - return buffer; - } - - public float getX(int index) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); - } - int base = index * 2; - // 使用 renderVertices(已由 syncRenderVerticesFromLocal 填充并标记 bakedToWorld) - if (renderVertices != null && bakedToWorld && renderVertices.length > base + 1) { - return renderVertices[base]; - } - // 回退到局部顶点(变形/编辑写入这里) - return vertices[base]; - } - - - public float getY(int index) { - if (index < 0 || index >= getVertexCount()) { - throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); - } - int base = index * 2; - if (renderVertices != null && bakedToWorld && renderVertices.length > base + 1) { - return renderVertices[base + 1]; - } - return vertices[base + 1]; - } - - - /** - * 获取索引缓冲区数据 - */ - public IntBuffer getIndexBuffer(IntBuffer buffer) { - if (buffer == null || buffer.capacity() < indices.length) { - throw new IllegalArgumentException("Buffer is null or too small"); - } - buffer.clear(); - buffer.put(indices); - buffer.flip(); - return buffer; - } - - /** - * 获取交错的顶点+UV数据(用于VBO) - */ - public FloatBuffer getInterleavedBuffer(FloatBuffer buffer) { - int vertexCount = getVertexCount(); - int floatCount = vertexCount * 4; // 每个顶点:x, y, u, v - - if (buffer == null || buffer.remaining() < floatCount) { - throw new IllegalArgumentException("Buffer is null or too small"); - } - - buffer.clear(); - for (int i = 0; i < vertexCount; i++) { - buffer.put(getX(i)); // x - buffer.put(getY(i)); // y - buffer.put(uvs[i * 2]); // u - buffer.put(uvs[i * 2 + 1]); // v - } - buffer.flip(); - return buffer; - } - - // ==================== 状态管理 ==================== - - /** - * 标记数据已修改 - */ - public void markDirty() { - deleteGPU(); - this.dirty = true; - this.boundsDirty = true; - this.multiSelectionDirty = true; - } - - - /** - * 检查是否有移动过的木偶控制点 - */ - private boolean hasMovedPuppetPins() { - for (PuppetPin pin : puppetPins) { - Vector2f currentPos = pin.getPosition(); - Vector2f originalPos = pin.getOriginalPosition(); - if (currentPos.distanceSquared(originalPos) > 0.001f) { - return true; - } - } - return false; - } - - /** - * 设置当前的“悬停状态” - */ - public void setSuspension(boolean suspension) { - isSuspension = suspension; - } - - /** - * 清除脏标记 - */ - public void markClean() { - this.dirty = false; - } - - /** - * 检查数据是否已修改 - */ - public boolean isDirty() { - return dirty; - } - - /** - * 获取当前活跃的变形类型 - */ - public String getActiveDeformationType() { - boolean hasPuppet = hasActivePuppetDeformation(); - boolean hasSecondary = hasActiveSecondaryVertexDeformation(); - - if (hasPuppet && hasSecondary) { - return "CONFLICT"; - } else if (hasPuppet) { - return "PUPPET"; - } else if (hasSecondary) { - return "SECONDARY"; - } else { - return "NONE"; - } - } - - /** - * 将网格数据上传到 GPU(生成 VAO/VBO/EBO) - */ - public void uploadToGPU() { - if (uploaded) return; - - RenderSystem.assertOnRenderThread(); - - // 组织 interleaved buffer (x,y,u,v) - int vertexCount = getVertexCount(); - int floatCount = vertexCount * 4; // x,y,u,v - FloatBuffer interleaved = MemoryUtil.memAllocFloat(floatCount); - try { - getInterleavedBuffer(interleaved); - IntBuffer ib = MemoryUtil.memAllocInt(indices.length); - try { - getIndexBuffer(ib); - - // 使用 RenderSystem 生成 VAO - vaoId = RenderSystem.glGenVertexArrays(); - RenderSystem.glBindVertexArray(() -> vaoId); - - // 使用 RenderSystem 生成和绑定 VBO - vboId = RenderSystem.glGenBuffers(); - RenderSystem.glBindBuffer(RenderSystem.GL_ARRAY_BUFFER, () -> vboId); - RenderSystem.glBufferData(RenderSystem.GL_ARRAY_BUFFER, interleaved, RenderSystem.GL_STATIC_DRAW); - - // 使用 RenderSystem 生成和绑定 EBO - eboId = RenderSystem.glGenBuffers(); - RenderSystem.glBindBuffer(RenderSystem.GL_ELEMENT_ARRAY_BUFFER, () -> eboId); - RenderSystem.glBufferData(RenderSystem.GL_ELEMENT_ARRAY_BUFFER, ib, RenderSystem.GL_STATIC_DRAW); - - int stride = 4 * Float.BYTES; // x,y,u,v - - // position attrib (location 0) -> vec2 - RenderSystem.enableVertexAttribArray(0); - RenderSystem.vertexAttribPointer(0, 2, RenderSystem.GL_FLOAT, false, stride, 0); - - // uv attrib (location 1) -> vec2 - RenderSystem.enableVertexAttribArray(1); - RenderSystem.vertexAttribPointer(1, 2, RenderSystem.GL_FLOAT, false, stride, 2 * Float.BYTES); - - // unbind VAO (keep EBO bound to VAO on unbind) - RenderSystem.glBindVertexArray(() -> 0); - - indexCount = indices.length; - uploaded = true; - markClean(); - } finally { - MemoryUtil.memFree(ib); - } - } finally { - MemoryUtil.memFree(interleaved); - // unbind array buffer - RenderSystem.glBindBuffer(RenderSystem.GL_ARRAY_BUFFER, () -> 0); - } - } - - /** - * 解决变形冲突 - 优先使用木偶变形 - */ - public void resolveDeformationConflict(boolean preferPuppet) { - if (preferPuppet) { - // 保存当前二级顶点位置,然后重置它们 - for (SecondaryVertex vertex : secondaryVertices) { - vertex.saveAsOriginal(); - } - } else { - // 保存当前木偶控制点位置,然后重置它们 - for (PuppetPin pin : puppetPins) { - pin.saveAsOriginal(); - } - updateVerticesFromPuppetPins(); - } - markDirty(); - } - - /** - * 重置所有变形到原始状态 - */ - public void resetAllDeformations() { - // 重置木偶控制点 - for (PuppetPin pin : puppetPins) { - pin.resetToOriginal(); - } - - // 重置二级顶点 - for (SecondaryVertex vertex : secondaryVertices) { - vertex.resetToOriginal(); - } - - // 重置顶点到原始状态 - if (originalVertices != null && originalVertices.length == vertices.length) { - System.arraycopy(originalVertices, 0, vertices, 0, vertices.length); - } - - markDirty(); - } - - /** - * 绘制网格(会在第一次绘制时自动上传到 GPU) - */ - public void draw(int shaderProgram, Matrix3f modelMatrix) { - String deformationType = getActiveDeformationType(); - - if ("PUPPET".equals(deformationType) || "CONFLICT".equals(deformationType)) { - updateVerticesFromPuppetPins(); - } else if ("SECONDARY".equals(deformationType)) { - updateVerticesFromSecondaryVertices(); - } - - if (!visible) return; - if (indices == null || indices.length == 0) return; - - // 如果数据脏,强制删除旧 GPU 对象并重新上传 - if (dirty) { - deleteGPU(); - uploadToGPU(); - } - - if (!uploaded) { - uploadToGPU(); - } - - // 保存当前 program,必要时恢复 - int prevProgram = RenderSystem.getCurrentProgram(); - boolean switchedProgram = false; - if (shaderProgram != 0 && prevProgram != shaderProgram) { - RenderSystem.useProgram(shaderProgram); - switchedProgram = true; - } - - // 确保纹理绑定到纹理单元0,并通知 shader 使用纹理单元0 - RenderSystem.activeTexture(GL13.GL_TEXTURE0); - if (texture != null) { - texture.bind(); - } else { - RenderSystem.bindTexture(0); - } - if (shaderProgram != 0) { - int texLoc = RenderSystem.getUniformLocation(shaderProgram, "uTexture"); - if (texLoc != -1) RenderSystem.uniform1i(texLoc, 0); - } - - if (shaderProgram != 0) { - int loc = RenderSystem.getUniformLocation(shaderProgram, "uModelMatrix"); - if (loc == -1) loc = RenderSystem.getUniformLocation(shaderProgram, "uModel"); - if (loc != -1) RenderSystem.uniformMatrix3(loc, modelMatrix); - } - - // 绑定 VAO 并绘制 - RenderSystem.glBindVertexArray(() -> vaoId); - RenderSystem.drawElements(RenderSystem.DRAW_TRIANGLES, indexCount, - RenderSystem.GL_UNSIGNED_INT, 0); - RenderSystem.glBindVertexArray(() -> 0); - - // 解绑纹理(恢复到单元0的绑定0) - if (texture != null) { - texture.unbind(); - } else { - RenderSystem.bindTexture(0); - } - - // 恢复之前的 program(如果我们切换过) - if (switchedProgram) { - RenderSystem.useProgram(prevProgram); - } - - // 选中框绘制(需要切换到固色 shader) - if (selected && !isRenderVertices) { - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - - RenderSystem.pushState(); - try { - ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader"); - if (solidShader != null && solidShader.programId != 0) { - solidShader.use(); - int modelLoc = solidShader.getUniformLocation("uModelMatrix"); - if (modelLoc != -1) { - RenderSystem.uniformMatrix3(modelLoc, modelMatrix); - } - int colorLoc = solidShader.getUniformLocation("uColor"); - if (colorLoc != -1) { - RenderSystem.uniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); - } - } - - if (isInMultiSelection()) { - drawMultiSelectionBox(); - } else { - drawSelectBox(); - } - } finally { - RenderSystem.popState(); - } - } - - RanderToolsManager.getInstance().renderAllTools(modelMatrix, this); - - if (isSuspension && !selected) { - RenderSystem.pushState(); - - ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader"); - if (solidShader != null && solidShader.programId != 0) { - solidShader.use(); - int modelLoc = solidShader.getUniformLocation("uModelMatrix"); - if (modelLoc != -1) { - RenderSystem.uniformMatrix3(modelLoc, modelMatrix); - } - int colorLoc = solidShader.getUniformLocation("uColor"); - if (colorLoc != -1) { - RenderSystem.uniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); - } - } - - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - - Tesselator t = Tesselator.getInstance(); - BufferBuilder bb = t.getBuilder(); - - BoundingBox bbox = getBounds(); - if (bbox != null && bbox.isValid()) { - bb.begin(GL11.GL_LINE_LOOP, 4); - bb.setColor(new Vector4f(1f, 0f, 0f, 1f)); - bb.vertex(bbox.getMinX(), bbox.getMinY(), 0f, 0f); - bb.vertex(bbox.getMaxX(), bbox.getMinY(), 0f, 0f); - bb.vertex(bbox.getMaxX(), bbox.getMaxY(), 0f, 0f); - bb.vertex(bbox.getMinX(), bbox.getMaxY(), 0f, 0f); - t.end(); - - String hoverText = getName(); - float textX = bbox.getMaxX() + 5f; - float textY = bbox.getMaxY(); - Vector4f bgColor = new Vector4f(1f, 0f, 0f, 0.8f); - Vector4f fgColor = new Vector4f(1f, 1f, 1f, 1f); - - float lineHeight = 18f; - - List lines = splitLines(hoverText, 30); - - float textHeight = lines.size() * lineHeight; - float textWidth = 0f; - for (String line : lines) { - textWidth = Math.max(textWidth, ModelRender.getTextRenderer().getTextWidth(line)); - } - - bb.begin(GL11.GL_TRIANGLES, 6); - bb.setColor(bgColor); - bb.vertex(textX, textY, 0f, 0f); - bb.vertex(textX + textWidth, textY, 0f, 0f); - bb.vertex(textX + textWidth, textY + textHeight, 0f, 0f); - bb.vertex(textX + textWidth, textY + textHeight, 0f, 0f); - bb.vertex(textX, textY + textHeight, 0f, 0f); - bb.vertex(textX, textY, 0f, 0f); - t.end(); - - for (int i = 0; i < lines.size(); i++) { - String line = lines.get(i); - ModelRender.renderText(line, textX, textY + (i + 1) * lineHeight - 5, fgColor); - } - } - - RenderSystem.popState(); - } - } - - public static List splitLines(String text, int maxCharsPerLine) { - List lines = new ArrayList<>(); - StringBuilder line = new StringBuilder(); - - for (String word : text.split(" ")) { - if (line.length() + word.length() + 1 > maxCharsPerLine) { - if (!line.isEmpty()) { - lines.add(line.toString()); - line = new StringBuilder(); - } - if (word.length() > maxCharsPerLine) { - int start = 0; - while (start < word.length()) { - int end = Math.min(start + maxCharsPerLine, word.length()); - lines.add(word.substring(start, end)); - start = end; - } - continue; - } - } - if (!line.isEmpty()) { - line.append(" "); - } - line.append(word); - } - if (!line.isEmpty()) { - lines.add(line.toString()); - } - return lines; - } - - private void drawSelectBox() { - BoundingBox bounds = getBounds(); - MultiSelectionBoxRenderer.drawSelectBox(bounds, pivot); - } - - /** - * 添加网格到多选列表 - */ - public void addToMultiSelection(Mesh2D mesh) { - if (mesh == null || mesh == this || multiSelectedParts.contains(mesh)) { - return; - } - - multiSelectedParts.add(mesh); - multiSelectionDirty = true; - markDirty(); - - if (!mesh.multiSelectedParts.contains(this)) { - mesh.multiSelectedParts.add(this); - mesh.multiSelectionDirty = true; - mesh.markDirty(); - } - - logger.debug("网格 {} 添加到 {} 的多选列表", mesh.getName(), this.getName()); - } - - /** - * 从多选列表移除网格 - */ - public void removeFromMultiSelection(Mesh2D mesh) { - if (multiSelectedParts.remove(mesh)) { - multiSelectionDirty = true; - markDirty(); - - if (mesh.multiSelectedParts.remove(this)) { - mesh.multiSelectionDirty = true; - mesh.markDirty(); - } - - logger.debug("网格 {} 从 {} 的多选列表移除", mesh.getName(), this.getName()); - } - } - - /** - * 清空多选列表 - */ - public void clearMultiSelection() { - if (multiSelectedParts.isEmpty()) { - return; - } - - List toRemove = new ArrayList<>(multiSelectedParts); - - for (Mesh2D mesh : toRemove) { - removeFromMultiSelection(mesh); - } - - multiSelectedParts.clear(); - multiSelectionDirty = true; - markDirty(); - - logger.debug("清空网格 {} 的多选列表", this.getName()); - } - - /** - * 获取多选列表 - */ - public List getMultiSelectedParts() { - return List.copyOf(multiSelectedParts); - } - - /** - * 检查是否在多选状态 - */ - public boolean isInMultiSelection() { - return !multiSelectedParts.isEmpty(); - } - - /** - * 获取多选状态下的组合边界框 - */ - public BoundingBox getMultiSelectionBounds() { - if (multiSelectionDirty) { - updateMultiSelectionBounds(); - } - return new BoundingBox(multiSelectionBounds); - } - - /** - * 更新多选边界框 - */ - private void updateMultiSelectionBounds() { - multiSelectionBounds.reset(); - - BoundingBox selfBounds = getBounds(); - if (selfBounds.isValid()) { - multiSelectionBounds.expand(selfBounds); - } - - for (Mesh2D mesh : multiSelectedParts) { - mesh.updateBounds(); - BoundingBox meshBounds = mesh.getBounds(); - if (meshBounds.isValid()) { - multiSelectionBounds.expand(meshBounds); - } - } - - multiSelectionDirty = false; - - logger.debug("更新多选边界: [{}, {}, {}, {}]", - multiSelectionBounds.getMinX(), multiSelectionBounds.getMinY(), - multiSelectionBounds.getMaxX(), multiSelectionBounds.getMaxY()); - } - - /** - * 强制更新多选边界框(在外部变换操作后调用) - */ - public void forceUpdateMultiSelectionBounds() { - multiSelectionDirty = true; - updateMultiSelectionBounds(); - - for (Mesh2D mesh : multiSelectedParts) { - mesh.multiSelectionDirty = true; - } - } - - /** - * 检查点是否在多选边界框内 - */ - public boolean multiSelectionContainsPoint(float x, float y) { - if (!isInMultiSelection()) { - return containsPoint(x, y); - } - - BoundingBox multiBounds = getMultiSelectionBounds(); - return multiBounds.contains(x, y); - } - - public boolean multiSelectionContainsPoint(Vector2f point) { - return multiSelectionContainsPoint(point.x, point.y); - } - - /** - * 在多选状态下绘制组合边界框 - */ - private void drawMultiSelectionBox() { - BoundingBox multiBounds = getMultiSelectionBounds(); - MultiSelectionBoxRenderer.drawMultiSelectionBox(multiBounds); - } - - /** - * 绘制虚线边框 - */ - private void drawDashedBorder(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { - final float DASH_LENGTH = 8.0f; // 虚线段的长度 - final float GAP_LENGTH = 4.0f; // 间隔的长度 - final Vector4f BORDER_COLOR = new Vector4f(1.0f, 1.0f, 0.0f, 1.0f); // 黄色虚线 - - float width = maxX - minX; - float height = maxY - minY; - - // 绘制上边虚线 - drawDashedLine(bb, minX, minY, maxX, minY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR); - - // 绘制右边虚线 - drawDashedLine(bb, maxX, minY, maxX, maxY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR); - drawDashedLine(bb, maxX, maxY, minX, maxY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR); - drawDashedLine(bb, minX, maxY, minX, minY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR); - } - - /** - * 绘制虚线线段 - */ - private void drawDashedLine(BufferBuilder bb, float startX, float startY, float endX, float endY, - float dashLength, float gapLength, Vector4f color) { - float dx = endX - startX; - float dy = endY - startY; - float lineLength = (float) Math.sqrt(dx * dx + dy * dy); - float dashCount = lineLength / (dashLength + gapLength); - - float dirX = dx / lineLength; - float dirY = dy / lineLength; - - int segmentCount = (int) Math.ceil(dashCount); - - for (int i = 0; i < segmentCount; i++) { - float segmentStart = i * (dashLength + gapLength); - float segmentEnd = segmentStart + dashLength; - - // 确保不超过线段总长度 - if (segmentStart > lineLength) break; - if (segmentEnd > lineLength) segmentEnd = lineLength; - - float segStartX = startX + dirX * segmentStart; - float segStartY = startY + dirY * segmentStart; - float segEndX = startX + dirX * segmentEnd; - float segEndY = startY + dirY * segmentEnd; - - // 绘制虚线段 - bb.begin(GL11.GL_LINES, 2); - bb.setColor(color); - bb.vertex(segStartX, segStartY, 0.0f, 0.0f); - bb.vertex(segEndX, segEndY, 0.0f, 0.0f); - bb.endImmediate(); - } - } - - /** - * 绘制多选状态下的调整手柄 - */ - private void drawMultiSelectionResizeHandles(BufferBuilder bb, float minX, float minY, float maxX, float maxY, - float cornerSize, float borderThickness) { - Vector4f handleColor = new Vector4f(1.0f, 1.0f, 0.0f, 1.0f); // 黄色手柄 - - // 绘制四个角点 - drawCornerHandle(bb, minX, minY, handleColor, cornerSize); // 左上 - drawCornerHandle(bb, maxX, minY, handleColor, cornerSize); // 右上 - drawCornerHandle(bb, minX, maxY, handleColor, cornerSize); // 左下 - drawCornerHandle(bb, maxX, maxY, handleColor, cornerSize); // 右下 - - // 绘制边线中点 - drawEdgeHandle(bb, (minX + maxX) / 2, minY, handleColor, borderThickness); // 上边中点 - drawEdgeHandle(bb, (minX + maxX) / 2, maxY, handleColor, borderThickness); // 下边中点 - drawEdgeHandle(bb, minX, (minY + maxY) / 2, handleColor, borderThickness); // 左边中点 - drawEdgeHandle(bb, maxX, (minY + maxY) / 2, handleColor, borderThickness); // 右边中点 - } - - /** - * 绘制多选中心点 - */ - private void drawMultiSelectionCenterPoint(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { - BoundingBox multiBounds = getMultiSelectionBounds(); - Vector2f center = multiBounds.getCenter(); - - float centerX = center.x; - float centerY = center.y; - float pointSize = 6.0f; - Vector4f centerColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); // 红色中心点 - - // 绘制中心点(十字形) - bb.begin(GL11.GL_LINES, 4); - 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(); - } - - /** - * 绘制多选旋转手柄 - */ - private void drawMultiSelectionRotationHandle(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { - BoundingBox multiBounds = getMultiSelectionBounds(); - Vector2f center = multiBounds.getCenter(); - - float centerX = center.x; - float centerY = center.y; - float rotationHandleY = minY - ROTATION_HANDLE_DISTANCE; - - 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(centerX, 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 = centerX + (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(centerX - arrowSize, rotationHandleY - arrowSize, 0.0f, 0.0f); - bb.vertex(centerX + arrowSize, rotationHandleY + arrowSize, 0.0f, 0.0f); - - bb.vertex(centerX + arrowSize, rotationHandleY - arrowSize, 0.0f, 0.0f); - bb.vertex(centerX - arrowSize, rotationHandleY + arrowSize, 0.0f, 0.0f); - bb.endImmediate(); - } - - private void drawCenterPoint(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { - // 使用 Mesh2D 的 pivot 作为中心点位置,但当 pivot 不在 bounds 内时回退为 bounds 中心(避免渲染时跳回 0,0 的情况) - float centerX = pivot.x; - float centerY = pivot.y; - - // 如果 pivot 不在当前 bounds(可能因为 pivot 是局部坐标或坐标空间不一致),回退到 bounds 中心 - boolean pivotInBounds = (centerX >= minX && centerX <= maxX && centerY >= minY && centerY <= maxY); - - if (!pivotInBounds) { - // 使用 bounds 中心作为可视化中心,避免显示到 (0,0) - centerX = (minX + maxX) * 0.5f; - centerY = (minY + maxY) * 0.5f; - logger.trace("pivot ({},{}) not in bounds -> using bounds center ({},{}) for rendering", pivot.x, pivot.y, centerX, centerY); - } - - float pointSize = 6.0f; // 中心点大小 - Vector4f centerColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); // 红色中心点 - - // 绘制中心点(十字形) - bb.begin(GL11.GL_LINES, 4); - 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("绘制中心点 (rendered): ({}, {}) ; pivot(actual): ({}, {})", centerX, centerY, pivot.x, pivot.y); - } - - - /** - * 绘制旋转手柄 - */ - private void drawRotationHandle(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { - // 计算可视中心(与 drawCenterPoint 的回退逻辑保持一致) - float centerX = pivot.x; - float centerY = pivot.y; - boolean pivotInBounds = (centerX >= minX && centerX <= maxX && centerY >= minY && centerY <= maxY); - if (!pivotInBounds) { - centerX = (minX + maxX) * 0.5f; - centerY = (minY + maxY) * 0.5f; - } - - // 旋转手柄位置(在边界框上方) - 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(); - } - - /** - * 绘制调整大小的手柄 - */ - private void drawResizeHandles(BufferBuilder bb, float minX, float minY, float maxX, float maxY, - float cornerSize, float borderThickness) { - Vector4f handleColor = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); - - // 绘制四个角点 - drawCornerHandle(bb, minX, minY, handleColor, cornerSize); // 左上 - drawCornerHandle(bb, maxX, minY, handleColor, cornerSize); // 右上 - drawCornerHandle(bb, minX, maxY, handleColor, cornerSize); // 左下 - drawCornerHandle(bb, maxX, maxY, handleColor, cornerSize); // 右下 - - // 绘制边线中点(可选) - drawEdgeHandle(bb, (minX + maxX) / 2, minY, handleColor, borderThickness); // 上边中点 - drawEdgeHandle(bb, (minX + maxX) / 2, maxY, handleColor, borderThickness); // 下边中点 - drawEdgeHandle(bb, minX, (minY + maxY) / 2, handleColor, borderThickness); // 左边中点 - drawEdgeHandle(bb, maxX, (minY + maxY) / 2, handleColor, borderThickness); // 右边中点 - } - - private void drawCornerHandle(BufferBuilder bb, float x, float y, Vector4f color, float cornerSize) { - float halfSize = cornerSize / 2; - // 使用 RenderSystem 的常量 - bb.begin(RenderSystem.GL_TRIANGLE_FAN, 4); // 改为 RenderSystem.GL_TRIANGLE_FAN - bb.setColor(color); - bb.vertex(x - halfSize, y - halfSize, 0.0f, 0.0f); - bb.vertex(x + halfSize, y - halfSize, 0.0f, 0.0f); - bb.vertex(x + halfSize, y + halfSize, 0.0f, 0.0f); - bb.vertex(x - halfSize, y + halfSize, 0.0f, 0.0f); - bb.endImmediate(); - } - - private void drawEdgeHandle(BufferBuilder bb, float x, float y, Vector4f color, float borderThickness) { - float halfSize = borderThickness / 2; - // 使用 RenderSystem 的常量 - bb.begin(RenderSystem.GL_TRIANGLE_FAN, 4); // 改为 RenderSystem.GL_TRIANGLE_FAN - bb.setColor(color); - bb.vertex(x - halfSize, y - halfSize, 0.0f, 0.0f); - bb.vertex(x + halfSize, y - halfSize, 0.0f, 0.0f); - bb.vertex(x + halfSize, y + halfSize, 0.0f, 0.0f); - bb.vertex(x - halfSize, y + halfSize, 0.0f, 0.0f); - bb.endImmediate(); - } - - // 角点标记 - private void drawCornerPoints(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { - float cornerSize = 8.0f; // 定义局部常量 - Vector4f cornerColor = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); - - Vector2f[] corners = { - new Vector2f(minX, minY), - new Vector2f(maxX, minY), - new Vector2f(maxX, maxY), - new Vector2f(minX, maxY) - }; - - for (Vector2f corner : corners) { - // 使用 RenderSystem 的常量 - bb.begin(RenderSystem.GL_LINE_STRIP, 3); // 改为 RenderSystem.GL_LINE_STRIP - bb.setColor(cornerColor); - bb.vertex(corner.x - cornerSize, corner.y, 0.0f, 0.0f); - bb.vertex(corner.x, corner.y, 0.0f, 0.0f); - bb.vertex(corner.x, corner.y + cornerSize, 0.0f, 0.0f); - bb.endImmediate(); - } - } - - /** - * 计算模型的边界框 [minX, minY, maxX, maxY] - */ - public float[] calculateBoundingBox() { - // 使用现有的边界计算功能 - BoundingBox bounds = getBounds(); - return new float[]{ - bounds.getMinX(), - bounds.getMinY(), - bounds.getMaxX(), - bounds.getMaxY() - }; - } - - /** - * 计算带扩展的边界框 [minX, minY, maxX, maxY] - */ - public float[] calculateBoundingBox(float expand) { - float[] bounds = calculateBoundingBox(); - return new float[]{ - bounds[0] - expand, - bounds[1] - expand, - bounds[2] + expand, - bounds[3] + expand - }; - } - - public void draw() { - if (!visible) return; - if (indices == null || indices.length == 0) return; - if (!uploaded) { - uploadToGPU(); - } - if (texture != null) { - texture.bind(); - } - GL30.glBindVertexArray(vaoId); - GL11.glDrawElements(GL11.GL_TRIANGLES, indexCount, GL11.GL_UNSIGNED_INT, 0); - GL30.glBindVertexArray(0); - if (texture != null) { - texture.unbind(); - } - } - - /** - * 从 GPU 删除本网格相关的 VAO/VBO/EBO - */ - public void deleteGPU() { - if (!uploaded) return; - // 禁用属性并删除缓冲 - try { - GL30.glBindVertexArray(0); - if (vboId != -1) { - GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); - GL15.glDeleteBuffers(vboId); - vboId = -1; - } - if (eboId != -1) { - GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0); - GL15.glDeleteBuffers(eboId); - eboId = -1; - } - if (vaoId != -1) { - GL30.glDeleteVertexArrays(vaoId); - vaoId = -1; - } - } catch (Exception ignored) { - // 在某些上下文销毁阶段 GL 调用可能不可用 - } finally { - uploaded = false; - } - } - - // ==================== 木偶工具API方法 ==================== - - - /** - * 获取木偶控制点列表 - */ - public List getPuppetPins() { - return new ArrayList<>(puppetPins); - } - - /** - * 通过ID获取木偶控制点 - */ - public PuppetPin getPuppetPin(int id) { - return puppetPins.stream() - .filter(pin -> pin.getId() == id) - .findFirst() - .orElse(null); - } - - /** - * 清空所有木偶控制点 - */ - public void clearPuppetPins() { - puppetPins.clear(); - selectedPuppetPin = null; - markDirty(); - } - - /** - * 重置所有木偶控制点到原始位置 - */ - public void resetPuppetPinsToOriginal() { - for (PuppetPin pin : puppetPins) { - pin.resetToOriginal(); - } - updateVerticesFromPuppetPins(); - } - - /** - * 保存当前木偶控制点位置为原始位置 - */ - public void savePuppetPinsAsOriginal() { - for (PuppetPin pin : puppetPins) { - pin.saveAsOriginal(); - } - // 同时保存当前顶点为原始顶点 - saveAsOriginal(); - } - - // ==================== Getter/Setter ==================== - - public String getName() { - if (modelPart != null) { - return modelPart.getName(); - } - return name; - } - - public void setName(String name) { - this.name = name; - } - - public float[] getVertices() { - return (vertices != null) ? vertices.clone() : new float[0]; - } - - public float[] getUVs() { - return uvs.clone(); - } - - public int[] getIndices() { - return indices.clone(); - } - - public Texture getTexture() { - return texture; - } - - public void setTexture(Texture texture) { - this.texture = texture; - } - - public boolean isVisible() { - return visible; - } - - public void setVisible(boolean visible) { - this.visible = visible; - } - - public int getDrawMode() { - return drawMode; - } - - public void setDrawMode(int drawMode) { - if (drawMode < POINTS || drawMode > TRIANGLE_FAN) { - throw new IllegalArgumentException("Invalid draw mode: " + drawMode); - } - this.drawMode = drawMode; - } - - public int getIndexCount() { - return indices.length; - } - - // ==================== 工具方法 ==================== - - /** - * 创建网格的深拷贝 - */ - 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]; - - // 保留 originalVertices(如果有),否则把当前 vertices 作为原始数据 - copy.originalVertices = this.originalVertices != null ? this.originalVertices.clone() : copy.vertices.clone(); - - // 复制 pivot 与 originalPivot - copy.pivot = new Vector2f(this.pivot); - copy.originalPivot = new Vector2f(this.originalPivot); - - // 复制渲染/状态字段(保留纹理引用,但重置 GPU 句柄) - copy.texture = this.texture; - copy.visible = this.visible; - copy.drawMode = this.drawMode; - copy.bakedToWorld = this.bakedToWorld; - - // 重置 GPU 相关句柄,强制重新 uploadToGPU() 在渲染线程执行 - copy.vaoId = -1; - copy.vboId = -1; - copy.eboId = -1; - copy.indexCount = this.indices != null ? this.indices.length : 0; - copy.uploaded = false; - - // 状态标记 - copy.dirty = true; - copy.boundsDirty = true; - copy.bounds = new BoundingBox(); - copy.selected = this.selected; - - for (SecondaryVertex vertex : this.secondaryVertices) { - Vector2f pos = vertex.getPosition(); - Vector2f uv = vertex.getUV(); - Vector2f origPos = vertex.getOriginalPosition(); - - SecondaryVertex copiedVertex = copy.addSecondaryVertex(pos, uv); - copiedVertex.setPosition(origPos); // 设置原始位置 - copiedVertex.saveAsOriginal(); // 保存为原始位置 - - if (vertex.isSelected()) { - copy.setSelectedSecondaryVertex(copiedVertex); - } - } - - copy.showSecondaryVertices = this.showSecondaryVertices; - copy.secondaryVertexColor = new Vector4f(this.secondaryVertexColor); - copy.selectedSecondaryVertexColor = new Vector4f(this.selectedSecondaryVertexColor); - copy.secondaryVertexSize = this.secondaryVertexSize; - - return copy; - } - - - public int getVaoId() { - return vaoId; - } - - /** - * 获取绘制模式字符串 - */ - public String getDrawModeString() { - switch (drawMode) { - case POINTS: - return "POINTS"; - case LINES: - return "LINES"; - case LINE_STRIP: - return "LINE_STRIP"; - case TRIANGLES: - return "TRIANGLES"; - case TRIANGLE_STRIP: - return "TRIANGLE_STRIP"; - case TRIANGLE_FAN: - return "TRIANGLE_FAN"; - default: - return "UNKNOWN"; - } - } - - public ModelPart getModelPart() { - return modelPart; - } - - /** - * 标记或查询网格顶点是否已经被烘焙到世界坐标 - */ - public void setBakedToWorld(boolean baked) { - this.bakedToWorld = baked; - } - - public boolean isBakedToWorld() { - return bakedToWorld; - } - - // ==================== Object 方法 ==================== - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Mesh2D mesh2D = (Mesh2D) o; - return visible == mesh2D.visible && - drawMode == mesh2D.drawMode && - Objects.equals(name, mesh2D.name) && - Objects.deepEquals(vertices, mesh2D.vertices) && - Objects.deepEquals(uvs, mesh2D.uvs) && - Objects.deepEquals(indices, mesh2D.indices) && - Objects.equals(pivot, mesh2D.pivot); - } - - @Override - public int hashCode() { - return Objects.hash(name, - java.util.Arrays.hashCode(vertices), - java.util.Arrays.hashCode(uvs), - java.util.Arrays.hashCode(indices), - pivot, - visible, drawMode); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("Mesh2D{") - .append("name='").append(name).append('\'') - .append(", vertices=").append(getVertexCount()) - .append(", secondaryVertices=").append(secondaryVertices.size()) - .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(", selected=").append(selected) - .append(", inMultiSelection=").append(isInMultiSelection()) - .append(", multiSelectionCount=").append(multiSelectedParts.size()) - .append(", drawMode=").append(getDrawModeString()) - .append(", bounds=").append(getBounds()); - - if (isInMultiSelection()) { - sb.append(", multiSelectionBounds=").append(getMultiSelectionBounds()); - } - - if (vertices != null && vertices.length > 0) { - sb.append(", coordinates=["); - for (int i = 0; i < vertices.length; i += 2) { - if (i > 0) sb.append(", "); - sb.append("(") - .append(String.format("%.2f", vertices[i])) - .append(", ") - .append(String.format("%.2f", vertices[i + 1])) - .append(")"); - } - sb.append("]"); - } - - sb.append('}'); - return sb.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/PuppetPin.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/PuppetPin.java index af9957f..9d0cb1a 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/PuppetPin.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/PuppetPin.java @@ -1,5 +1,6 @@ package com.chuangzhou.vivid2D.render.model.util; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import org.joml.Vector2f; import java.util.HashMap; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java deleted file mode 100644 index 444a75c..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java +++ /dev/null @@ -1,222 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import org.joml.Vector2f; - -import java.util.ArrayList; -import java.util.List; - -public class SecondaryVertex { - - private final Vector2f position; - private final Vector2f originalPosition; - private final Vector2f uv; - - private int id; - private static int nextId = 0; - private boolean selected = false; - - private final transient Vector2f worldPosition = new Vector2f(); - private final transient Vector2f renderPosition = new Vector2f(); - - private final ControlShape controlShape; - - public void setId(int id) { - this.id = id; - } - - public void setSelected(boolean b) { - this.selected = b; - } - - public boolean isSelected() { - return selected; - } - - public static class ControlShape { - private final int shapeId; - private static int nextShapeId = 0; - private boolean pinned = false; - private boolean locked = false; - private final Vector2f minControlPoint = new Vector2f(0, 0); - private final Vector2f maxControlPoint = new Vector2f(0, 0); - private final List controlVertices = new ArrayList<>(); - - public ControlShape() { - this.shapeId = nextShapeId++; - } - - public int getShapeId() { - return shapeId; - } - - public boolean isPinned() { - return pinned; - } - - public void setPinned(boolean pinned) { - this.pinned = pinned; - } - - public boolean isLocked() { - return locked; - } - - public void setLocked(boolean locked) { - this.locked = locked; - } - - public Vector2f getMinControlPoint() { - return new Vector2f(minControlPoint); - } - - public void setMinControlPoint(Vector2f minControlPoint) { - this.minControlPoint.set(minControlPoint); - } - - public Vector2f getMaxControlPoint() { - return new Vector2f(maxControlPoint); - } - - public void setMaxControlPoint(Vector2f maxControlPoint) { - this.maxControlPoint.set(maxControlPoint); - } - - public List getControlVertices() { - return controlVertices; - } - - public void addControlVertex(SecondaryVertex vertex) { - if (vertex != null && !controlVertices.contains(vertex)) { - controlVertices.add(vertex); - } - } - - public void removeControlVertex(SecondaryVertex vertex) { - controlVertices.remove(vertex); - } - - public void clearControlVertices() { - controlVertices.clear(); - } - - @Override - public String toString() { - return String.format("ControlShape{id=%d, pinned=%s, locked=%s, min=(%.2f, %.2f), max=(%.2f, %.2f), vertCount=%d}", - shapeId, pinned, locked, minControlPoint.x, minControlPoint.y, maxControlPoint.x, maxControlPoint.y, controlVertices.size()); - } - } - - public SecondaryVertex(float x, float y, float u, float v) { - this.position = new Vector2f(x, y); - this.originalPosition = new Vector2f(x, y); - this.uv = new Vector2f(u, v); - this.id = nextId++; - this.controlShape = new ControlShape(); - } - - public Vector2f getPosition() { - return new Vector2f(position); - } - - public void setPosition(float x, float y) { - this.position.set(x, y); - } - - public void setPosition(Vector2f position) { - this.position.set(position); - } - - public void move(float dx, float dy) { - this.position.add(dx, dy); - } - - public Vector2f getOriginalPosition() { - return new Vector2f(originalPosition); - } - - public void setOriginalPosition(Vector2f originalPosition) { - this.originalPosition.set(originalPosition); - } - - public void resetToOriginal() { - this.position.set(originalPosition); - } - - public void saveAsOriginal() { - this.originalPosition.set(position); - } - - - public Vector2f getUV() { - return new Vector2f(uv); - } - - public void setUV(float u, float v) { - this.uv.set(u, v); - } - - public void setWorldPosition(Vector2f p) { - if (p == null) return; - this.worldPosition.set(p); - } - - public Vector2f getRenderPosition() { - return new Vector2f(renderPosition); - } - - public void setRenderPosition(float x, float y) { - this.renderPosition.set(x, y); - } - - public void setRenderPosition(Vector2f p) { - if (p == null) return; - this.renderPosition.set(p); - } - - public int getId() { - return id; - } - - public boolean isPinned() { - return controlShape.isPinned(); - } - - public void setPinned(boolean pinned) { - this.controlShape.setPinned(pinned); - } - - public boolean isLocked() { - return controlShape.isLocked(); - } - - public void setLocked(boolean locked) { - this.controlShape.setLocked(locked); - } - - public ControlShape getControlShape() { - return controlShape; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - SecondaryVertex that = (SecondaryVertex) o; - return id == that.id; - } - - @Override - public int hashCode() { - return id; - } - - @Override - public String toString() { - return String.format("SecondaryVertex{id=%d, position=(%.2f, %.2f), uv=(%.2f, %.2f), pinned=%s, locked=%s, shape=%s}", - id, position.x, position.y, uv.x, uv.y, isPinned(), isLocked(), controlShape.toString()); - } - - public static void resetNextId(int newNextId) { - nextId = newNextId; - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java new file mode 100644 index 0000000..8620255 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java @@ -0,0 +1,130 @@ +package com.chuangzhou.vivid2D.render.model.util; + +import org.joml.Vector2f; + +import java.util.Objects; + +/** + * 封装一个2D顶点,包含位置、UV坐标和原始位置。 + * + * @author Gemini + */ +public class Vertex { + + public Vector2f position; // 当前顶点位置 (x, y) + public Vector2f uv; // UV坐标 (u, v) + public Vector2f originalPosition; // 原始顶点位置 (用于变形) + private VertexTag tag; + private boolean selected; + + /** + * 构造函数 + * + * @param x 顶点 x 坐标 + * @param y 顶点 y 坐标 + * @param u 顶点 u 坐标 + * @param v 顶点 v 坐标 + */ + public Vertex(float x, float y, float u, float v) { + this.position = new Vector2f(x, y); + this.uv = new Vector2f(u, v); + this.originalPosition = new Vector2f(x, y); // 初始时,原始位置=当前位置 + this.tag = VertexTag.DEFAULT; + } + + /** + * 构造函数 + * + * @param position 顶点位置 + * @param uv UV坐标 + */ + public Vertex(Vector2f position, Vector2f uv) { + this.position = new Vector2f(position); + this.uv = new Vector2f(uv); + this.originalPosition = new Vector2f(position); + this.tag = VertexTag.DEFAULT; + } + + /** + * 构造函数(用于复制) + * + * @param position 顶点位置 + * @param uv UV坐标 + * @param originalPosition 原始位置 + */ + public Vertex(Vector2f position, Vector2f uv, Vector2f originalPosition) { + this.position = new Vector2f(position); + this.uv = new Vector2f(uv); + this.originalPosition = new Vector2f(originalPosition); + this.tag = VertexTag.DEFAULT; + } + + public VertexTag getTag() { + return tag; + } + + public void setTag(VertexTag tag) { + this.tag = tag; + } + + /** + * 重置为原始位置 + */ + public void resetToOriginal() { + this.position.set(this.originalPosition); + } + + /** + * 保存当前位置为新的原始位置 + */ + public void saveAsOriginal() { + this.originalPosition.set(this.position); + } + + /** + * 设置顶点是否被选中(在渲染中被调用) + * @param selected 是否被选中 + */ + public void setSelected(boolean selected) { + this.selected = selected; + } + + /** + * 获取顶点是否被选中 + * @return 是否被选中 + */ + public boolean isSelected() { + return selected; + } + + /** + * 创建此顶点的深拷贝 + */ + public Vertex copy() { + return new Vertex(this.position, this.uv, this.originalPosition); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Vertex vertex = (Vertex) o; + return Objects.equals(position, vertex.position) && + Objects.equals(uv, vertex.uv) && + Objects.equals(originalPosition, vertex.originalPosition); + } + + @Override + public int hashCode() { + return Objects.hash(position, uv, originalPosition); + } + + @Override + public String toString() { + return "Vertex{" + + "pos=" + position + + ", uv=" + uv + + ", orig=" + originalPosition + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexList.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexList.java new file mode 100644 index 0000000..f232690 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexList.java @@ -0,0 +1,188 @@ +package com.chuangzhou.vivid2D.render.model.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +/** + * 一个带有名称和标签的 Vertex 集合。 + * + * @author Gemini + */ +public class VertexList implements Iterable { + + public final List vertices; + private String name; + + /** + * 默认构造函数。 + * 名称默认为 "KongFuZi",标签默认为 "DEFAULT"。 + */ + public VertexList() { + this.vertices = new ArrayList<>(); + this.name = "KongFuZi"; + } + + /** + * 构造函数 + * + * @param name 此列表的名称 + * @param tag 此列表的标签 (VertexTag) + */ + public VertexList(String name) { + this.vertices = new ArrayList<>(); + this.name = Objects.requireNonNull(name, "Name cannot be null"); + } + + /** + * 构造函数 + * + * @param name 此列表的名称 + * @param tag 此列表的标签 + * @param initialVertices 用于初始化列表的顶点集合 + */ + public VertexList(String name, Collection initialVertices) { + this(name); + if (initialVertices != null) { + this.vertices.addAll(initialVertices); + } + } + + // --- 列表管理 --- + + /** + * 向列表末尾添加一个顶点。 + * + * @param vertex 要添加的顶点 + */ + public void add(Vertex vertex) { + if (vertex != null) { + this.vertices.add(vertex); + } + } + + /** + * 移除列表中的指定顶点。 + * + * @param vertex 要移除的顶点 + * @return 如果成功移除则为 true + */ + public boolean remove(Vertex vertex) { + return this.vertices.remove(vertex); + } + + /** + * 移除指定索引处的顶点。 + * + * @param index 要移除的索引 + * @return 被移除的顶点 + */ + public Vertex remove(int index) { + return this.vertices.remove(index); + } + + /** + * 获取指定索引处的顶点。 + * + * @param index 索引 + * @return 顶点 + */ + public Vertex get(int index) { + return this.vertices.get(index); + } + + /** + * 返回列表中的顶点数量。 + * + * @return 顶点数量 + */ + public int size() { + return this.vertices.size(); + } + + /** + * 检查列表是否为空。 + * + * @return 如果为空则为 true + */ + public boolean isEmpty() { + return this.vertices.isEmpty(); + } + + /** + * 清空列表中的所有顶点。 + */ + public void clear() { + this.vertices.clear(); + } + + /** + * 返回内部列表的只读视图。 + * (注意:如果需要修改,请使用 add/remove 等方法) + * + * @return 顶点的列表 + */ + public List getVertices() { + return new ArrayList<>(vertices); // 返回一个副本以防止外部修改 + } + + // --- Getter 和 Setter --- + + /** + * 获取此列表的名称。 + */ + public String getName() { + return name; + } + + /** + * 设置此列表的名称。 + * + * @param name 新名称 + */ + public void setName(String name) { + this.name = Objects.requireNonNull(name, "Name cannot be null"); + } + + // --- 迭代器 --- + + /** + * 返回顶点的迭代器。 + */ + @Override + public @NotNull Iterator iterator() { + return this.vertices.iterator(); + } + + // --- Object 方法 --- + + @Override + public String toString() { + return "VertexList{" + + "name='" + name + '\'' + + ", vertexCount=" + vertices.size() + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + VertexList that = (VertexList) o; + return Objects.equals(vertices, that.vertices) && + Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(vertices, name); + } + + public void set(int index, Vertex vertex) { + this.vertices.set(index, vertex); + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexTag.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexTag.java new file mode 100644 index 0000000..1877101 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexTag.java @@ -0,0 +1,21 @@ +package com.chuangzhou.vivid2D.render.model.util; + +/** + * 定义 VertexList 的标签类型。 + */ +public enum VertexTag { + /** + * 用于变形的顶点 + */ + DEFORMATION, + + /** + * 默认的顶点 + */ + DEFAULT, + + /** + * 其他类型的顶点 + */ + OTHER +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/manager/RanderToolsManager.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/manager/RanderToolsManager.java index a0e628f..01ec103 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/manager/RanderToolsManager.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/manager/RanderToolsManager.java @@ -3,6 +3,7 @@ package com.chuangzhou.vivid2D.render.model.util.manager; import com.chuangzhou.vivid2D.render.awt.tools.Tool; import com.chuangzhou.vivid2D.render.model.util.tools.RanderTools; import org.joml.Matrix3f; +import org.slf4j.Logger; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -12,6 +13,7 @@ import java.util.concurrent.ConcurrentHashMap; * 负责管理Tool和RanderTools之间的绑定关系 */ public class RanderToolsManager { + private static Logger logger = org.slf4j.LoggerFactory.getLogger(RanderToolsManager.class); // 存储Tool和RanderTools的绑定关系 private final Map toolRanderToolsMap; @@ -74,7 +76,8 @@ public class RanderToolsManager { successCount++; } } catch (Exception e) { - System.err.println("渲染工具执行失败: " + randerTools.getClass().getSimpleName() + " - " + e.getMessage()); + e.printStackTrace(); + logger.info("渲染工具执行失败: {} - {}", randerTools.getClass().getSimpleName(), e.getMessage()); } } return successCount; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/LiquifyTargetPartRander.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/LiquifyTargetPartRander.java deleted file mode 100644 index 386c31c..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/LiquifyTargetPartRander.java +++ /dev/null @@ -1,363 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util.tools; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.TextRenderer; -import com.chuangzhou.vivid2D.render.model.util.BoundingBox; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; -import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; -import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement; -import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram; -import org.joml.Matrix3f; -import org.joml.Vector4f; -import org.lwjgl.opengl.GL11; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * LiquifyTargetPartRander - 优化版(针对 isRenderVertices 大网格卡顿做了多项性能优化) - * - * 优化点: - * 1. 对大网格(顶点数超过阈值)降低绘制细节:不绘制内部三角线,采样绘制顶点(点与编号减少频率)。 - * 2. 将多次 begin()/endImmediate() 批次化:尽量合并连续的线段绘制到单次 begin/end。 - * 3. 降低每顶点的内存分配与 API 调用(把文本渲染请求收集、复用局部变量)。 - * 4. dashed line 绘制改为在小段数量内合并绘制或直接绘制单条轻量直线以减少 draw call。 - * 5. 对 circle/point 的 segment 数做自适应限制(越大网格越少细分)。 - */ -public class LiquifyTargetPartRander extends RanderTools { - private final Vector4f liquifyOverlayColor = new Vector4f(1.0f, 0.5f, 0.0f, 0.3f); // 半透明橙色 - - // 可调阈值(根据你的机器/场景调小或调大) - private static final int LARGE_VERTEX_THRESHOLD = 1500; // 超过则使用低质量绘制 - private static final int TARGET_VERTEX_DRAW = 300; // 大网格时目标绘制的顶点数量 - - @Override - public void init(Map algorithmEnabled) { - algorithmEnabled.put("showLiquifyOverlay", false); - algorithmEnabled.put("isRenderVertices", false); - } - - @Override - public boolean render(Matrix3f modelMatrix,Object renderContext) { - if (renderContext instanceof Mesh2D mesh2D) { - drawLiquifyOverlay(mesh2D, modelMatrix); - return true; - } - return false; - } - - /** - * 主渲染入口。收集文本命令到 pendingTexts,绘制完成后 popState,再把文本绘制出来(外部渲染) - */ - private void drawLiquifyOverlay(Mesh2D mesh2D, Matrix3f modelMatrix) { - if (!isAlgorithmEnabled("showLiquifyOverlay") || !mesh2D.isShowLiquifyOverlay()) return; - - List pendingTexts = new ArrayList<>(); - - RenderSystem.pushState(); - try { - ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader"); - if (solidShader != null && solidShader.programId != 0) { - solidShader.use(); - int modelLoc = solidShader.getUniformLocation("uModelMatrix"); - if (modelLoc != -1) RenderSystem.uniformMatrix3(modelLoc, modelMatrix); - } - - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - - Tesselator t = Tesselator.getInstance(); - BufferBuilder bb = t.getBuilder(); - - BoundingBox bounds = mesh2D.getBounds(); - if (bounds == null || !bounds.isValid()) return; - - if (!isAlgorithmEnabled("isRenderVertices")) { - // 普通模式:美观渲染(轻填充 + 多层轮廓) - Vector4f shadowCol = new Vector4f(0f, 0f, 0f, 0.08f); - drawRoundedBoundingFill(bb, bounds, shadowCol, 6); - - Vector4f innerFill = new Vector4f(liquifyOverlayColor.x, liquifyOverlayColor.y, liquifyOverlayColor.z, 0.06f); - drawRoundedBoundingFill(bb, bounds, innerFill, 4); - - float[] alphas = new float[]{0.42f, 0.24f, 0.12f}; - float[] expand = new float[]{6f, 3f, 0f}; - for (int i = 0; i < alphas.length; i++) { - Vector4f stroke = new Vector4f(liquifyOverlayColor.x, liquifyOverlayColor.y, liquifyOverlayColor.z, alphas[i]); - drawRoundedBoundingOutline(bb, bounds, stroke, Math.max(1f, 2.5f - i), expand[i]); - } - - collectLiquifyStatusText(mesh2D, pendingTexts); - - } else { - // isRenderVertices 模式:性能优先 + 少量视觉信息 - Vector4f outline = new Vector4f(liquifyOverlayColor.x, liquifyOverlayColor.y, liquifyOverlayColor.z, 0.16f); - drawRoundedBoundingOutline(bb, bounds, outline, 1.0f, 0f); - - // 优化点:根据顶点数量选择低/高质量绘制 - int vertexCount = mesh2D.getRenderVertices() == null ? 0 : mesh2D.getRenderVertices().length / 2; - boolean large = vertexCount > LARGE_VERTEX_THRESHOLD; - - // 边框:始终绘制(合并一次 begin/end) - drawOutlineOnce(bb, mesh2D); - - // 内部线:大网格时跳过以减轻负担 - if (!large && mesh2D.getIndices() != null && mesh2D.getIndices().length >= 3) { - // 合并内线绘制(批量) - drawIndexedInternalLinesBatch(bb, mesh2D); - } - - // 顶点点标记:采样绘制以降低数量 - drawLiquifyVertexPointsOptimized(mesh2D, bb, pendingTexts, large, vertexCount); - drawCompactLiquifyBadge(mesh2D, bb, bounds); - } - - } finally { - RenderSystem.popState(); - } - - // 延迟绘制文本(在外层安全) - if (!pendingTexts.isEmpty()) { - TextRenderer tr = ModelRender.getTextRenderer(); - if (tr != null) { - for (TextItem ti : pendingTexts) { - try { - ModelRender.renderText(ti.text, ti.x, ti.y, ti.color); - } catch (Exception ignored) {} - } - } - } - } - - // ========== 辅助方法 ========== - - private void drawRoundedBoundingFill(BufferBuilder bb, BoundingBox bounds, Vector4f color, float padding) { - float minX = bounds.getMinX() - padding; - float minY = bounds.getMinY() - padding; - float maxX = bounds.getMaxX() + padding; - float maxY = bounds.getMaxY() + padding; - - bb.begin(GL11.GL_TRIANGLES, 6); - bb.setColor(color); - bb.vertex(minX, minY, 0f, 0f); - bb.vertex(maxX, minY, 0f, 0f); - bb.vertex(maxX, maxY, 0f, 0f); - - bb.vertex(maxX, maxY, 0f, 0f); - bb.vertex(minX, maxY, 0f, 0f); - bb.vertex(minX, minY, 0f, 0f); - bb.endImmediate(); - } - - private void drawRoundedBoundingOutline(BufferBuilder bb, BoundingBox bounds, Vector4f color, float lineWidth, float padding) { - float minX = bounds.getMinX() - padding; - float minY = bounds.getMinY() - padding; - float maxX = bounds.getMaxX() + padding; - float maxY = bounds.getMaxY() + padding; - - float lw = Math.max(1.0f, Math.min(lineWidth, 4.0f)); // 限制线宽,避免驱动开销 - GL11.glLineWidth(lw); - bb.begin(GL11.GL_LINE_LOOP, 4); - bb.setColor(color); - bb.vertex(minX, minY, 0f, 0f); - bb.vertex(maxX, minY, 0f, 0f); - bb.vertex(maxX, maxY, 0f, 0f); - bb.vertex(minX, maxY, 0f, 0f); - bb.endImmediate(); - GL11.glLineWidth(1.0f); - } - - // 合并绘制外轮廓(单次 begin/end) - private void drawOutlineOnce(BufferBuilder bb, Mesh2D mesh2D) { - if (mesh2D.getRenderVertices() == null || mesh2D.getRenderVertices().length < 4) return; - Vector4f OUTER_LINE = new Vector4f(1f, 0.85f, 0.35f, 0.12f); - int count = mesh2D.getRenderVertices().length / 2; - GL11.glLineWidth(1.0f); - bb.begin(GL11.GL_LINE_LOOP, count); - bb.setColor(OUTER_LINE); - float[] verts = mesh2D.getRenderVertices(); - for (int i = 0; i < count; i++) { - int base = i * 2; - bb.vertex(verts[base], verts[base + 1], 0f, 0f); - } - bb.endImmediate(); - } - - // 批量绘制内部索引线(只在小网格下使用,避免大量 drawcalls) - private void drawIndexedInternalLinesBatch(BufferBuilder bb, Mesh2D mesh2D) { - int[] idx = mesh2D.getIndices(); - if (idx == null || idx.length < 3) return; - Vector4f innerLine = new Vector4f(1f, 0.6f, 0.0f, 0.06f); - // 每三角产生3条线 => 最多 idx.length lines. 合并为一次 GL_LINES 绘制 - int lines = (idx.length / 3) * 6; // 每三角 6 顶点(3 条线) - bb.begin(GL11.GL_LINES, lines); - bb.setColor(innerLine); - float[] verts = mesh2D.getRenderVertices(); - for (int i = 0; i < idx.length; i += 3) { - int i1 = idx[i]; - int i2 = idx[i + 1]; - int i3 = idx[i + 2]; - // line i1-i2 - bb.vertex(verts[i1 * 2], verts[i1 * 2 + 1], 0f, 0f); - bb.vertex(verts[i2 * 2], verts[i2 * 2 + 1], 0f, 0f); - // line i2-i3 - bb.vertex(verts[i2 * 2], verts[i2 * 2 + 1], 0f, 0f); - bb.vertex(verts[i3 * 2], verts[i3 * 2 + 1], 0f, 0f); - // line i3-i1 - bb.vertex(verts[i3 * 2], verts[i3 * 2 + 1], 0f, 0f); - bb.vertex(verts[i1 * 2], verts[i1 * 2 + 1], 0f, 0f); - } - bb.endImmediate(); - } - - // 顶点点标记 - 优化版:大网格时采样绘制、减少 segment、合并操作 - private void drawLiquifyVertexPointsOptimized(Mesh2D mesh2D, BufferBuilder bb, List pendingTexts, boolean large, int vertexCount) { - if (mesh2D.getRenderVertices() == null) return; - - final float BASE_SIZE = Math.max(1.5f, mesh2D.secondaryVertexSize * (large ? 0.35f : 0.6f)); - final Vector4f FILL = new Vector4f(1f, 0.6f, 0.15f, large ? 0.10f : 0.18f); - final Vector4f STROKE = new Vector4f(1f, 1f, 1f, large ? 0.75f : 0.9f); - - float[] verts = mesh2D.getRenderVertices(); - - // 采样步长,保证绘制点数量不会超过 TARGET_VERTEX_DRAW(越大网格步长越大) - int step = 1; - if (large && vertexCount > TARGET_VERTEX_DRAW) { - step = Math.max(1, vertexCount / TARGET_VERTEX_DRAW); - } - - // 批量绘制填充圆(尽量减少 begin/end) - // 我们使用较低分段数(segments)以降低每个圆的开销 - int segments = large ? 8 : 12; - - // 为减少 draw calls:先填充所有圆(多个 TRIANGLE_FAN 批次合并不现实,因为每个中心不同) - // 但我们至少减少每个圆的 segments 和避免不必要的计算 - for (int i = 0; i < vertexCount; i += step) { - int base = i * 2; - float x = verts[base]; - float y = verts[base + 1]; - - drawHollowCircleOptimized(bb, x, y, BASE_SIZE, FILL, STROKE, segments); - } - - // 编号仅在非常小网格或明确需要时绘制(避免大量文本) - boolean showIndex = false; - if (showIndex && !large) { - TextRenderer tr = ModelRender.getTextRenderer(); - if (tr != null) { - for (int i = 0; i < vertexCount; i += 1) { - int base = i * 2; - float x = verts[base]; - float y = verts[base + 1]; - String numberText = String.valueOf(i); - float textWidth = tr.getTextWidth(numberText); - float textX = x + 6.0f; - float textY = y - 4.0f; - bb.begin(GL11.GL_TRIANGLES, 6); - bb.setColor(new Vector4f(0.06f, 0.06f, 0.06f, 0.7f)); - bb.vertex(textX - 2, textY - 8, 0f, 0f); - bb.vertex(textX + textWidth + 2, textY - 8, 0f, 0f); - bb.vertex(textX + textWidth + 2, textY + 2, 0f, 0f); - bb.vertex(textX + textWidth + 2, textY + 2, 0f, 0f); - bb.vertex(textX - 2, textY + 2, 0f, 0f); - bb.vertex(textX - 2, textY - 8, 0f, 0f); - bb.endImmediate(); - pendingTexts.add(new TextItem(numberText, textX, textY, new Vector4f(1f,1f,1f,0.88f))); - } - } - } - } - - // 更轻量的空心圆绘制(segments 少) - private void drawHollowCircleOptimized(BufferBuilder bb, float cx, float cy, float radius, Vector4f fill, Vector4f stroke, int segments) { - if (radius <= 0f) return; - segments = Math.max(6, Math.min(segments, 16)); - // 填充(每个圆仍需一次 TRIANGLE_FAN) - bb.begin(GL11.GL_TRIANGLE_FAN, segments + 2); - bb.setColor(fill); - bb.vertex(cx, cy, 0f, 0f); - for (int i = 0; i <= segments; i++) { - double ang = 2.0 * Math.PI * i / segments; - float x = cx + (float) Math.cos(ang) * radius; - float y = cy + (float) Math.sin(ang) * radius; - bb.setColor(fill); - bb.vertex(x, y, 0f, 0f); - } - bb.endImmediate(); - - // 描边(LINE_LOOP) - bb.begin(GL11.GL_LINE_LOOP, segments); - bb.setColor(stroke); - for (int i = 0; i < segments; i++) { - double ang = 2.0 * Math.PI * i / segments; - float x = cx + (float) Math.cos(ang) * radius; - float y = cy + (float) Math.sin(ang) * radius; - bb.vertex(x, y, 0f, 0f); - } - bb.endImmediate(); - } - - // compact badge(小徽章) - private void drawCompactLiquifyBadge(Mesh2D mesh2D, BufferBuilder bb, BoundingBox bounds) { - float badgeX = bounds.getMinX() + (bounds.getWidth() * 0.06f); - float badgeY = bounds.getMaxY() - (bounds.getHeight() * 0.06f); - float r = Math.min(bounds.getWidth(), bounds.getHeight()) * 0.035f; - r = Math.max(4f, Math.min(r, 18f)); - - int seg = 12; - bb.begin(GL11.GL_TRIANGLE_FAN, seg + 1); - bb.setColor(new Vector4f(1f, 0.6f, 0.1f, 0.62f)); - bb.vertex(badgeX, badgeY, 0f, 0f); - for (int i = 0; i <= seg; i++) { - float ang = (float) (i * 2 * Math.PI / seg); - bb.vertex(badgeX + (float)Math.cos(ang) * r, badgeY + (float)Math.sin(ang) * r, 0f, 0f); - } - bb.endImmediate(); - - drawSimpleBrushIcon(bb, badgeX, badgeY, r * 0.9f); - } - - private void drawSimpleBrushIcon(BufferBuilder bb, float centerX, float centerY, float size) { - float iconSize = Math.max(1f, size * 0.6f); - bb.begin(GL11.GL_LINES, 2); - bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 0.9f)); - bb.vertex(centerX - iconSize * 0.3f, centerY - iconSize * 0.5f, 0f, 0f); - bb.vertex(centerX, centerY + iconSize * 0.3f, 0f, 0f); - bb.endImmediate(); - bb.begin(GL11.GL_TRIANGLES, 3); - bb.setColor(new Vector4f(1.0f, 0.3f, 0.0f, 0.9f)); - bb.vertex(centerX, centerY + iconSize * 0.3f, 0f, 0f); - bb.vertex(centerX - iconSize * 0.2f, centerY + iconSize * 0.6f, 0f, 0f); - bb.vertex(centerX + iconSize * 0.2f, centerY + iconSize * 0.6f, 0f, 0f); - bb.endImmediate(); - } - - // 收集文本(在 pushState 内只收集,不渲染) - private void collectLiquifyStatusText(Mesh2D mesh2D, List pendingTexts) { - BoundingBox bounds = mesh2D.getBounds(); - if (bounds == null || !bounds.isValid()) return; - String liquifyText = "Liquify"; - String hintText = "Ctrl: Show Vertices"; - TextRenderer tr = ModelRender.getTextRenderer(); - if (tr == null) return; - - float centerX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f; - float textY = bounds.getMaxY() + 20.0f; - float textWidth = tr.getTextWidth("LIQUIFY MODE"); - float textX = centerX - textWidth / 2.0f; - pendingTexts.add(new TextItem("LIQUIFY MODE", textX, textY, new Vector4f(1.0f, 0.8f, 0.0f, 1.0f))); - - float indicatorX = bounds.getMaxX() + Math.max(bounds.getWidth(), bounds.getHeight()) * 0.08f; - float indicatorY = bounds.getMaxY() - Math.max(bounds.getWidth(), bounds.getHeight()) * 0.06f; - float tx = indicatorX - tr.getTextWidth(liquifyText) / 2f; - float ty = indicatorY - Math.max(bounds.getWidth(), bounds.getHeight()) * 0.05f - 6f; - pendingTexts.add(new TextItem(liquifyText, tx, ty, new Vector4f(1f, 0.84f, 0.36f, 0.88f))); - pendingTexts.add(new TextItem(hintText, tx, ty + 14f, new Vector4f(0.8f, 0.8f, 0.8f, 0.65f))); - } - - // 临时文本项 - private record TextItem(String text, float x, float y, Vector4f color) { } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/PuppetDeformationRander.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/PuppetDeformationRander.java deleted file mode 100644 index 056a149..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/PuppetDeformationRander.java +++ /dev/null @@ -1,281 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util.tools; - -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.TextRenderer; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.PuppetPin; -import com.chuangzhou.vivid2D.render.systems.RenderSystem; -import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; -import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; -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.Vector4f; -import org.lwjgl.opengl.GL11; - -import java.util.Map; - -public class PuppetDeformationRander extends RanderTools{ - - // 影响范围渲染颜色 - private final Vector4f influenceRangeColor = new Vector4f(0.3f, 0.3f, 1.0f, 0.3f); // 半透明蓝色 - private final Vector4f influenceBorderColor = new Vector4f(0.1f, 0.1f, 0.8f, 0.6f); // 边框颜色 - - @Override - public void init(Map algorithmEnabled) { - algorithmEnabled.put("showPuppetPins", false); - algorithmEnabled.put("showInfluenceRanges", true); // 控制是否显示影响范围 - } - - @Override - public boolean render(Matrix3f modelMatrix, Object renderContext) { - if (renderContext instanceof Mesh2D mesh2D) { - drawPuppetPins(mesh2D, modelMatrix); - return true; - } - return false; - } - - /** - * 绘制木偶控制点 - */ - private void drawPuppetPins(Mesh2D mesh2D, Matrix3f modelMatrix) { - if (!isAlgorithmEnabled("showPuppetPins") || mesh2D.getPuppetPins().isEmpty()) return; - - RenderSystem.pushState(); - try { - ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader"); - if (solidShader != null && solidShader.programId != 0) { - solidShader.use(); - - // 设置模型矩阵 - int modelLoc = solidShader.getUniformLocation("uModelMatrix"); - if (modelLoc != -1) { - RenderSystem.uniformMatrix3(modelLoc, modelMatrix); - } - } - - RenderSystem.enableBlend(); - RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - - Tesselator t = Tesselator.getInstance(); - BufferBuilder bb = t.getBuilder(); - - // 绘制控制点影响范围 - if (isAlgorithmEnabled("showInfluenceRanges")) { - drawPuppetPinInfluenceRanges(mesh2D, bb); - } - - // 绘制控制点 - for (PuppetPin pin : mesh2D.getPuppetPins()) { - Vector2f position = pin.getPosition(); - Vector4f color = pin.isSelected() ? mesh2D.getSelectedPuppetPinColor() : mesh2D.getPuppetPinColor(); - - drawPuppetPin(bb, position.x, position.y, color, mesh2D.getPuppetPinSize(), pin.isSelected()); - - // 为选中的控制点绘制信息 - if (pin.isSelected()) { - drawPuppetPinInfo(mesh2D, bb, pin, position.x, position.y); - } - } - - } finally { - RenderSystem.popState(); - } - } - - /** - * 绘制控制点影响范围 - 显示实际的权重分布 - */ - private void drawPuppetPinInfluenceRanges(Mesh2D mesh2D, BufferBuilder bb) { - for (PuppetPin pin : mesh2D.getPuppetPins()) { - Vector2f position = pin.getPosition(); - float radius = pin.getInfluenceRadius(); - - // 获取控制点的权重映射 - Map weightMap = pin.getWeightMap(); - if (weightMap == null || weightMap.isEmpty()) continue; - - // 绘制影响范围的网格可视化 - 使用预计算的权重 - drawInfluenceGridFromWeightMap(mesh2D, bb, pin, position, radius, weightMap); - - // 绘制影响范围边界 - drawInfluenceBoundary(bb, position, radius); - } - } - - /** - * 根据预计算的权重映射绘制影响范围 - */ - private void drawInfluenceGridFromWeightMap(Mesh2D mesh2D, BufferBuilder bb, PuppetPin pin, - Vector2f position, float radius, Map weightMap) { - // 创建网格来可视化影响范围 - int gridSize = 12; // 减少网格细分以提高性能 - float cellSize = radius * 2 / gridSize; - - for (int i = 0; i < gridSize; i++) { - for (int j = 0; j < gridSize; j++) { - float x = position.x - radius + i * cellSize + cellSize / 2; - float y = position.y - radius + j * cellSize + cellSize / 2; - - // 计算该点到控制点的距离 - float dx = x - position.x; - float dy = y - position.y; - float distance = (float) Math.sqrt(dx * dx + dy * dy); - - if (distance <= radius) { - // 使用预计算的权重函数,确保与变形算法一致 - float weight = calculateWeight(distance, radius, pin.getFalloffType()); - - // 根据权重设置颜色强度 - Vector4f cellColor = new Vector4f( - influenceRangeColor.x, - influenceRangeColor.y, - influenceRangeColor.z, - influenceRangeColor.w * weight - ); - - // 绘制网格单元 - drawInfluenceCell(bb, x, y, cellSize * 0.8f, cellColor); - } - } - } - } - - /** - * 绘制影响范围边界 - */ - private void drawInfluenceBoundary(BufferBuilder bb, Vector2f position, float radius) { - // 绘制边界圆圈 - bb.begin(GL11.GL_LINE_LOOP, 32); - bb.setColor(influenceBorderColor); - - for (int i = 0; i < 32; i++) { - float angle = (float) (i * 2 * Math.PI / 32); - float x = position.x + (float) Math.cos(angle) * radius; - float y = position.y + (float) Math.sin(angle) * radius; - bb.vertex(x, y, 0f, 0f); - } - bb.endImmediate(); - } - - /** - * 绘制影响范围网格单元 - */ - private void drawInfluenceCell(BufferBuilder bb, float x, float y, float size, Vector4f color) { - float halfSize = size / 2; - - bb.begin(GL11.GL_TRIANGLES, 6); - bb.setColor(color); - - // 绘制小方块 - bb.vertex(x - halfSize, y - halfSize, 0f, 0f); - bb.vertex(x + halfSize, y - halfSize, 0f, 0f); - bb.vertex(x + halfSize, y + halfSize, 0f, 0f); - - bb.vertex(x + halfSize, y + halfSize, 0f, 0f); - bb.vertex(x - halfSize, y + halfSize, 0f, 0f); - bb.vertex(x - halfSize, y - halfSize, 0f, 0f); - - bb.endImmediate(); - } - - /** - * 计算权重(与木偶变形算法保持一致) - */ - private float calculateWeight(float distance, float radius, PuppetPin.FalloffType falloffType) { - if (distance >= radius) return 0.0f; - - float normalizedDistance = distance / radius; - - switch (falloffType) { - case LINEAR: - return 1.0f - normalizedDistance; - case SMOOTH: - return (float) (1.0f - Math.pow(normalizedDistance, 2)); - case SHARP: - return (float) (1.0f - Math.pow(normalizedDistance, 0.5f)); - case CONSTANT: - return 1.0f; - default: - return 1.0f - normalizedDistance; - } - } - - /** - * 绘制木偶控制点(更醒目的样式) - */ - private void drawPuppetPin(BufferBuilder bb, float x, float y, Vector4f color, float size, boolean selected) { - float halfSize = size / 2; - - if (selected) { - // 选中的控制点:带圆圈的十字 - bb.begin(GL11.GL_LINES, 4); - bb.setColor(color); - bb.vertex(x - halfSize, y, 0f, 0f); - bb.vertex(x + halfSize, y, 0f, 0f); - bb.vertex(x, y - halfSize, 0f, 0f); - bb.vertex(x, y + halfSize, 0f, 0f); - bb.endImmediate(); - - // 外圈圆圈 - bb.begin(GL11.GL_LINE_LOOP, 16); - bb.setColor(color); - float circleSize = size * 1.5f; - for (int i = 0; i < 16; i++) { - float angle = (float) (i * 2 * Math.PI / 16); - float cx = x + (float) Math.cos(angle) * circleSize; - float cy = y + (float) Math.sin(angle) * circleSize; - bb.vertex(cx, cy, 0f, 0f); - } - bb.endImmediate(); - } else { - // 普通控制点:实心圆圈 - bb.begin(GL11.GL_TRIANGLE_FAN, 16); - bb.setColor(color); - bb.vertex(x, y, 0f, 0f); // 中心点 - for (int i = 0; i <= 16; i++) { - float angle = (float) (i * 2 * Math.PI / 16); - float cx = x + (float) Math.cos(angle) * halfSize; - float cy = y + (float) Math.sin(angle) * halfSize; - bb.vertex(cx, cy, 0f, 0f); - } - bb.endImmediate(); - - // 边框 - bb.begin(GL11.GL_LINE_LOOP, 16); - bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); - for (int i = 0; i < 16; i++) { - float angle = (float) (i * 2 * Math.PI / 16); - float cx = x + (float) Math.cos(angle) * halfSize; - float cy = y + (float) Math.sin(angle) * halfSize; - bb.vertex(cx, cy, 0f, 0f); - } - bb.endImmediate(); - } - } - - /** - * 绘制控制点信息 - */ - private void drawPuppetPinInfo(Mesh2D mesh2D, BufferBuilder bb, PuppetPin pin, float x, float y) { - String infoText = pin.getName() + " (R:" + (int) pin.getInfluenceRadius() + ")"; - TextRenderer textRenderer = ModelRender.getTextRenderer(); - if (textRenderer != null) { - float textWidth = textRenderer.getTextWidth(infoText); - float textX = x + mesh2D.getPuppetPinSize() + 5.0f; - float textY = y - 6.0f; - bb.begin(GL11.GL_TRIANGLES, 6); - bb.setColor(new Vector4f(0.1f, 0.1f, 0.1f, 0.8f)); - bb.vertex(textX - 3, textY - 10, 0f, 0f); - bb.vertex(textX + textWidth + 3, textY - 10, 0f, 0f); - bb.vertex(textX + textWidth + 3, textY + 4, 0f, 0f); - bb.vertex(textX + textWidth + 3, textY + 4, 0f, 0f); - bb.vertex(textX - 3, textY + 4, 0f, 0f); - bb.vertex(textX - 3, textY - 10, 0f, 0f); - bb.endImmediate(); - ModelRender.renderText(infoText, textX, textY, new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/VertexDeformationRander.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/VertexDeformationRander.java index ce75774..c72f9d1 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/VertexDeformationRander.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/VertexDeformationRander.java @@ -1,400 +1,143 @@ package com.chuangzhou.vivid2D.render.model.util.tools; -import com.chuangzhou.vivid2D.render.ModelRender; -import com.chuangzhou.vivid2D.render.TextRenderer; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex; +import com.chuangzhou.vivid2D.render.model.Mesh2D; +import com.chuangzhou.vivid2D.render.model.util.Vertex; +import com.chuangzhou.vivid2D.render.model.util.VertexTag; import com.chuangzhou.vivid2D.render.systems.RenderSystem; import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; -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.Vector4f; import org.lwjgl.opengl.GL11; -import java.util.Comparator; -import java.util.Map; import java.util.*; +import java.util.stream.Collectors; +/** + * [MODIFIED] 顶点变形渲染工具 + * 该类现在渲染 Mesh2D 中被标记为 VertexTag.DEFORMATION 的一级顶点。 + */ public class VertexDeformationRander extends RanderTools { + + // [NEW] 定义可变形顶点的默认渲染大小 + private static final float DEFORMATION_VERTEX_SIZE = 8.0f; + @Override public void init(Map algorithmEnabled) { - algorithmEnabled.put("showSecondaryVertices", false); - algorithmEnabled.put("showSecondaryVertexInfluence", true); + // [MODIFIED] 更新算法名称以反映新功能 + algorithmEnabled.put("showDeformationVertices", false); + algorithmEnabled.put("showDeformationVertexInfluence", true); } @Override public boolean render(Matrix3f modelMatrix, Object renderContext) { - if (renderContext instanceof Mesh2D mesh2D){ - drawSecondaryVertices(mesh2D, modelMatrix); - return true; + if (renderContext instanceof Mesh2D mesh2D) { + // [MODIFIED] 检查新的算法开关并调用新的渲染方法 + if (mesh2D.getStates("showDeformationVertices")) { + drawDeformationVertices(mesh2D); + return true; + } } return false; } - private void drawSecondaryVertices(Mesh2D mesh2D, Matrix3f modelMatrix) { - if (!isAlgorithmEnabled("showSecondaryVertices") || mesh2D.getSecondaryVertices().isEmpty() || !mesh2D.isShowSecondaryVertices()) return; + /** + * [MODIFIED] 重写此方法以渲染被标记为 DEFORMATION 的一级顶点。 + * @param mesh2D 要渲染的网格 + */ + private void drawDeformationVertices(Mesh2D mesh2D) { + // 1. 筛选出所有可变形的顶点 + List deformationVertices = mesh2D.getActiveVertexList().vertices.stream() + .filter(v -> v.getTag() == VertexTag.DEFORMATION) + .collect(Collectors.toList()); + + if (deformationVertices.isEmpty()) { + return; // 没有可变形顶点,无需渲染 + } + + Tesselator t = Tesselator.getInstance(); + BufferBuilder bb = t.getBuilder(); + RenderSystem.pushState(); try { - - ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader"); - if (solidShader != null && solidShader.programId != 0) { - solidShader.use(); - int modelLoc = solidShader.getUniformLocation("uModelMatrix"); - if (modelLoc != -1) RenderSystem.uniformMatrix3(modelLoc, modelMatrix); - } RenderSystem.enableBlend(); RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - GL11.glEnable(GL11.GL_LINE_SMOOTH); - GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST); - Tesselator t = Tesselator.getInstance(); - BufferBuilder bb = t.getBuilder(); - java.util.List verts = mesh2D.getSecondaryVertices(); + GL11.glDisable(GL11.GL_DEPTH_TEST); - // 1) 绘制所有控制点连线(按存储顺序),但使用 LINE_LOOP 以保证闭合(如果需要闭合) - if (verts.size() >= 2) { - // 如果顶点存储顺序不能保证多边形闭合,仍能作为参考线显示 - bb.begin(GL11.GL_LINE_STRIP, verts.size()); - bb.setColor(new Vector4f(1f, 1f, 1f, 0.12f)); - for (SecondaryVertex v : verts) { - Vector2f p = v.getPosition(); - bb.vertex(p.x, p.y, 0f, 0f); - } - bb.endImmediate(); + // 2. 绘制连接线 + drawConnectionLines(bb, deformationVertices); + + // 3. 绘制每个可变形顶点 + for (Vertex vertex : deformationVertices) { + Vector2f p = vertex.position; + + // 定义颜色 + Vector4f centerCol = new Vector4f(0.1f, 0.9f, 0.2f, 0.7f); // 中心亮绿色 + Vector4f outerCol = new Vector4f(0.0f, 0.5f, 0.1f, 0.3f); // 外部深绿色 + Vector4f outlineCol = new Vector4f(1.0f, 1.0f, 1.0f, 0.9f); // 白色轮廓 + + // 绘制顶点视觉效果 + drawCircleGradient(bb, p.x, p.y, DEFORMATION_VERTEX_SIZE, centerCol, outerCol, 16); + drawCircleOutline(bb, p.x, p.y, DEFORMATION_VERTEX_SIZE, outlineCol, 16); + drawCircleOutline(bb, p.x, p.y, DEFORMATION_VERTEX_SIZE * 0.5f, new Vector4f(1.0f, 1.0f, 1.0f, 0.5f), 12); } - - // 2) 收集被选中的点(以及可能的 preview),并构建一个**按极角排序的简单多边形** - java.util.List selPts = new java.util.ArrayList<>(); - for (SecondaryVertex sv : verts) if (sv.isSelected()) selPts.add(sv.getPosition()); - Vector2f preview = mesh2D.getPreviewPoint(); - if ((selPts.isEmpty() || selPts.size() < 3) && preview != null) { - // 当选中点少于3时,把 preview 当作临时点 - selPts.add(preview); - } - - if (!selPts.isEmpty()) { - // 去重 - java.util.Set seen = new java.util.HashSet<>(); - java.util.List uniq = new java.util.ArrayList<>(); - for (Vector2f v : selPts) { - String k = String.format("%.6f_%.6f", v.x, v.y); - if (!seen.contains(k)) { - seen.add(k); - uniq.add(new Vector2f(v.x, v.y)); - } - } - - // 生成一个按极角排序的多边形(相对于重心) - java.util.List poly = buildOrderedPolygon(uniq); - - // 如果自交则替换为凸包(保证无自交) - if (polygonIsSelfIntersecting(poly)) { - poly = convexHull(poly); - } - - // 需要至少3点才能绘制填充多边形 - if (poly.size() >= 3) { - drawSelectionPolygon(bb, poly); - } else if (poly.size() == 2) { - // 两点时绘制一条闭合的短线(可视化) - bb.begin(GL11.GL_LINES, 2); - bb.setColor(new Vector4f(0.95f, 0.6f, 0.15f, 0.28f)); - bb.vertex(poly.get(0).x, poly.get(0).y, 0f, 0f); - bb.vertex(poly.get(1).x, poly.get(1).y, 0f, 0f); - bb.endImmediate(); - } else if (poly.size() == 1) { - // 单点:画一个小圆做提示(在 draw loop 后会再次画点) - } - } - - // 3) 如果启用影响范围显示,显示 controlShape 的包围盒(针对被选中的顶点) - if (isAlgorithmEnabled("showSecondaryVertexInfluence")) { - for (SecondaryVertex vertex : verts) { - if (vertex.isSelected()) { - Vector2f min = vertex.getControlShape().getMinControlPoint(); - Vector2f max = vertex.getControlShape().getMaxControlPoint(); - Vector4f baseColor = mesh2D.selectedSecondaryVertexColor; - if (max.x > min.x || max.y > min.y) { - drawInfluenceBox(bb, min, max, baseColor); - } - } - } - } - - // 4) 绘制二级顶点的点样式(阴影、渐变、高光、边框、pin/lock、编号) - for (SecondaryVertex vertex : verts) { - Vector2f position = vertex.getPosition(); - Vector4f baseColor = vertex.isSelected() ? mesh2D.selectedSecondaryVertexColor : mesh2D.secondaryVertexColor; - float size = mesh2D.secondaryVertexSize; - drawCircleSolid(bb, position.x + 2f, position.y - 2f, size * 0.8f, new Vector4f(0f, 0f, 0f, 0.22f), 20); - Vector4f centerCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, Math.min(1.0f, baseColor.w + 0.15f)); - Vector4f outerCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, baseColor.w * 0.9f); - drawCircleGradient(bb, position.x, position.y, size * 0.9f, centerCol, outerCol, 28); - drawCircleSolid(bb, position.x - size * 0.12f, position.y + size * 0.12f, size * 0.22f, - new Vector4f(1f, 1f, 1f, 0.75f), 12); - drawCircleOutline(bb, position.x, position.y, size * 0.95f, new Vector4f(1f, 1f, 1f, 0.9f), 28); - drawPinLockIcon(bb, vertex, position.x, position.y, size); - if (vertex.isSelected()) drawVertexId(mesh2D, bb, vertex.getId(), position.x, position.y, size); - } - } finally { - GL11.glDisable(GL11.GL_LINE_SMOOTH); RenderSystem.popState(); } } /** - * 根据输入点(任意顺序),生成一个“简单多边形”顺序: - * - 先计算重心 - * - 按相对于重心的 atan2 排序(极角排序) - * 该方法不会尝试消除自交,调用方可以在需要时再用 polygonIsSelfIntersecting() 检查并 fallback 到 convexHull() + * [MODIFIED] 此方法现在接收一个 List + * 绘制连接可变形顶点的线。 */ - private java.util.List buildOrderedPolygon(java.util.List pts) { - java.util.List out = new ArrayList<>(); - if (pts == null || pts.isEmpty()) return out; - // 去重 - java.util.Set seen = new java.util.HashSet<>(); - java.util.List uniq = new ArrayList<>(); - for (Vector2f p : pts) { - String k = String.format("%.6f_%.6f", p.x, p.y); - if (!seen.contains(k)) { seen.add(k); uniq.add(new Vector2f(p.x, p.y)); } - } - if (uniq.size() <= 1) return new ArrayList<>(uniq); - - // 计算重心 - Vector2f cen = new Vector2f(0f, 0f); - for (Vector2f v : uniq) { cen.x += v.x; cen.y += v.y; } - cen.x /= uniq.size(); cen.y /= uniq.size(); - - // 按角度排序 - uniq.sort(Comparator.comparingDouble(a -> Math.atan2(a.y - cen.y, a.x - cen.x))); - - // 如果排序后首尾非常接近但方向不一致,尝试修正:确保多边形非自交的简单方式是返回它并由调用方处理 - return uniq; - } - - private boolean polygonIsSelfIntersecting(java.util.List poly) { - if (poly == null) return false; - int n = poly.size(); - if (n < 4) return false; - for (int i = 0; i < n; i++) { - Vector2f a1 = poly.get(i); - Vector2f a2 = poly.get((i + 1) % n); - for (int j = i + 1; j < n; j++) { - if (Math.abs(i - j) <= 1 || (i == 0 && j == n - 1)) continue; - Vector2f b1 = poly.get(j); - Vector2f b2 = poly.get((j + 1) % n); - if (segmentsIntersect(a1.x, a1.y, a2.x, a2.y, b1.x, b1.y, b2.x, b2.y)) { - return true; - } - } - } - return false; - } - - private java.util.List convexHull(java.util.List points) { - java.util.List pts = new java.util.ArrayList<>(); - if (points == null || points.isEmpty()) return pts; - - java.util.Set seen = new java.util.HashSet<>(); - for (Vector2f p : points) { - String k = String.format("%.6f_%.6f", p.x, p.y); - if (!seen.contains(k)) { - seen.add(k); - pts.add(new Vector2f(p.x, p.y)); - } - } - if (pts.size() <= 1) return new java.util.ArrayList<>(pts); - - pts.sort((a, b) -> { - int cmp = Float.compare(a.x, b.x); - if (cmp != 0) return cmp; - return Float.compare(a.y, b.y); - }); - - java.util.List lower = new java.util.ArrayList<>(); - for (Vector2f p : pts) { - while (lower.size() >= 2) { - Vector2f p1 = lower.get(lower.size() - 2); - Vector2f p2 = lower.get(lower.size() - 1); - if (orient(p1.x, p1.y, p2.x, p2.y, p.x, p.y) <= 0) { - lower.remove(lower.size() - 1); - } else break; - } - lower.add(p); - } - - java.util.List upper = new java.util.ArrayList<>(); - for (int i = pts.size() - 1; i >= 0; i--) { - Vector2f p = pts.get(i); - while (upper.size() >= 2) { - Vector2f p1 = upper.get(upper.size() - 2); - Vector2f p2 = upper.get(upper.size() - 1); - if (orient(p1.x, p1.y, p2.x, p2.y, p.x, p.y) <= 0) { - upper.remove(upper.size() - 1); - } else break; - } - upper.add(p); - } - - lower.remove(lower.size() - 1); - upper.remove(upper.size() - 1); - lower.addAll(upper); - return lower; - } - - private boolean segmentsIntersect(double x1, double y1, double x2, double y2, - double x3, double y3, double x4, double y4) { - if (Math.max(x1, x2) < Math.min(x3, x4) || Math.max(x3, x4) < Math.min(x1, x2) || - Math.max(y1, y2) < Math.min(y3, y4) || Math.max(y3, y4) < Math.min(y1, y2)) { - return false; - } - double d1 = orient(x3, y3, x4, y4, x1, y1); - double d2 = orient(x3, y3, x4, y4, x2, y2); - double d3 = orient(x1, y1, x2, y2, x3, y3); - double d4 = orient(x1, y1, x2, y2, x4, y4); - - if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && - ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) { - return true; - } - - if (Math.abs(d1) < 1e-10 && onSegment(x3, y3, x4, y4, x1, y1)) return true; - if (Math.abs(d2) < 1e-10 && onSegment(x3, y3, x4, y4, x2, y2)) return true; - if (Math.abs(d3) < 1e-10 && onSegment(x1, y1, x2, y2, x3, y3)) return true; - if (Math.abs(d4) < 1e-10 && onSegment(x1, y1, x2, y2, x4, y4)) return true; - - return false; - } - - private double orient(double ax, double ay, double bx, double by, double cx, double cy) { - return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax); - } - - private boolean onSegment(double ax, double ay, double bx, double by, double px, double py) { - double minx = Math.min(ax, bx) - 1e-8; - double maxx = Math.max(ax, bx) + 1e-8; - double miny = Math.min(ay, by) - 1e-8; - double maxy = Math.max(ay, by) + 1e-8; - return px >= minx && px <= maxx && py >= miny && py <= maxy && - Math.abs(orient(ax, ay, bx, by, px, py)) < 1e-8; - } - - - private void drawSelectionPolygon(BufferBuilder bb, java.util.List poly) { - if (poly == null || poly.size() < 3) return; - - Vector4f fillCol = new Vector4f(0.95f, 0.6f, 0.15f, 0.06f); - Vector4f edgeCol = new Vector4f(0.95f, 0.6f, 0.15f, 0.28f); - - // 填充(使用 TRIANGLE_FAN) - bb.begin(GL11.GL_TRIANGLE_FAN, poly.size() + 2); - bb.setColor(fillCol); - float cx = 0f, cy = 0f; - for (Vector2f v : poly) { cx += v.x; cy += v.y; } - cx /= poly.size(); cy /= poly.size(); - bb.vertex(cx, cy, 0f, 0f); - for (Vector2f v : poly) { - bb.setColor(fillCol); - bb.vertex(v.x, v.y, 0f, 0f); - } - // 闭合回首点 - Vector2f first = poly.get(0); - bb.setColor(fillCol); - bb.vertex(first.x, first.y, 0f, 0f); - bb.endImmediate(); - - // 额外绘制多个渐变环用于视觉加强(可选) - int rings = 3; - float ringStep = 8f; - for (int r = 1; r <= rings; r++) { - float out = r * ringStep; - java.util.List expanded = new java.util.ArrayList<>(); - int n = poly.size(); - for (int i = 0; i < n; i++) { - Vector2f prev = poly.get((i - 1 + n) % n); - Vector2f curr = poly.get(i); - Vector2f next = poly.get((i + 1) % n); - float ex = next.x - prev.x; - float ey = next.y - prev.y; - float nx = -ey; - float ny = ex; - float len = (float)Math.sqrt(nx*nx + ny*ny) + 1e-9f; - nx /= len; ny /= len; - expanded.add(new Vector2f(curr.x + nx * out, curr.y + ny * out)); - } - float alpha = 0.06f * (1f - (float)r / (rings + 1)); - Vector4f ringCol = new Vector4f(fillCol.x, fillCol.y, fillCol.z, alpha); - bb.begin(GL11.GL_TRIANGLE_FAN, expanded.size() + 2); - bb.setColor(ringCol); - bb.vertex(cx, cy, 0f, 0f); - for (Vector2f v : expanded) { - bb.setColor(ringCol); - bb.vertex(v.x, v.y, 0f, 0f); - } - bb.setColor(ringCol); - bb.vertex(expanded.get(0).x, expanded.get(0).y, 0f, 0f); - bb.endImmediate(); - } - - // 边框(使用 LINE_LOOP 保证闭合) - bb.begin(GL11.GL_LINE_LOOP, poly.size()); - bb.setColor(edgeCol); - for (Vector2f v : poly) { - bb.vertex(v.x, v.y, 0f, 0f); - } - bb.endImmediate(); - } - - private void drawPredictedOutline(BufferBuilder bb, float[] predictedVertices, Mesh2D mesh2D) { - if (predictedVertices == null || predictedVertices.length < 4) return; - - bb.begin(GL11.GL_LINE_LOOP, predictedVertices.length / 2); - bb.setColor(new Vector4f(0.95f, 0.6f, 0.15f, 0.28f)); - for (int i = 0; i < predictedVertices.length; i += 2) { - bb.vertex(predictedVertices[i], predictedVertices[i + 1], 0f, 0f); - } - bb.endImmediate(); - - int step = Math.max(1, (predictedVertices.length / 2) / 40); - for (int i = 0; i < predictedVertices.length; i += 2 * step) { - int j = (i + 2 * step) % predictedVertices.length; - bb.begin(GL11.GL_LINES, 2); - bb.setColor(new Vector4f(0.95f, 0.6f, 0.15f, 0.12f)); - bb.vertex(predictedVertices[i], predictedVertices[i + 1], 0f, 0f); - bb.vertex(predictedVertices[j], predictedVertices[j + 1], 0f, 0f); - bb.endImmediate(); - } - - float psize = mesh2D.secondaryVertexSize * 0.6f; - for (int i = 0; i < predictedVertices.length; i += 2) { - drawCircleSolid(bb, predictedVertices[i], predictedVertices[i + 1], psize, new Vector4f(0.95f, 0.6f, 0.15f, 0.9f), 10); - drawCircleOutline(bb, predictedVertices[i], predictedVertices[i + 1], psize * 0.9f, new Vector4f(1f,1f,1f,0.85f), 10); - } - } - - private void drawConnectionLines(BufferBuilder bb, Mesh2D mesh2D) { - java.util.List verts = mesh2D.getSecondaryVertices(); + private void drawConnectionLines(BufferBuilder bb, List verts) { if (verts.size() < 2) return; - GL11.glLineWidth(1.0f); - bb.begin(GL11.GL_LINE_STRIP, verts.size()); - bb.setColor(new Vector4f(1f, 1f, 1f, 0.12f)); - for (SecondaryVertex v : verts) { - Vector2f p = v.getPosition(); + // 为了美观,我们对顶点进行排序以绘制一个简单的多边形轮廓 + List positions = verts.stream().map(v -> v.position).collect(Collectors.toList()); + List orderedPositions = buildOrderedPolygon(positions); + + if (orderedPositions.size() < 2) return; + + GL11.glLineWidth(1.5f); + bb.begin(GL11.GL_LINE_LOOP, orderedPositions.size()); + bb.setColor(new Vector4f(1f, 1f, 1f, 0.25f)); + for (Vector2f p : orderedPositions) { bb.vertex(p.x, p.y, 0f, 0f); } bb.endImmediate(); + GL11.glLineWidth(1.0f); - for (int i = 0; i < verts.size(); i++) { - SecondaryVertex a = verts.get(i); - SecondaryVertex b = verts.get((i + 1) % verts.size()); - drawDashedLine(bb, a.getPosition().x, a.getPosition().y, b.getPosition().x, b.getPosition().y, 6, 3, new Vector4f(1f,1f,1f,0.06f)); + for (int i = 0; i < orderedPositions.size(); i++) { + Vector2f a = orderedPositions.get(i); + Vector2f b = orderedPositions.get((i + 1) % orderedPositions.size()); + drawDashedLine(bb, a.x, a.y, b.x, b.y, 6, 4, new Vector4f(1f,1f,1f,0.1f)); } } + /** + * 根据输入点(任意顺序),生成一个“简单多边形”顺序: + * - 先计算重心 + * - 按相对于重心的 atan2 排序(极角排序) + */ + private java.util.List buildOrderedPolygon(java.util.List pts) { + if (pts == null || pts.size() <= 1) return new ArrayList<>(pts); + + // 计算重心 + Vector2f cen = new Vector2f(0f, 0f); + for (Vector2f v : pts) { cen.add(v); } + cen.div(pts.size()); + + // 创建副本并按角度排序 + List sortedPts = new ArrayList<>(pts); + sortedPts.sort(Comparator.comparingDouble(p -> Math.atan2(p.y - cen.y, p.x - cen.x))); + + return sortedPts; + } + + // --- 以下是无需修改的通用绘图和几何辅助方法 --- + private void drawDashedLine(BufferBuilder bb, float x1, float y1, float x2, float y2, float segmentLen, float gapLen, Vector4f color) { float dx = x2 - x1; float dy = y2 - y1; @@ -422,46 +165,6 @@ public class VertexDeformationRander extends RanderTools { } } - private void drawPinLockIcon(BufferBuilder bb, SecondaryVertex v, float px, float py, float size) { - float iconSize = size * 0.9f; - float ix = px + size * 0.9f; - float iy = py + size * 0.2f; - - if (v.isPinned()) { - bb.begin(GL11.GL_TRIANGLES, 6); - bb.setColor(new Vector4f(0.95f, 0.75f, 0.2f, 0.95f)); - bb.vertex(ix - iconSize*0.12f, iy - iconSize*0.3f, 0f, 0f); - bb.vertex(ix + iconSize*0.12f, iy - iconSize*0.3f, 0f, 0f); - bb.vertex(ix + iconSize*0.12f, iy + iconSize*0.15f, 0f, 0f); - - bb.vertex(ix + iconSize*0.12f, iy + iconSize*0.15f, 0f, 0f); - bb.vertex(ix - iconSize*0.12f, iy + iconSize*0.15f, 0f, 0f); - bb.vertex(ix - iconSize*0.12f, iy - iconSize*0.3f, 0f, 0f); - bb.endImmediate(); - - drawCircleSolid(bb, ix, iy + iconSize*0.25f, iconSize*0.18f, new Vector4f(1f,1f,1f,0.9f), 12); - } - - if (v.isLocked()) { - float lx = ix + iconSize * 0.6f; - float ly = iy; - float w = iconSize * 0.8f; - float h = iconSize * 0.6f; - bb.begin(GL11.GL_TRIANGLES, 6); - bb.setColor(new Vector4f(0.16f,0.16f,0.16f,0.95f)); - bb.vertex(lx - w/2, ly - h/2, 0f, 0f); - bb.vertex(lx + w/2, ly - h/2, 0f, 0f); - bb.vertex(lx + w/2, ly + h/2, 0f, 0f); - - bb.vertex(lx + w/2, ly + h/2, 0f, 0f); - bb.vertex(lx - w/2, ly + h/2, 0f, 0f); - bb.vertex(lx - w/2, ly - h/2, 0f, 0f); - bb.endImmediate(); - - drawCircleGradient(bb, lx, ly - h*0.15f, w*0.35f, new Vector4f(1f,1f,1f,0.95f), new Vector4f(1f,1f,1f,0.6f), 10); - } - } - private void drawCircleSolid(BufferBuilder bb, float cx, float cy, float radius, Vector4f color, int segments) { if (radius <= 0f) return; segments = Math.max(6, segments); @@ -508,74 +211,4 @@ public class VertexDeformationRander extends RanderTools { } bb.endImmediate(); } - - private void drawInfluenceBox(BufferBuilder bb, Vector2f min, Vector2f max, Vector4f baseColor) { - Vector4f fillCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, 0.08f); - bb.begin(GL11.GL_QUADS, 4); - bb.setColor(fillCol); - bb.vertex(min.x, min.y, 0f, 0f); - bb.vertex(max.x, min.y, 0f, 0f); - bb.vertex(max.x, max.y, 0f, 0f); - bb.vertex(min.x, max.y, 0f, 0f); - bb.endImmediate(); - - Vector4f edgeCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, 0.35f); - GL11.glLineWidth(2.0f); - bb.begin(GL11.GL_LINE_LOOP, 4); - bb.setColor(edgeCol); - bb.vertex(min.x, min.y, 0f, 0f); - bb.vertex(max.x, min.y, 0f, 0f); - bb.vertex(max.x, max.y, 0f, 0f); - bb.vertex(min.x, max.y, 0f, 0f); - bb.endImmediate(); - GL11.glLineWidth(1.0f); - } - - private void drawVertexId(Mesh2D mesh2D, BufferBuilder bb, int id, float x, float y, float size) { - String idText = String.valueOf(id); - TextRenderer textRenderer = ModelRender.getTextRenderer(); - if (textRenderer != null) { - float textWidth = textRenderer.getTextWidth(idText); - float textX = x + size + 6.0f; - float textY = y - size * 0.2f; - - float padX = 6f; - float padY = 4f; - float left = textX - padX; - float right = textX + textWidth + padX; - float top = textY - 12f - padY; - float bottom = textY + 4f + padY; - - bb.begin(GL11.GL_TRIANGLES, 6); - bb.setColor(new Vector4f(0f, 0f, 0f, 0.25f)); - float sx = 2f, sy = -2f; - bb.vertex(left + sx, top + sy, 0f, 0f); - bb.vertex(right + sx, top + sy, 0f, 0f); - bb.vertex(right + sx, bottom + sy, 0f, 0f); - bb.vertex(right + sx, bottom + sy, 0f, 0f); - bb.vertex(left + sx, bottom + sy, 0f, 0f); - bb.vertex(left + sx, top + sy, 0f, 0f); - bb.endImmediate(); - - bb.begin(GL11.GL_TRIANGLES, 6); - bb.setColor(new Vector4f(0.06f, 0.06f, 0.06f, 0.88f)); - bb.vertex(left, top, 0f, 0f); - bb.vertex(right, top, 0f, 0f); - bb.vertex(right, bottom, 0f, 0f); - bb.vertex(right, bottom, 0f, 0f); - bb.vertex(left, bottom, 0f, 0f); - bb.vertex(left, top, 0f, 0f); - bb.endImmediate(); - - bb.begin(GL11.GL_LINE_LOOP, 4); - bb.setColor(new Vector4f(1f, 1f, 1f, 0.1f)); - bb.vertex(left, top, 0f, 0f); - bb.vertex(right, top, 0f, 0f); - bb.vertex(right, bottom, 0f, 0f); - bb.vertex(left, bottom, 0f, 0f); - bb.endImmediate(); - - ModelRender.renderText(idText, textX, textY, new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); - } - } -} +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java index 5a85d12..183ee91 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderLightingTest.java @@ -4,7 +4,7 @@ import com.chuangzhou.vivid2D.render.ModelRender; import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.LightSource; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.Texture; import com.chuangzhou.vivid2D.render.systems.RenderSystem; import org.joml.Vector2f; diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java index e5798c0..855e9e4 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest.java @@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.test; import com.chuangzhou.vivid2D.render.ModelRender; 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.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; import com.chuangzhou.vivid2D.render.model.util.Texture; import com.chuangzhou.vivid2D.render.systems.RenderSystem; diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest2.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest2.java index b37f1ce..7672c8e 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest2.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest2.java @@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.test; import com.chuangzhou.vivid2D.render.ModelRender; 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.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.Texture; import com.chuangzhou.vivid2D.render.systems.RenderSystem; import org.lwjgl.glfw.GLFW; diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTextureTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTextureTest.java index f4c6ae0..306101b 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTextureTest.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTextureTest.java @@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.test; import com.chuangzhou.vivid2D.render.ModelRender; 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.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.Texture; import org.lwjgl.glfw.GLFWVidMode; import org.lwjgl.opengl.GL; diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java index 909210a..1cf879a 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest.java @@ -1,6 +1,7 @@ package com.chuangzhou.vivid2D.test; import com.chuangzhou.vivid2D.render.model.AnimationParameter; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.transform.WaveDeformer; diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java index de1539d..ecc7ce0 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelTest2.java @@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.test; import com.chuangzhou.vivid2D.render.ModelRender; 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.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; import com.chuangzhou.vivid2D.render.model.util.Texture; import com.chuangzhou.vivid2D.render.systems.RenderSystem; diff --git a/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java b/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java index 5885187..ddff295 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/TestModelGLPanel.java @@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.test; import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; 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.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem; import com.chuangzhou.vivid2D.render.model.util.Texture; import org.joml.Vector2f; diff --git a/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java b/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java index 5843583..b951fbd 100644 --- a/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java +++ b/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java @@ -8,10 +8,8 @@ import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData; import com.chuangzhou.vivid2D.render.awt.tools.VertexDeformationTool; 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.SecondaryVertex; +import com.chuangzhou.vivid2D.render.model.Mesh2D; import com.chuangzhou.vivid2D.util.ManagementDataToJsonConverter; -import com.chuangzhou.vivid2D.util.ModelDataJsonConverter; import jnafilechooser.api.JnaFileChooser; import org.jetbrains.annotations.NotNull; @@ -38,8 +36,6 @@ public class MainWindow extends JFrame { private final ParametersPanel parametersPanel; private final ModelPartInfoPanel partInfoPanel; - private final SecondaryVertexPanel secondaryVertexPanel; - private final KeyBindingManager keyBindingManager; public String currentModelPath = null; private JLabel statusBarLabel; @@ -61,7 +57,6 @@ public class MainWindow extends JFrame { this.transformPanel = new TransformPanel(renderPanel); this.parametersPanel = new ParametersPanel(renderPanel); this.partInfoPanel = new ModelPartInfoPanel(renderPanel); - this.secondaryVertexPanel = new SecondaryVertexPanel(); createMenuBar(); createToolBar(); @@ -188,14 +183,10 @@ public class MainWindow extends JFrame { paramScroll.setBorder(BorderFactory.createTitledBorder("参数管理")); paramScroll.setPreferredSize(new Dimension(300, 200)); - // 【修改布局】将 partInfoPanel 和 secondaryVertexPanel 放在一个垂直分割面板中 - secondaryVertexPanel.setBorder(BorderFactory.createTitledBorder("顶点信息")); - secondaryVertexPanel.setPreferredSize(new Dimension(300, 200)); - JSplitPane infoSplit = new JSplitPane( JSplitPane.VERTICAL_SPLIT, partInfoPanel, - secondaryVertexPanel // 加入二级顶点面板 + paramScroll ); infoSplit.setResizeWeight(0.5); infoSplit.setOneTouchExpandable(true); @@ -348,31 +339,7 @@ public class MainWindow extends JFrame { }); VertexDeformationTool vertexDeformationTool = (VertexDeformationTool) renderPanel.getToolManagement().getTool("顶点变形工具"); - - vertexDeformationTool.addChangeListener((part, mesh, vertex, type) -> { - // 确保在 AWT 事件分发线程 (EDT) 更新 UI - SwingUtilities.invokeLater(() -> { - if (vertex != null) { - secondaryVertexPanel.setSecondaryVertex(vertex); - } - if (type == VertexDeformationTool.ChangeType.DELETE) { - SecondaryVertex newSelected = vertexDeformationTool.getSelectedVertex(); - secondaryVertexPanel.setSecondaryVertex(newSelected); - } - if (type == VertexDeformationTool.ChangeType.CREATE || - type == VertexDeformationTool.ChangeType.MOVE || - type == VertexDeformationTool.ChangeType.DELETE) { - setModelModified(true); - } - }); - }); renderPanel.getToolManagement().addToolChangeListener(newTool -> SwingUtilities.invokeLater(() -> { - if (newTool instanceof VertexDeformationTool) { - SecondaryVertex selectedVertex = ((VertexDeformationTool) newTool).getSelectedVertex(); - secondaryVertexPanel.setSecondaryVertex(selectedVertex); - } else { - secondaryVertexPanel.setSecondaryVertex(null); - } })); } @@ -384,7 +351,6 @@ public class MainWindow extends JFrame { transformPanel.setEnabled(enabled); parametersPanel.setEnabled(enabled); partInfoPanel.setEnabled(enabled); - secondaryVertexPanel.setEnabled(enabled); renderPanel.setEnabled(enabled); for (Component comp : menuBar.getComponents()) { if (comp instanceof JMenu menu) {