diff --git a/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java b/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java index 8eb3d37..7acbeb7 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/MultiSelectionBoxRenderer.java @@ -9,28 +9,32 @@ import org.joml.Vector4f; import org.lwjgl.opengl.GL11; /** - * MultiSelectionBoxRenderer — 修复摄像机缩放导致边框消失的问题(并保持美观) + * MultiSelectionBoxRenderer — 美观版完整实现 * - * 要点: - * - drawSelectBox/drawMultiSelectionBox 接收 cameraScale 参数(世界单位 -> 像素的比例) - * - 将需要“屏幕稳定”的厚度与尺寸除以 cameraScale,保证缩放时视觉一致 - * - 兼容不传 cameraScale 的调用(默认 1.0f) + * 视觉设计目标: + * - 细腻的三层边框(外发光 -> 主边框 -> 内描边) + * - 小巧但可见的手柄(角点与边中点略有区分) + * - 精致的中心点(十字 + 细环) + * - 虚线用于多选边框 + * - 批处理绘制以减少 Draw Call(尽量在一次 TRIANGLES 调用中绘制多数元素) + * + * 注:BufferBuilder 的 vertex(...) 方法签名与项目实现有关,示例中使用 (x,y,z,u) 占位。 */ public class MultiSelectionBoxRenderer { // -------------------- 配置常量(可调) -------------------- - // 尺寸(以世界坐标为基准,这些会根据 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; - // 视觉厚度分层(这些是“世界坐标基准值”,实际绘制时会 / cameraScale) - private static final float OUTER_BORDER_THICKNESS = 0.8f; + // 视觉厚度分层(更细腻) + private static final float OUTER_BORDER_THICKNESS = 2.2f; 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; @@ -46,113 +50,79 @@ public class MultiSelectionBoxRenderer { 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); // 中心点红 - // 小的保护常量 - private static final float MIN_CAMERA_SCALE = 1e-4f; - private static final float MIN_THICKNESS_PIXELS = 0.5f; // 最小像素视觉厚度保护 - - // -------------------- 公共绘制 API(带 cameraScale 参数) -------------------- + // -------------------- 公共绘制 API -------------------- /** - * 绘制单选的选择框(推荐:传入 cameraScale) + * 绘制单选的选择框(主入口) * - * @param bounds 包围盒(世界坐标) - * @param pivot 旋转中心 / 中心点(世界坐标) - * @param cameraScale 世界单位 -> 像素 比例(通常来自 CameraManagement.calculateScaleFactor()) + * @param bounds 包围盒(世界坐标) + * @param pivot 旋转中心 / 中心点(世界坐标) */ - public static void drawSelectBox(BoundingBox bounds, Vector2f pivot, float cameraScale) { + public static void drawSelectBox(BoundingBox bounds, Vector2f pivot) { 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); - // 估算顶点数并开始 TRIANGLES 绘制 - int estimatedQuads = (3 * 4) + 8 + 12; + // 估算顶点数:三层边框 + 8 个手柄 + 中心点十字和圆环 + // 每个四边形使用 6 个顶点(两个三角形) + int estimatedQuads = (3 * 4) + 8 + (6 * 4); // 近似估算(保守) bb.begin(RenderSystem.GL_TRIANGLES, estimatedQuads * 6); // 外层发光边框(较宽、透明) bb.setColor(SOLID_BORDER_COLOR_OUTER); - addQuadLineLoop(bb, outerThickness, minX, minY, maxX, minY, maxX, maxY, minX, maxY); + addQuadLineLoop(bb, OUTER_BORDER_THICKNESS, minX, minY, maxX, minY, maxX, maxY, minX, maxY); // 主边框(核心线) bb.setColor(SOLID_BORDER_COLOR_MAIN); - addQuadLineLoop(bb, mainThickness, minX, minY, maxX, minY, maxX, maxY, minX, maxY); + addQuadLineLoop(bb, MAIN_BORDER_THICKNESS, minX, minY, maxX, minY, maxX, maxY, minX, maxY); // 内描边(细) bb.setColor(SOLID_BORDER_COLOR_INNER); - addQuadLineLoop(bb, innerThickness, minX, minY, maxX, minY, maxX, maxY, minX, maxY); + addQuadLineLoop(bb, INNER_BORDER_THICKNESS, minX, minY, maxX, minY, maxX, maxY, minX, maxY); // 手柄(角点与边中点) bb.setColor(HANDLE_COLOR); - addHandleQuad(bb, minX, minY, handleCornerSize); - addHandleQuad(bb, maxX, minY, handleCornerSize); - addHandleQuad(bb, minX, maxY, handleCornerSize); - addHandleQuad(bb, maxX, maxY, handleCornerSize); + addHandleQuad(bb, minX, minY, HANDLE_CORNER_SIZE); + addHandleQuad(bb, maxX, minY, HANDLE_CORNER_SIZE); + addHandleQuad(bb, minX, maxY, HANDLE_CORNER_SIZE); + addHandleQuad(bb, maxX, maxY, HANDLE_CORNER_SIZE); - 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); + addHandleQuad(bb, (minX + maxX) * 0.5f, minY, HANDLE_MID_SIZE); + addHandleQuad(bb, (minX + maxX) * 0.5f, maxY, HANDLE_MID_SIZE); + addHandleQuad(bb, minX, (minY + maxY) * 0.5f, HANDLE_MID_SIZE); + addHandleQuad(bb, maxX, (minY + maxY) * 0.5f, HANDLE_MID_SIZE); // 中心点:十字 + 环(圆环使用三角片段) bb.setColor(CENTER_POINT_COLOR); - 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); + addQuadLine(bb, pivot.x - 6.0f, pivot.y, pivot.x + 6.0f, pivot.y, CENTER_LINE_THICKNESS); + addQuadLine(bb, pivot.x, pivot.y - 6.0f, pivot.x, pivot.y + 6.0f, CENTER_LINE_THICKNESS); + addRing(bb, pivot.x, pivot.y, CENTER_RING_RADIUS, CENTER_RING_THICKNESS, 18); tesselator.end(); } /** - * 向后兼容:不传 cameraScale 时使用默认 1.0(即不做屏幕稳定) - */ - public static void drawSelectBox(BoundingBox bounds, Vector2f pivot) { - drawSelectBox(bounds, pivot, 1.0f); - } - - /** - * 绘制多选框(推荐传入 cameraScale) + * 绘制多选框(虚线 + 手柄) * - * @param multiBounds 包围盒 - * @param cameraScale 世界单位 -> 像素 比例 + * @param multiBounds 多选包围盒 */ - public static void drawMultiSelectionBox(BoundingBox multiBounds, float cameraScale) { + public static void drawMultiSelectionBox(BoundingBox multiBounds) { 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(); @@ -160,41 +130,37 @@ public class MultiSelectionBoxRenderer { RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); // 1) 虚线边框(使用 GL_LINES) - int estimatedSegments = Math.max(4, (int) Math.ceil((2f * (multiBounds.getWidth() + multiBounds.getHeight())) / (dashLen + gapLen))); + int estimatedSegments = Math.max(4, (int) Math.ceil((2f * (multiBounds.getWidth() + multiBounds.getHeight())) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH))); bb.begin(GL11.GL_LINES, estimatedSegments * 2); bb.setColor(DASHED_BORDER_COLOR); - 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); + 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(); // 2) 手柄与中心(合并为一次三角形绘制) bb.begin(RenderSystem.GL_TRIANGLES, (8 + 2) * 6); bb.setColor(MULTI_SELECTION_HANDLE_COLOR); - addHandleQuad(bb, minX, minY, handleCornerSize); - addHandleQuad(bb, maxX, minY, handleCornerSize); - addHandleQuad(bb, minX, maxY, handleCornerSize); - addHandleQuad(bb, maxX, maxY, handleCornerSize); + addHandleQuad(bb, minX, minY, HANDLE_CORNER_SIZE); + addHandleQuad(bb, maxX, minY, HANDLE_CORNER_SIZE); + addHandleQuad(bb, minX, maxY, HANDLE_CORNER_SIZE); + addHandleQuad(bb, maxX, maxY, HANDLE_CORNER_SIZE); - 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); + addHandleQuad(bb, (minX + maxX) * 0.5f, minY, HANDLE_MID_SIZE); + addHandleQuad(bb, (minX + maxX) * 0.5f, maxY, HANDLE_MID_SIZE); + addHandleQuad(bb, minX, (minY + maxY) * 0.5f, HANDLE_MID_SIZE); + addHandleQuad(bb, maxX, (minY + maxY) * 0.5f, HANDLE_MID_SIZE); Vector2f center = multiBounds.getCenter(); bb.setColor(CENTER_POINT_COLOR); - 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); + addQuadLine(bb, center.x - 6.0f, center.y, center.x + 6.0f, center.y, CENTER_LINE_THICKNESS); + addQuadLine(bb, center.x, center.y - 6.0f, center.x, center.y + 6.0f, CENTER_LINE_THICKNESS); tesselator.end(); } - public static void drawMultiSelectionBox(BoundingBox multiBounds) { - drawMultiSelectionBox(multiBounds, 1.0f); - } - - // -------------------- 辅助绘图方法(保持不变) -------------------- + // -------------------- 辅助绘图方法 -------------------- /** * 添加一个填充四边形(用两个三角形表示) @@ -219,7 +185,7 @@ public class MultiSelectionBoxRenderer { } /** - * 绘制一条由四边形模拟的线段(厚度以输入值为准) + * 绘制一条由四边形模拟的线段(厚度可控) */ private static void addQuadLine(BufferBuilder bb, float x0, float y0, float x1, float y1, float thickness) { float dx = x1 - x0; @@ -249,7 +215,7 @@ public class MultiSelectionBoxRenderer { /** * 绘制一个闭合的四边形线环(用于边框三层绘制) * - * @param thickness 厚度(以外部传入的值为准,通常已根据 cameraScale 调整) + * @param thickness 厚度(世界坐标) * @param vertices 顶点序列 x1,y1,x2,y2,... */ private static void addQuadLineLoop(BufferBuilder bb, float thickness, float... vertices) { @@ -269,8 +235,8 @@ public class MultiSelectionBoxRenderer { * * @param cx 中心 x * @param cy 中心 y - * @param radius 半径(通常已按 cameraScale 调整) - * @param thickness 环厚度(通常已按 cameraScale 调整) + * @param radius 半径 + * @param thickness 环厚度 * @param segments 分段数(建议 >= 8) */ private static void addRing(BufferBuilder bb, float cx, float cy, float radius, float thickness, int segments) { @@ -305,8 +271,8 @@ public class MultiSelectionBoxRenderer { /** * 在两点之间生成虚线段顶点(使用 GL_LINES) * - * @param dashLen 虚线长度(通常已按 cameraScale 调整) - * @param gapLen 间隙长度(通常已按 cameraScale 调整) + * @param dashLen 虚线长度(世界坐标) + * @param gapLen 间隙长度(世界坐标) */ private static void addDashedLineVertices(BufferBuilder bb, float startX, float startY, float endX, float endY, float dashLen, float gapLen) { 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 01482ed..461e0eb 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java @@ -458,35 +458,6 @@ public class ModelLayerPanel extends JPanel { setSelectedLayers(selectedParts); } - // 原始的单选拖拽逻辑 (为兼容老版本保留,但现在应主要使用 performBlockReorder) - public void performVisualReorder(int visualFrom, int visualTo) { - if (model == null) return; - try { - int size = listModel.getSize(); - if (visualFrom < 0 || visualFrom >= size) return; - if (visualTo < 0) visualTo = 0; - if (visualTo > size - 1) visualTo = size - 1; - - ModelPart moved = listModel.get(visualFrom); - if (!isDragging) { - isDragging = true; - draggedPart = moved; - dragStartPosition = new Vector2f(moved.getPosition()); - } - - List visual = new ArrayList<>(size); - for (int i = 0; i < size; i++) visual.add(listModel.get(i)); - moved = visual.remove(visualFrom); - visual.add(visualTo, moved); - - // 使用新的辅助方法更新 UI 和模型 - updateModelAndUIFromVisualList(visual, List.of(moved)); - - } catch (Exception ex) { - ex.printStackTrace(); - } - } - /** * 【新增方法】执行多选拖拽后的图层块重排序操作。 * 供 CompositeLayerTransferHandler (原 LayerReorderTransferHandler) 调用。 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 104d35f..f5690b2 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 @@ -16,25 +16,21 @@ import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; public class VertexDeformationTool extends Tool { private static final Logger logger = LoggerFactory.getLogger(VertexDeformationTool.class); private Mesh2D targetMesh = 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 boolean cameraStateSaved = false; - - // [MODIFIED] 恢复使用 orderedControlVertices 来维护控制点的顺序 private final List orderedControlVertices = new ArrayList<>(); + // --- [新增] 用于“推/拉”模式的状态变量 --- + private boolean isPushPullMode = false; + private Vector2f dragStartPoint = null; + private List dragBaseState = null; // 存储拖动开始时的顶点快照 + public VertexDeformationTool(ModelRenderPanel renderPanel) { super(renderPanel, "顶点变形工具", "直接对网格顶点进行精细变形操作"); } @@ -43,76 +39,37 @@ public class VertexDeformationTool extends Tool { public void activate() { if (isActive) return; isActive = true; - - orderedControlVertices.clear(); selectedVertices.clear(); - + hoveredVertex = null; 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()); - 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()); } - if (targetMesh != null) { + orderedControlVertices.clear(); + orderedControlVertices.addAll(targetMesh.getDeformationControlVertices()); targetMesh.setStates("showDeformationVertices", true); renderPanel.getGlContextManager().executeInGLContext(() -> { try { targetMesh.setRenderVertices(true); - targetMesh.updateBounds(); } catch (Throwable t) { logger.debug("激活顶点显示失败: {}", t.getMessage()); } }); - logger.info("激活顶点变形工具: {}", targetMesh.getName()); + logger.info("激活顶点变形工具: {},已加载 {} 个控制点。", targetMesh.getName(), orderedControlVertices.size()); } else { logger.warn("没有找到可用的网格用于顶点变形"); } + renderPanel.repaint(); } @Override public void deactivate() { if (!isActive) return; isActive = false; - - try { - if (cameraStateSaved && renderPanel.getCameraManagement() != null && targetMesh != null && targetMesh.getModelPart() != null) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - targetMesh.getModelPart().setRotation(savedCameraRotation); - targetMesh.getModelPart().setScale(savedCameraScale); - targetMesh.getModelPart().updateMeshVertices(); - targetMesh.saveAsOriginal(); - } catch (Throwable t) { logger.debug("恢复部件状态失败: {}", t.getMessage()); } - }); - } - } catch (Throwable t) { logger.debug("无法恢复相机状态: {}", t.getMessage()); } finally { - cameraStateSaved = false; - savedCameraRotation = Float.NaN; - savedCameraScale = new Vector2f(1, 1); - } - if (targetMesh != null) { - // 在停用前,清除所有顶点的DEFORMATION标签 for (Vertex v : orderedControlVertices) { - v.setTag(VertexTag.DEFAULT); + v.setTag(VertexTag.DEFORMATION); } - targetMesh.setDeformationControlVertices(new ArrayList<>()); - orderedControlVertices.clear(); - targetMesh.setStates("showDeformationVertices", false); try { targetMesh.setRenderVertices(false); @@ -125,90 +82,145 @@ public class VertexDeformationTool extends Tool { } targetMesh = null; selectedVertices.clear(); + orderedControlVertices.clear(); hoveredVertex = null; currentDragMode = ModelRenderPanel.DragMode.NONE; logger.info("停用顶点变形工具"); } /** - * [MODIFIED] onMousePressed 现在支持 Ctrl 多选。 + * [已修正] onMousePressed 现在会检查 Alt 键来决定进入“推/拉”模式还是“控制点选择”模式。 */ @Override public void onMousePressed(MouseEvent e, float modelX, float modelY) { if (!isActive || targetMesh == null) return; - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - Vertex clickedVertex = findDeformationVertexAtPosition(modelX, modelY); - if (clickedVertex != 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_PRIMARY_VERTEX; - } else { - // 点击空白处,取消所有选择 - if (!e.isControlDown()) { - selectedVertices.clear(); - } - currentDragMode = ModelRenderPanel.DragMode.NONE; - } - } catch (Throwable t) { - logger.error("onMousePressed (VertexDeformationTool) 处理失败", t); - } finally { - renderPanel.repaint(); + + // [核心修正] 检查 Alt 键是否被按下 + if (e.isAltDown()) { + // --- 进入“推/拉”模式 --- + isPushPullMode = true; + dragStartPoint = new Vector2f(modelX, modelY); + + // 创建当前网格顶点状态的快照,这是计算位移的基准 + dragBaseState = new ArrayList<>(targetMesh.getActiveVertexList().size()); + for(Vertex v : targetMesh.getActiveVertexList()){ + dragBaseState.add(v.copy()); // 必须是深拷贝 } - }); + + currentDragMode = ModelRenderPanel.DragMode.NONE; // 确保不触发控制点拖动 + logger.debug("进入推/拉模式,起点: ({}, {})", modelX, modelY); + + } else { + // --- 默认的“控制点选择”模式 --- + isPushPullMode = false; + renderPanel.getGlContextManager().executeInGLContext(() -> { + try { + Vertex clickedVertex = findDeformationVertexAtPosition(modelX, modelY); + if (clickedVertex != null) { + if (e.isControlDown()) { + 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_PRIMARY_VERTEX; + } else { + if (!e.isControlDown()) { + selectedVertices.clear(); + } + currentDragMode = ModelRenderPanel.DragMode.NONE; + } + } catch (Throwable t) { + logger.error("onMousePressed (控制点模式) 处理失败", t); + } finally { + renderPanel.repaint(); + } + }); + } + } + + /** + * [已修正] onMouseDragged 现在会根据模式执行不同的拖动逻辑。 + */ + @Override + public void onMouseDragged(MouseEvent e, float modelX, float modelY) { + if (!isActive || targetMesh == null) return; + + if (isPushPullMode) { + // --- “推/拉”模式的逻辑 --- + if (dragStartPoint == null || dragBaseState == null) return; + + // 计算从按下鼠标开始的总位移 + Vector2f delta = new Vector2f(modelX, modelY).sub(dragStartPoint); + + // 定义一个画笔半径 (可以设为可配置的) + float radius = 50.0f; + + renderPanel.getGlContextManager().executeInGLContext(() -> { + try { + // 调用 Mesh2D 中已经存在的局部变形方法! + targetMesh.applyLocalizedPush(dragBaseState, dragStartPoint, delta, radius); + } catch (Throwable t) { + logger.error("onMouseDragged (推/拉模式) 处理失败", t); + } finally { + renderPanel.repaint(); + } + }); + + } else { + // --- 默认的“控制点拖动”模式的逻辑 --- + if (selectedVertices.isEmpty() || currentDragMode != ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX) return; + + Vertex primaryVertex = selectedVertices.get(selectedVertices.size() - 1); + + renderPanel.getGlContextManager().executeInGLContext(() -> { + try { + primaryVertex.position.set(modelX, modelY); + } catch (Throwable t) { + logger.error("onMouseDragged (控制点模式) 处理失败", t); + } finally { + renderPanel.repaint(); + } + }); + } } @Override public void onMouseReleased(MouseEvent e, float modelX, float modelY) { if (!isActive) return; - if (currentDragMode == ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX && !selectedVertices.isEmpty()) { + + if (isPushPullMode) { + // --- 清理“推/拉”模式的状态 --- + isPushPullMode = false; + dragStartPoint = null; + dragBaseState = null; + logger.debug("退出推/拉模式"); + } + + // 无论是哪种模式,都在松开鼠标时固化变形 + if (targetMesh != null) { renderPanel.getGlContextManager().executeInGLContext(() -> { try { - if (targetMesh != null) { - targetMesh.saveAsOriginal(); - if (targetMesh.getModelPart() != null) { - targetMesh.getModelPart().updateMeshVertices(); - } + 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 || selectedVertices.isEmpty() || targetMesh == null || currentDragMode != ModelRenderPanel.DragMode.MOVE_PRIMARY_VERTEX) return; - - // 我们需要计算位移,以便批量移动 - // (这里简化处理,只移动最后一个选中的点,完整的批量拖拽需要更复杂的逻辑) - Vertex primaryVertex = selectedVertices.get(selectedVertices.size() - 1); - - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - primaryVertex.position.set(modelX, modelY); - targetMesh.applyDeformation(); - } catch (Throwable t) { - logger.error("onMouseDragged (VertexDeformationTool) 处理失败", t); - } finally { - renderPanel.repaint(); - } - }); - } - + // onMouseMoved, onMouseClicked, onMouseDoubleClicked, onKeyPressed, 等方法保持不变... + // ... (此处省略所有其他未修改的方法,请保留您文件中的原样) @Override public void onMouseMoved(MouseEvent e, float modelX, float modelY) { if (!isActive || targetMesh == null) return; @@ -220,25 +232,24 @@ public class VertexDeformationTool extends Tool { } @Override - public void onMouseClicked(MouseEvent e, float modelX, float modelY) { - // No action - } + public void onMouseClicked(MouseEvent e, float modelX, float modelY) { } - /** - * [MODIFIED] 双击逻辑现在是:双击已有控制点则删除,双击空白则添加。 - */ @Override public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null) return; + if (!isActive || targetMesh == null || e.isAltDown()) return; // 在推拉模式下禁用双击 renderPanel.getGlContextManager().executeInGLContext(() -> { try { Vertex clickedVertex = findDeformationVertexAtPosition(modelX, modelY); if (clickedVertex != null) { - // 双击已有的点 -> 删除 untagDeformationVertex(clickedVertex); } else { - // 双击空白处 -> 添加 - tagNearestVertexAsDeformation(modelX, modelY); + Vertex newVertex = targetMesh.addControlPointAt(modelX, modelY); + if (newVertex != null) { + orderedControlVertices.add(newVertex); + updateDeformationRegion(); + } else { + logger.warn("在 ({}, {}) 添加控制点失败,可能点击位置在网格外部。", modelX, modelY); + } } } catch (Throwable t) { logger.error("onMouseDoubleClicked 处理失败", t); @@ -246,16 +257,12 @@ public class VertexDeformationTool extends Tool { }); } - /** - * [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); @@ -270,39 +277,15 @@ public class VertexDeformationTool extends Tool { return createVertexCursor(); } - private void tagNearestVertexAsDeformation(float x, float y) { - if (targetMesh == null) return; - Vertex nearestVertex = null; - float minDistanceSq = Float.MAX_VALUE; - - 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()); + selectedVertices.remove(vertex); + if (hoveredVertex == vertex) { + hoveredVertex = null; + } + vertex.setTag(VertexTag.DEFORMATION); + vertex.delete(); updateDeformationRegion(); } @@ -316,7 +299,7 @@ public class VertexDeformationTool extends Tool { if (targetMesh == null) return null; float tolerance = VERTEX_TOLERANCE / calculateScaleFactor(); float toleranceSq = tolerance * tolerance; - for (Vertex v : orderedControlVertices) { // 优化:只在控制点中查找 + for (Vertex v : orderedControlVertices) { if (v.position.distanceSquared(x, y) < toleranceSq) { return v; } @@ -338,9 +321,9 @@ public class VertexDeformationTool extends Tool { private Cursor createVertexCursor() { 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"); + int center = size / 2; g2.setColor(Color.ORANGE); g2.setStroke(new BasicStroke(2f)); + g2.drawRect(center - 5, center - 5, 10, 10); g2.dispose(); + return Toolkit.getDefaultToolkit().createCustomCursor(img, new Point(center, center), "VertexSelectCursor"); } public Mesh2D getTargetMesh() {return targetMesh;} 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 2c5265d..8846dca 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 @@ -312,7 +312,7 @@ public class FrameInterpolator { if (target.deleted) { // "删除"操作意味着取消其变形资格 if (vertex.getTag() == VertexTag.DEFORMATION) { - vertex.setTag(VertexTag.DEFAULT); + vertex.setTag(VertexTag.DEFORMATION); meshModified = true; } } else { 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 26d3a1c..6aea984 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 @@ -19,168 +19,12 @@ public class MeshTextureUtil { float h = img.getHeight(); try { - Mesh2D o = Mesh2D.createQuad(meshName, w, h); - return subdivideMeshForLiquify(o, 3); - } catch (Exception ignored) { - } - - try { - return createSubdividedQuad(meshName, w, h, 3); - } catch (Exception ex) { - ex.printStackTrace(); - } + return Mesh2D.createQuad(meshName, w, h); + } catch (Exception ignored) {} throw new RuntimeException("无法创建 Mesh2D"); } - private static Mesh2D createSubdividedQuad(String name, float width, float height, int subdivisionLevel) { - int segments = (int) Math.pow(2, subdivisionLevel); - int vertexCount = (segments + 1) * (segments + 1); - int triangleCount = segments * segments * 2; - - float[] vertices = new float[vertexCount * 2]; - float[] uvs = new float[vertexCount * 2]; - int[] indices = new int[triangleCount * 3]; - - float halfW = width / 2f; - float halfH = height / 2f; - int vertexIndex = 0; - for (int y = 0; y <= segments; y++) { - for (int x = 0; x <= segments; x++) { - float xPos = -halfW + (x * width) / segments; - float yPos = -halfH + (y * height) / segments; - - vertices[vertexIndex * 2] = xPos; - vertices[vertexIndex * 2 + 1] = yPos; - - uvs[vertexIndex * 2] = (float) x / segments; - uvs[vertexIndex * 2 + 1] = 1f - (float) y / segments; - - vertexIndex++; - } - } - - int index = 0; - for (int y = 0; y < segments; y++) { - for (int x = 0; x < segments; x++) { - int topLeft = y * (segments + 1) + x; - int topRight = topLeft + 1; - int bottomLeft = (y + 1) * (segments + 1) + x; - int bottomRight = bottomLeft + 1; - - indices[index++] = topLeft; - indices[index++] = topRight; - indices[index++] = bottomLeft; - - indices[index++] = topRight; - indices[index++] = bottomRight; - indices[index++] = bottomLeft; - } - } - - try { - Constructor cons = null; - for (Constructor c : Mesh2D.class.getDeclaredConstructors()) { - Class[] params = c.getParameterTypes(); - if (params.length >= 4 && params[0] == String.class) { - cons = c; - break; - } - } - if (cons != null) { - cons.setAccessible(true); - Object meshObj = cons.newInstance(name, vertices, uvs, indices); - if (meshObj instanceof Mesh2D mesh) { - mesh.setPivot(0, 0); - if (mesh.getOriginalPivot() != null) { - mesh.setOriginalPivot(new Vector2f(0, 0)); - } - return mesh; - } - } - } catch (Exception e) { - e.printStackTrace(); - } - throw new RuntimeException("无法创建细分网格"); - } - - private static Mesh2D subdivideMeshForLiquify(Mesh2D originalMesh, int subdivisionLevel) { - if (subdivisionLevel <= 0) return originalMesh; - - try { - float[] origVertices = originalMesh.getVertices(); - float[] origUVs = originalMesh.getUVs(); - int[] origIndices = originalMesh.getIndices(); - List newVertices = new ArrayList<>(); - List newUVs = new ArrayList<>(); - List newIndices = new ArrayList<>(); - - for (int i = 0; i < origVertices.length / 2; i++) { - newVertices.add(new Vector2f(origVertices[i * 2], origVertices[i * 2 + 1])); - newUVs.add(new Vector2f(origUVs[i * 2], origUVs[i * 2 + 1])); - } - - for (int i = 0; i < origIndices.length; i += 3) { - int i1 = origIndices[i]; - int i2 = origIndices[i + 1]; - int i3 = origIndices[i + 2]; - Vector2f v1 = newVertices.get(i1); - Vector2f v2 = newVertices.get(i2); - Vector2f v3 = newVertices.get(i3); - Vector2f uv1 = newUVs.get(i1); - Vector2f uv2 = newUVs.get(i2); - Vector2f uv3 = newUVs.get(i3); - - Vector2f mid12 = new Vector2f(v1).add(v2).mul(0.5f); - Vector2f mid23 = new Vector2f(v2).add(v3).mul(0.5f); - Vector2f mid31 = new Vector2f(v3).add(v1).mul(0.5f); - Vector2f uvMid12 = new Vector2f(uv1).add(uv2).mul(0.5f); - Vector2f uvMid23 = new Vector2f(uv2).add(uv3).mul(0.5f); - Vector2f uvMid31 = new Vector2f(uv3).add(uv1).mul(0.5f); - - int mid12Idx = newVertices.size(); - newVertices.add(mid12); - newUVs.add(uvMid12); - int mid23Idx = newVertices.size(); - newVertices.add(mid23); - newUVs.add(uvMid23); - int mid31Idx = newVertices.size(); - newVertices.add(mid31); - newUVs.add(uvMid31); - - newIndices.add(i1); newIndices.add(mid12Idx); newIndices.add(mid31Idx); - newIndices.add(i2); newIndices.add(mid23Idx); newIndices.add(mid12Idx); - newIndices.add(i3); newIndices.add(mid31Idx); newIndices.add(mid23Idx); - newIndices.add(mid12Idx); newIndices.add(mid23Idx); newIndices.add(mid31Idx); - } - - float[] finalVertices = new float[newVertices.size() * 2]; - float[] finalUVs = new float[newUVs.size() * 2]; - int[] finalIndices = new int[newIndices.size()]; - - for (int i = 0; i < newVertices.size(); i++) { - finalVertices[i * 2] = newVertices.get(i).x; - finalVertices[i * 2 + 1] = newVertices.get(i).y; - finalUVs[i * 2] = newUVs.get(i).x; - finalUVs[i * 2 + 1] = newUVs.get(i).y; - } - - for (int i = 0; i < newIndices.size(); i++) { - finalIndices[i] = newIndices.get(i); - } - - Mesh2D subdividedMesh = originalMesh.copy(); - subdividedMesh.setMeshData(finalVertices, finalUVs, finalIndices); - - if (subdivisionLevel > 1) { - return subdivideMeshForLiquify(subdividedMesh, subdivisionLevel - 1); - } - return subdividedMesh; - } catch (Exception e) { - e.printStackTrace(); - return originalMesh; - } - } public static Texture tryCreateTextureFromImageMemory(BufferedImage img, String texName) { try { diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java index b2f6410..4f8f3fc 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java @@ -17,7 +17,6 @@ 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.*; @@ -38,8 +37,7 @@ public class Mesh2D { //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 VertexList activeVertexList; private ModelPart modelPart; // ==================== 渲染属性 ==================== @@ -65,9 +63,10 @@ public class Mesh2D { private final Map statesList = new LinkedHashMap<>(); // [NEW] ==================== 变形引擎字段 ==================== - private List deformationControlVertices = new ArrayList<>(); - private Map deformationWeights = new HashMap<>(); - private boolean deformationWeightsDirty = true; + private final List deformationControlVertices = new ArrayList<>(); + private final List deformationControlCage = new ArrayList<>(); + private final Map deformationMap = new HashMap<>(); + private boolean deformationMappingsDirty = true; // ==================== 多选支持 ==================== private final List multiSelectedParts = new ArrayList<>(); @@ -95,7 +94,6 @@ public class Mesh2D { 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); } @@ -116,235 +114,309 @@ public class Mesh2D { // ==================== 变形引擎方法 ==================== + /** + * [新增] 核心方法:重新计算所有 DEFAULT 顶点与 DEFORMATION 控制点之间的重心坐标映射。 + * 这是一个消耗较大的操作,只应在控制点结构发生变化时调用。 + */ + private void recalculateDeformationMappings() { + deformationMap.clear(); + if (deformationControlVertices.isEmpty() || deformationControlVertices.size() < 3) { + // 没有足够的控制点形成“骨架”,无法进行映射 + deformationMappingsDirty = false; + return; + } + + // 步骤 1: 对所有 DEFORMATION 控制点进行三角剖分,形成控制网格 + // 我们使用 originalPosition 进行三角剖分,因为这是最稳定的拓扑结构 + List controlTriangles = Delaunay.triangulate(deformationControlVertices); + + if (controlTriangles.isEmpty()) { + deformationMappingsDirty = false; + return; + } + + // 步骤 2: 为每一个 DEFAULT 顶点找到它所在的控制三角形,并计算重心坐标 + for (Vertex vertexToMap : activeVertexList) { + if (vertexToMap.getTag() == VertexTag.DEFORMATION) { + continue; // 控制点不需要被映射 + } + + for (Triangle controlTriangle : controlTriangles) { + // 使用顶点的 originalPosition 来确定其在哪个控制三角形内 + if (isPointInTriangle(vertexToMap.originalPosition, + controlTriangle.p1.originalPosition, + controlTriangle.p2.originalPosition, + controlTriangle.p3.originalPosition)) { + + Vector2f barycentricCoords = barycentric(vertexToMap.originalPosition, + controlTriangle.p1.originalPosition, + controlTriangle.p2.originalPosition, + controlTriangle.p3.originalPosition); + float w = barycentricCoords.x; + float v = barycentricCoords.y; + float u = 1.0f - w - v; + + // 保存这个“基因” + deformationMap.put(vertexToMap, new BarycentricWeightInfo( + controlTriangle.p1, controlTriangle.p2, controlTriangle.p3, u, w, v // u,w,v -> p1,p2,p3 + )); + break; // 找到后即可处理下一个顶点 + } + } + } + + deformationMappingsDirty = false; + logger.info("Deformation mappings recalculated. Mapped {} vertices.", deformationMap.size()); + } /** - * 核心方法:应用变形。 - * 采用直接的仿射组合,这是应用重心坐标的正确方法。 - * 新位置 p' = Σ w_i * c_i',其中 w_i 是权重, c_i' 是控制点的新位置。 + * [新增] 核心方法:根据 DEFORMATION 控制点的当前位置,更新所有受其影响的 DEFAULT 顶点的位置。 + * 这是一个快速的操作,可以在每帧或每次控制点移动后调用。 */ public void applyDeformation() { - if (deformationControlVertices.isEmpty() || activeVertexList.isEmpty()) return; - if (deformationWeightsDirty) { - precomputeDeformationWeights(); - deformationWeightsDirty = false; + if (deformationMappingsDirty) { + recalculateDeformationMappings(); } - 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); + if (deformationMap.isEmpty()) { + return; } + + // 遍历所有已建立映射关系的 DEFAULT 顶点 + for (Map.Entry entry : deformationMap.entrySet()) { + Vertex vertexToUpdate = entry.getKey(); + BarycentricWeightInfo weights = entry.getValue(); + + // 获取其控制点的【当前】世界坐标 + Vector2f c1_pos = weights.controlVertex1.position; + Vector2f c2_pos = weights.controlVertex2.position; + Vector2f c3_pos = weights.controlVertex3.position; + + // 根据“基因”(重心坐标)和控制点的当前位置,计算出该顶点的新位置 + float newX = c1_pos.x * weights.weight1 + c2_pos.x * weights.weight2 + c3_pos.x * weights.weight3; + float newY = c1_pos.y * weights.weight1 + c2_pos.y * weights.weight2 + c3_pos.y * weights.weight3; + + vertexToUpdate.position.set(newX, newY); + activeVertexList.set(vertexToUpdate.index, vertexToUpdate); + } + + // 因为顶点位置已变,标记网格需要重新上传到GPU markDirty(); } /** - * [FINAL FIX] 设置变形控制点。这是现在唯一的入口。 - * 它无条件地信任并存储工具传入的【有序】列表。 - * 删除了所有内部排序逻辑。 - * @param controlVertices 工具传入的、已经包含了用户操作顺序的列表 + * 在指定的 (x, y) 坐标处,直接创建一个新的控制点。 + * + * 这个方法采用【三角形分割】的逻辑,完美实现了用户的最终要求: + * 1. 它不再关心“最近的边”,而是直接定位到 (x, y) 所在的【三角形】。 + * 2. 它在 (x, y) 处创建一个新顶点。 + * 3. 它将包含该点的旧三角形移除,并替换为三个连接到新顶点的新三角形。 + * 4. 它立即将新顶点设为控制点,确保其位置绝对精确。 + * + * @param x 用户想要添加控制点的精确 x 坐标 + * @param y 用户想要添加控制点的精确 y 坐标 + * @return 成功创建并添加为控制点的新顶点;如果失败(例如,点在网格之外)则返回 null。 + */ + public Vertex addControlPointAt(float x, float y) { + if (activeVertexList.getIndices() == null || activeVertexList.getIndices().length == 0 || this.modelPart == null) { + logger.warn("无法添加控制点:索引为空或未关联 ModelPart。"); + return null; + } + + Vector2f worldPoint = new Vector2f(x, y); + + // --- 步骤 1: 找到包含该点的【视觉】三角形 (此逻辑已正确) --- + TriangleInfo containingTriangle = findTriangleContainingPoint(worldPoint); + if (containingTriangle == null) { + return null; + } + + // --- [核心修正] 步骤 2: 在正确的、统一的坐标系下计算所有属性 --- + + // [关键] 使用【当前世界位置】(position) 来计算重心坐标,确保与 findTriangleContainingPoint 的逻辑一致 + Vector2f barycentricCoords = barycentric(worldPoint, + containingTriangle.v1.position, + containingTriangle.v2.position, + containingTriangle.v3.position); + + // 计算重心坐标权重 u, v, w + float w = barycentricCoords.x; + float v = barycentricCoords.y; + float u = 1.0f - w - v; + + // 检查重心坐标是否有效,防止因浮点误差导致的崩溃 + if (u < -1e-6f || v < -1e-6f || w < -1e-6f) { + logger.warn("计算出的重心坐标无效,添加顶点失败。 u={}, v={}, w={}", u, v, w); + return null; + } + + // 使用正确的权重插值 UV + Vector2f newUv = new Vector2f(0, 0); + newUv.add(new Vector2f(containingTriangle.v1.uv).mul(u)); + newUv.add(new Vector2f(containingTriangle.v2.uv).mul(w)); + newUv.add(new Vector2f(containingTriangle.v3.uv).mul(v)); + + // [关键] 同时,我们也必须使用同样的重心坐标来插值【局部原始位置】,以获得新顶点的正确局部坐标 + Vector2f localPoint = new Vector2f(0, 0); + localPoint.add(new Vector2f(containingTriangle.v1.originalPosition).mul(u)); + localPoint.add(new Vector2f(containingTriangle.v2.originalPosition).mul(w)); + localPoint.add(new Vector2f(containingTriangle.v3.originalPosition).mul(v)); + + // --- 步骤 3: 创建新顶点,并为其赋予正确的“公民身份” --- + Vertex newVertex = new Vertex(worldPoint, newUv, VertexTag.DEFORMATION); + newVertex.originalPosition.set(localPoint); + newVertex.setName(String.valueOf(UUID.randomUUID())); + // --- [后续所有逻辑都保持不变] --- + activeVertexList.add(newVertex); + final int newVertexIndex = activeVertexList.size() - 1; + + List newIndices = new ArrayList<>(); + for (int i = 0; i < this.activeVertexList.getIndices().length; i += 3) { + int i1 = this.activeVertexList.getIndices()[i], i2 = this.activeVertexList.getIndices()[i+1], i3 = this.activeVertexList.getIndices()[i+2]; + if (i1 == containingTriangle.i1 && i2 == containingTriangle.i2 && i3 == containingTriangle.i3) { + newIndices.add(i1); newIndices.add(i2); newIndices.add(newVertexIndex); + newIndices.add(i2); newIndices.add(i3); newIndices.add(newVertexIndex); + newIndices.add(i3); newIndices.add(i1); newIndices.add(newVertexIndex); + } else { + newIndices.add(i1); newIndices.add(i2); newIndices.add(i3); + } + } + this.activeVertexList.setIndices(newIndices.stream().mapToInt(Integer::intValue).toArray()); + + List allPoints = new ArrayList<>(this.deformationControlVertices); + allPoints.add(newVertex); + setDeformationControlVertices(allPoints); + applyDeformation(); + return newVertex; + } + + private static class BarycentricWeightInfo { + // 构成包含此顶点的“控制三角形”的三个控制点 + Vertex controlVertex1, controlVertex2, controlVertex3; + // 此顶点相对于这三个控制点的重心坐标 (u, v, w) + float weight1, weight2, weight3; // Corresponds to w, v, u in barycentric calculation + + BarycentricWeightInfo(Vertex c1, Vertex c2, Vertex c3, float w1, float w2, float w3) { + this.controlVertex1 = c1; + this.controlVertex2 = c2; + this.controlVertex3 = c3; + this.weight1 = w1; // Weight for controlVertex1 + this.weight2 = w2; // Weight for controlVertex2 + this.weight3 = w3; // Weight for controlVertex3 + } + } + + // [辅助内部类] 用于方便地传递找到的三角形信息 + private static class TriangleInfo { + int i1, i2, i3; + Vertex v1, v2, v3; + TriangleInfo(int i1, int i2, int i3, Vertex v1, Vertex v2, Vertex v3) { + this.i1 = i1; this.i2 = i2; this.i3 = i3; + this.v1 = v1; this.v2 = v2; this.v3 = v3; + } + } + + // [辅助方法] 查找包含给定点的三角形 + private TriangleInfo findTriangleContainingPoint(Vector2f point) { + for (int i = 0; i < this.activeVertexList.getIndices().length; i += 3) { + int i1 = this.activeVertexList.getIndices()[i]; + int i2 = this.activeVertexList.getIndices()[i + 1]; + int i3 = this.activeVertexList.getIndices()[i + 2]; + + if (i1 >= activeVertexList.size() || i2 >= activeVertexList.size() || i3 >= activeVertexList.size()) { + continue; + } + + Vertex v1 = activeVertexList.get(i1); + Vertex v2 = activeVertexList.get(i2); + Vertex v3 = activeVertexList.get(i3); + + // [核心修正] + // 直接使用 vertex.position,它已经是我们需要的世界坐标了。 + if (isPointInTriangle(point, v1.position, v2.position, v3.position)) { + return new TriangleInfo(i1, i2, i3, v1, v2, v3); + } + } + return null; + } + + // [辅助方法] 判断点是否在三角形内(基于Barycentric坐标) + private boolean isPointInTriangle(Vector2f p, Vector2f a, Vector2f b, Vector2f c) { + Vector2f coords = barycentric(p, a, b, c); + // 如果所有权重都在0和1之间,则点在三角形内 + return coords.x >= 0 && coords.y >= 0 && (coords.x + coords.y) <= 1; + } + + // [辅助方法] 计算Barycentric坐标 + private Vector2f barycentric(Vector2f p, Vector2f a, Vector2f b, Vector2f c) { + Vector2f v0 = new Vector2f(b).sub(a); + Vector2f v1 = new Vector2f(c).sub(a); + Vector2f v2 = new Vector2f(p).sub(a); + float d00 = v0.dot(v0); + float d01 = v0.dot(v1); + float d11 = v1.dot(v1); + float d20 = v2.dot(v0); + float d21 = v2.dot(v1); + float denom = d00 * d11 - d01 * d01; + if (Math.abs(denom) < 1e-9) return new Vector2f(-1, -1); // 退化三角形 + float w = (d11 * d20 - d01 * d21) / denom; + float v = (d00 * d21 - d01 * d20) / denom; + return new Vector2f(w, v); + } + + /** + * [重构] 设置内部控制点,并激活“内部点模式”。 + * 调用此方法会自动禁用“笼模式”。 */ public void setDeformationControlVertices(List controlVertices) { this.deformationControlVertices.clear(); + this.deformationControlCage.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.deformationControlVertices.addAll(controlVertices); } - this.deformationWeightsDirty = true; - logger.info("已设置并排序 {} 个变形控制点。", this.deformationControlVertices.size()); + + applyDeformation(); } - - /** - * 预计算权重。对于每个 DEFAULT 顶点,计算其相对于所有控制点的重心坐标。 - * 这是一个简化的实现,适用于凸多边形控制笼。 + * 尝试将当前顶点列表中的顶点进行“ localized push”,即基于当前顶点列表的状态,将当前顶点列表中的顶点进行“ localized push”。 + * @param baseVertexState 拖动开始时的顶点列表 + * @param pushCenterWorld 推送中心点 + * @param delta 推送向量 + * @param radius 推送半径 */ - private void precomputeDeformationWeights() { - deformationWeights.clear(); - if (deformationControlVertices.size() < 2) { // 至少需要2个点才能形成边 + public void applyLocalizedPush(List baseVertexState, Vector2f pushCenterWorld, Vector2f delta, float radius) { + if (activeVertexList == null || activeVertexList.isEmpty() || baseVertexState.size() != activeVertexList.size()) { return; } - logger.debug("开始预计算权重..."); + float radiusSq = radius * radius; - List controlBoundary = new ArrayList<>(); - for (Vertex v : this.deformationControlVertices) { - controlBoundary.add(v.originalPosition); - } + for (int i = 0; i < activeVertexList.size(); i++) { + Vertex currentVertex = activeVertexList.get(i); + Vertex baseStateVertex = baseVertexState.get(i); // 获取这个顶点在拖动开始时的状态 - int count = 0; - for (Vertex vertex : activeVertexList) { - if (vertex.getTag() == VertexTag.DEFAULT) { + // 为了保证影响范围计算的一致性,我们仍然基于顶点的原始位置来计算距离 + float distSq = currentVertex.originalPosition.distanceSquared(pushCenterWorld); - 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 (distSq < radiusSq) { + // 影响因子计算不变 + float normalizedDist = distSq / radiusSq; + float influence = (float) Math.exp(-normalizedDist * 4.0); - if (isPointOnSegment(vertex.originalPosition, control1.originalPosition, control2.originalPosition)) { - // 如果在边上,使用绝对安全的线性插值计算权重 - float distTotal = control1.originalPosition.distance(control2.originalPosition); - if (distTotal < 1e-6f) continue; + // [核心逻辑变更] + // 新的位置 = 拖动开始时的位置 + 本次拖动带来的位移 + currentVertex.position.set(baseStateVertex.position).add(delta.x * influence, delta.y * influence); - 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; - } + // 如果顶点在影响范围之外,它的位置应该恢复到本次拖动开始前的状态 + currentVertex.position.set(baseStateVertex.position); } } - // 如果所有边的叉积符号都相同,那么点就在内部(或边上) - 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); + // 标记网格为脏,以便GPU缓冲更新 + markDirty(); } // ==================== 网格数据设置 ==================== @@ -374,7 +446,7 @@ public class Mesh2D { float v = uvs[i * 2 + 1]; this.activeVertexList.add(new Vertex(x, y, u, v)); } - this.indices = indices.clone(); + activeVertexList.setIndices(indices.clone()); this.originalPivot.set(this.pivot); markDirty(); } @@ -571,29 +643,61 @@ public class Mesh2D { /** - * 创建矩形网格 + * 创建一个四边形网格。 + * 这个版本不仅包含四个角点,还在上、下、左、右四条边的中点,以及整个图形的中心,都添加了顶点。 + * 最终形成的网格由9个顶点和8个三角形构成,这种“钻石”拓扑结构非常适合平滑的变形。 + * + * @param name 网格的名称 + * @param width 宽度 + * @param height 高度 + * @return 一个新的、经过优化的 Mesh2D 对象 */ public static Mesh2D createQuad(String name, float width, float height) { - float hw = width / 2.0f; - float hh = height / 2.0f; + float hw = width / 2.0f; // half-width + float hh = height / 2.0f; // half-height + // 定义9个顶点: 4个角点, 4个边中点, 1个中心点 float[] vertices = { - -hw, -hh, // 左下 - hw, -hh, // 右下 - hw, hh, // 右上 - -hw, hh // 左上 + // 角点 + -hw, -hh, // 0: 左下 + hw, -hh, // 1: 右下 + hw, hh, // 2: 右上 + -hw, hh, // 3: 左上 + // 边中点 + 0, -hh, // 4: 下中 + hw, 0, // 5: 右中 + 0, hh, // 6: 上中 + -hw, 0, // 7: 左中 + // 中心点 + 0, 0 // 8: 中心 }; + // 对应的9个UV坐标 float[] uvs = { - 0.0f, 1.0f, // 左下 - 1.0f, 1.0f, // 右下 - 1.0f, 0.0f, // 右上 - 0.0f, 0.0f // 左上 + // 角点 + 0.0f, 1.0f, // 0: 左下 + 1.0f, 1.0f, // 1: 右下 + 1.0f, 0.0f, // 2: 右上 + 0.0f, 0.0f, // 3: 左上 + // 边中点 + 0.5f, 1.0f, // 4: 下中 + 1.0f, 0.5f, // 5: 右中 + 0.5f, 0.0f, // 6: 上中 + 0.0f, 0.5f, // 7: 左中 + // 中心点 + 0.5f, 0.5f // 8: 中心 }; + // 使用中心点(索引8)作为公共点,构建8个三角形 (Triangle Fan) int[] indices = { - 0, 1, 2, // 第一个三角形 - 0, 2, 3 // 第二个三角形 + 8, 0, 4, + 8, 4, 1, + 8, 1, 5, + 8, 5, 2, + 8, 2, 6, + 8, 6, 3, + 8, 3, 7, + 8, 7, 0 }; return new Mesh2D(name, vertices, uvs, indices); @@ -696,13 +800,12 @@ public class Mesh2D { newIndices.add(i + 1); } - this.indices = new int[newIndices.size()]; + activeVertexList.setIndices(new int[newIndices.size()]); for (int i = 0; i < newIndices.size(); i++) { - this.indices[i] = newIndices.get(i); + this.activeVertexList.getIndices()[i] = newIndices.get(i); } } else { - // 顶点太少,清空索引 - this.indices = new int[0]; + this.activeVertexList.setIndices(new int[0]); } } @@ -770,6 +873,13 @@ public class Mesh2D { } } + public void addVertex(Vertex vertex){ + if (activeVertexList != null) { + activeVertexList.add(vertex); + markDirty(); + } + } + /** * 在指定位置插入顶点 */ @@ -808,11 +918,11 @@ public class Mesh2D { * 在插入顶点后更新索引数组 */ private void updateIndicesAfterVertexInsertion(int insertedIndex) { - if (indices == null) return; + if (activeVertexList.getIndices() == null) return; - for (int i = 0; i < indices.length; i++) { - if (indices[i] >= insertedIndex) { - indices[i]++; + for (int i = 0; i < activeVertexList.getIndices().length; i++) { + if (activeVertexList.getIndices()[i] >= insertedIndex) { + activeVertexList.getIndices()[i]++; } } } @@ -821,14 +931,14 @@ public class Mesh2D { * 在移除顶点后更新索引数组 */ private void updateIndicesAfterVertexRemoval(int removedIndex) { - if (indices == null) return; + if (activeVertexList.getIndices() == 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]; + for (int i = 0; i < activeVertexList.getIndices().length; i += 3) { + int i1 = activeVertexList.getIndices()[i]; + int i2 = activeVertexList.getIndices()[i + 1]; + int i3 = activeVertexList.getIndices()[i + 2]; // 如果三角形包含被删除的顶点,跳过这个三角形 if (i1 == removedIndex || i2 == removedIndex || i3 == removedIndex) { @@ -846,9 +956,9 @@ public class Mesh2D { } // 转换回数组 - this.indices = new int[newIndicesList.size()]; + activeVertexList.setIndices(new int[newIndicesList.size()]); for (int i = 0; i < newIndicesList.size(); i++) { - this.indices[i] = newIndicesList.get(i); + activeVertexList.getIndices()[i] = newIndicesList.get(i); } } @@ -955,14 +1065,12 @@ public class Mesh2D { 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; } @@ -987,6 +1095,14 @@ public class Mesh2D { return activeVertexList; } + public List getDeformationControlCage() { + return deformationControlCage; + } + + public List getDeformationControlVertices() { + return deformationControlVertices; + } + /** * 顶点变换器接口 */ @@ -1183,11 +1299,11 @@ public class Mesh2D { * 获取索引缓冲区数据 */ public IntBuffer getIndexBuffer(IntBuffer buffer) { - if (buffer == null || buffer.capacity() < indices.length) { + if (buffer == null || buffer.capacity() < activeVertexList.getIndices().length) { throw new IllegalArgumentException("Buffer is null or too small"); } buffer.clear(); - buffer.put(indices); + buffer.put(activeVertexList.getIndices()); buffer.flip(); return buffer; } @@ -1251,15 +1367,21 @@ public class Mesh2D { public void uploadToGPU() { if (uploaded) return; + for (int i = activeVertexList.size() - 1; i >= 0; i--) { + Vertex v = activeVertexList.get(i); + if (v != null && v.isDelete()) { + removeVertexAndRemapIndices(v); + } + } RenderSystem.assertOnRenderThread(); int vertexCount = getVertexCount(); - if (vertexCount == 0 || indices.length == 0) { + if (vertexCount == 0 || activeVertexList.getIndices().length == 0) { return; } FloatBuffer interleaved = MemoryUtil.memAllocFloat(vertexCount * 4); - IntBuffer ib = MemoryUtil.memAllocInt(indices.length); + IntBuffer ib = MemoryUtil.memAllocInt(activeVertexList.getIndices().length); try { getInterleavedBuffer(interleaved); getIndexBuffer(ib); @@ -1282,7 +1404,7 @@ public class Mesh2D { RenderSystem.vertexAttribPointer(1, 2, RenderSystem.GL_FLOAT, false, stride, 2 * Float.BYTES); RenderSystem.glBindVertexArray(() -> 0); - indexCount = indices.length; + indexCount = activeVertexList.getIndices().length; uploaded = true; markClean(); // 上传完成后,将数据标记为干净 } finally { @@ -1297,8 +1419,7 @@ public class Mesh2D { */ public void draw(int shaderProgram, Matrix3f modelMatrix) { if (!visible) return; - if (indices == null || indices.length == 0) return; - + if (activeVertexList.getIndices() == null || activeVertexList.getIndices().length == 0) return; if (dirty) { deleteGPU(); uploadToGPU(); @@ -1428,7 +1549,7 @@ public class Mesh2D { } } - private void setSolidShader(Matrix3f modelMatrix) { + public void setSolidShader(Matrix3f modelMatrix) { ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader"); if (solidShader != null && solidShader.programId != 0) { solidShader.use(); @@ -1616,7 +1737,7 @@ public class Mesh2D { } public void draw() { if (!visible) return; - if (indices == null || indices.length == 0) return; + if (activeVertexList.getIndices() == null || activeVertexList.getIndices().length == 0) return; if (!uploaded) { uploadToGPU(); } @@ -1696,7 +1817,7 @@ public class Mesh2D { } public int[] getIndices() { - return indices.clone(); + return activeVertexList.getIndices().clone(); } public Texture getTexture() { @@ -1727,7 +1848,7 @@ public class Mesh2D { } public int getIndexCount() { - return indices.length; + return activeVertexList.getIndices().length; } // ==================== 工具方法 ==================== @@ -1737,8 +1858,6 @@ public class Mesh2D { */ 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; @@ -1791,7 +1910,6 @@ public class Mesh2D { return visible == mesh2D.visible && drawMode == mesh2D.drawMode && Objects.equals(name, mesh2D.name) && - java.util.Arrays.equals(indices, mesh2D.indices) && Objects.equals(pivot, mesh2D.pivot); } @@ -1800,7 +1918,7 @@ public class Mesh2D { int result = Objects.hash(name, pivot, visible, drawMode); - result = 31 * result + java.util.Arrays.hashCode(indices); + result = 31 * result + java.util.Arrays.hashCode(activeVertexList.getIndices()); return result; } @@ -1811,7 +1929,6 @@ public class 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) @@ -1840,4 +1957,198 @@ public class Mesh2D { sb.append('}'); return sb.toString(); } + + private static class Delaunay { + public static List triangulate(List vertices) { + List triangles = new ArrayList<>(); + if (vertices == null || vertices.size() < 3) { + return triangles; + } + + // 1. 创建一个足够大的“超级三角形”,它必须能包含所有点 + float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE; + float maxX = Float.MIN_VALUE, maxY = Float.MIN_VALUE; + for (Vertex v : vertices) { + minX = Math.min(minX, v.originalPosition.x); + minY = Math.min(minY, v.originalPosition.y); + maxX = Math.max(maxX, v.originalPosition.x); + maxY = Math.max(maxY, v.originalPosition.y); + } + float dx = maxX - minX; + float dy = maxY - minY; + float deltaMax = Math.max(dx, dy); + float midx = (minX + maxX) / 2f; + float midy = (minY + maxY) / 2f; + + Vertex p1 = new Vertex(midx - 20 * deltaMax, midy - deltaMax, 0, 0); + Vertex p2 = new Vertex(midx, midy + 20 * deltaMax, 0, 0); + Vertex p3 = new Vertex(midx + 20 * deltaMax, midy - deltaMax, 0, 0); + Triangle superTriangle = new Triangle(p1, p2, p3); + + triangles.add(superTriangle); + + // 2. 逐个将点添加到三角剖分中 + for (Vertex vertex : vertices) { + List badTriangles = new ArrayList<>(); + List polygon = new ArrayList<>(); + + // a. 找到所有外接圆包含当前点的“坏”三角形 + for (Triangle t : triangles) { + if (t.circumcircleContains(vertex)) { + badTriangles.add(t); + } + } + + // b. 从“坏”三角形中提取出不共享的边,形成一个多边形空腔 + for (Triangle t1 : badTriangles) { + for (Edge e : t1.edges) { + boolean shared = false; + for (Triangle t2 : badTriangles) { + if (t1 != t2 && t2.hasEdge(e)) { + shared = true; + break; + } + } + if (!shared) { + polygon.add(e); + } + } + } + + // c. 移除所有“坏”三角形 + triangles.removeAll(badTriangles); + + // d. 将当前点与多边形空腔的每一条边连接,形成新的三角形 + for (Edge e : polygon) { + triangles.add(new Triangle(e.p1, e.p2, vertex)); + } + } + + // 3. 移除所有与“超级三角形”的顶点相连的三角形 + triangles.removeIf(t -> t.hasVertex(p1) || t.hasVertex(p2) || t.hasVertex(p3)); + + return triangles; + } + } + + public void removeVertexAndRemapIndices(Vertex vertexToRemove) { + if (activeVertexList == null || !activeVertexList.vertices.contains(vertexToRemove)) { + return; + } + + final int removedIndex = activeVertexList.vertices.indexOf(vertexToRemove); + if (removedIndex == -1) { + return; + } + Set neighborIndices = new LinkedHashSet<>(); + for (int i = 0; i < activeVertexList.getIndices().length; i += 3) { + int i1 = activeVertexList.getIndices()[i]; + int i2 = activeVertexList.getIndices()[i + 1]; + int i3 = activeVertexList.getIndices()[i + 2]; + + if (i1 == removedIndex) { + neighborIndices.add(i2); + neighborIndices.add(i3); + } else if (i2 == removedIndex) { + neighborIndices.add(i1); + neighborIndices.add(i3); + } else if (i3 == removedIndex) { + neighborIndices.add(i1); + neighborIndices.add(i2); + } + } + List sortedNeighbors = new ArrayList<>(neighborIndices); + activeVertexList.remove(removedIndex); + List newIndicesList = new ArrayList<>(); + for (int i = 0; i < activeVertexList.getIndices().length; i += 3) { + int i1 = activeVertexList.getIndices()[i]; + int i2 = activeVertexList.getIndices()[i + 1]; + int i3 = activeVertexList.getIndices()[i + 2]; + 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); + } + if (sortedNeighbors.size() >= 3) { + List remappedNeighbors = new ArrayList<>(); + for (int neighborIdx : sortedNeighbors) { + remappedNeighbors.add(neighborIdx > removedIndex ? neighborIdx - 1 : neighborIdx); + } + int pivotIndex = remappedNeighbors.get(0); + for (int i = 1; i < remappedNeighbors.size() - 1; i++) { + newIndicesList.add(pivotIndex); + newIndicesList.add(remappedNeighbors.get(i)); + newIndicesList.add(remappedNeighbors.get(i + 1)); + } + } + activeVertexList.setIndices(newIndicesList.stream().mapToInt(Integer::intValue).toArray()); + markDirty(); + } + + private static class Triangle { + Vertex p1, p2, p3; + Edge[] edges; + Circle circumcircle; + + Triangle(Vertex v1, Vertex v2, Vertex v3) { + this.p1 = v1; this.p2 = v2; this.p3 = v3; + this.edges = new Edge[]{new Edge(v1, v2), new Edge(v2, v3), new Edge(v3, v1)}; + + // 计算外接圆 + float ax = p1.originalPosition.x, ay = p1.originalPosition.y; + float bx = p2.originalPosition.x, by = p2.originalPosition.y; + float cx = p3.originalPosition.x, cy = p3.originalPosition.y; + + float d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)); + if (Math.abs(d) < 1e-12) { // 共线 + this.circumcircle = new Circle(0,0,-1); // 无效圆 + return; + } + float asq = ax * ax + ay * ay; + float bsq = bx * bx + by * by; + float csq = cx * cx + cy * cy; + float ux = (asq * (by - cy) + bsq * (cy - ay) + csq * (ay - by)) / d; + float uy = (asq * (cx - bx) + bsq * (ax - cx) + csq * (bx - ax)) / d; + float r = (float) Math.sqrt((ax - ux) * (ax - ux) + (ay - uy) * (ay - uy)); + this.circumcircle = new Circle(ux, uy, r); + } + + boolean hasVertex(Vertex v) { return p1 == v || p2 == v || p3 == v; } + boolean hasEdge(Edge e) { return edges[0].equals(e) || edges[1].equals(e) || edges[2].equals(e); } + boolean circumcircleContains(Vertex v) { return circumcircle.contains(v); } + } + + private static class Edge { + Vertex p1, p2; + Edge(Vertex v1, Vertex v2) { this.p1 = v1; this.p2 = v2; } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + Edge edge = (Edge) obj; + return (p1 == edge.p1 && p2 == edge.p2) || (p1 == edge.p2 && p2 == edge.p1); + } + @Override + public int hashCode() { return Objects.hash(p1) + Objects.hash(p2); } + } + + private static class Circle { + float x, y, radius, radiusSq; + Circle(float x, float y, float r) { + this.x = x; this.y = y; this.radius = r; this.radiusSq = r * r; + } + boolean contains(Vertex v) { + if (radius < 0) return false; + float dx = x - v.originalPosition.x; + float dy = y - v.originalPosition.y; + return (dx * dx + dy * dy) < radiusSq; + } + } } \ No newline at end of file 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 d9b0ddf..60ba0ea 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -1612,7 +1612,6 @@ public class ModelPart { Vector2f worldPoint = Matrix3fUtils.transformPoint(this.worldTransform, localPoint); vertex.position.set(worldPoint); } - mesh.applyDeformation(); mesh.markDirty(); } for (ModelPart child : children) { 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 index 8620255..41ce4e9 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Vertex.java @@ -16,6 +16,9 @@ public class Vertex { public Vector2f originalPosition; // 原始顶点位置 (用于变形) private VertexTag tag; private boolean selected; + private String name; + private boolean isDelete; + public int index; /** * 构造函数 @@ -45,6 +48,13 @@ public class Vertex { this.tag = VertexTag.DEFAULT; } + public Vertex(Vector2f position, Vector2f uv, VertexTag tag) { + this.position = new Vector2f(position); + this.uv = new Vector2f(uv); + this.originalPosition = new Vector2f(position); + this.tag = tag; + } + /** * 构造函数(用于复制) * @@ -59,6 +69,20 @@ public class Vertex { this.tag = VertexTag.DEFAULT; } + /** + * 便捷构造函数,用于仅通过位置创建顶点 + * UV坐标将默认为 (0, 0)。 + * + * @param x 顶点 x 坐标 + * @param y 顶点 y 坐标 + */ + public Vertex(float x, float y) { + this.position = new Vector2f(x, y); + this.uv = new Vector2f(0, 0); // 为UV坐标提供一个默认值 + this.originalPosition = new Vector2f(x, y); + this.tag = VertexTag.DEFAULT; + } + public VertexTag getTag() { return tag; } @@ -67,6 +91,18 @@ public class Vertex { this.tag = tag; } + public void setIndex(int index) { + this.index = index; + } + + public void delete(){ + isDelete = true; + } + + public int getIndex() { + return index; + } + /** * 重置为原始位置 */ @@ -97,6 +133,10 @@ public class Vertex { return selected; } + public boolean isDelete() { + return isDelete; + } + /** * 创建此顶点的深拷贝 */ @@ -109,22 +149,38 @@ public class Vertex { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Vertex vertex = (Vertex) o; - return Objects.equals(position, vertex.position) && + return selected == vertex.selected && + isDelete == vertex.isDelete && + Objects.equals(position, vertex.position) && Objects.equals(uv, vertex.uv) && - Objects.equals(originalPosition, vertex.originalPosition); + Objects.equals(originalPosition, vertex.originalPosition) && + tag == vertex.tag && + Objects.equals(name, vertex.name); } @Override public int hashCode() { - return Objects.hash(position, uv, originalPosition); + return Objects.hash(position, uv, originalPosition, tag, selected, name, isDelete); } @Override public String toString() { return "Vertex{" + - "pos=" + position + + "name='" + name + '\'' + + ", pos=" + position + ", uv=" + uv + ", orig=" + originalPosition + + ", tag=" + tag + + ", selected=" + selected + + ", isDelete=" + isDelete + '}'; } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } } \ 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 index f232690..dee9c1a 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexList.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/VertexList.java @@ -2,14 +2,11 @@ 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; +import java.util.*; /** - * 一个带有名称和标签的 Vertex 集合。 + * 一个自包含的几何数据单元,用于管理顶点(vertices) + * 以及定义它们之间拓扑结构(三角形)的索引(indices)。 * * @author Gemini */ @@ -17,79 +14,115 @@ public class VertexList implements Iterable { public final List vertices; private String name; + private int[] indices; /** * 默认构造函数。 - * 名称默认为 "KongFuZi",标签默认为 "DEFAULT"。 */ public VertexList() { this.vertices = new ArrayList<>(); this.name = "KongFuZi"; + this.indices = new int[0]; // 初始化为空数组 } /** * 构造函数 * * @param name 此列表的名称 - * @param tag 此列表的标签 (VertexTag) */ public VertexList(String name) { this.vertices = new ArrayList<>(); this.name = Objects.requireNonNull(name, "Name cannot be null"); + this.indices = new int[0]; // 初始化为空数组 } /** * 构造函数 * - * @param name 此列表的名称 - * @param tag 此列表的标签 + * @param name 此列表的名称 * @param initialVertices 用于初始化列表的顶点集合 + * @param initialIndices 用于初始化列表的索引集合 */ - public VertexList(String name, Collection initialVertices) { + public VertexList(String name, Collection initialVertices, int[] initialIndices) { this(name); if (initialVertices != null) { this.vertices.addAll(initialVertices); } + setIndices(initialIndices); // 使用setter来安全地设置初始索引 } - // --- 列表管理 --- + // --- 列表管理 (已更新以支持索引) --- /** * 向列表末尾添加一个顶点。 + * 注意:这不会自动更新索引。您需要手动调用 setIndices() 来使用这个新顶点。 * * @param vertex 要添加的顶点 */ public void add(Vertex vertex) { if (vertex != null) { + vertex.setIndex(this.vertices.size()); this.vertices.add(vertex); } } /** - * 移除列表中的指定顶点。 + * 移除列表中的指定顶点,并安全地移除所有引用它的三角形,同时重映射所有后续索引。 * * @param vertex 要移除的顶点 * @return 如果成功移除则为 true */ - public boolean remove(Vertex vertex) { - return this.vertices.remove(vertex); + public boolean remove(Object vertex) { + int index = this.vertices.indexOf(vertex); + if (index != -1) { + remove(index); // 委托给基于索引的移除方法 + return true; + } + return false; } /** - * 移除指定索引处的顶点。 + * [已重构] 移除指定索引处的顶点,并安全地移除所有引用它的三角形,同时重映射所有后续索引。 * * @param index 要移除的索引 * @return 被移除的顶点 */ public Vertex remove(int index) { + if (index < 0 || index >= this.vertices.size()) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + this.vertices.size()); + } + + // 核心逻辑:在移除顶点前,必须重构索引数组 + List newIndicesList = new ArrayList<>(); + for (int i = 0; i < this.indices.length; i += 3) { + int i1 = this.indices[i]; + int i2 = this.indices[i + 1]; + int i3 = this.indices[i + 2]; + + // 如果三角形包含被删除的顶点,则整个三角形都应该被丢弃 + if (i1 == index || i2 == index || i3 == index) { + continue; // 跳过这个三角形 + } + + // 调整索引编号:任何大于被删除索引的索引都需要减一 + if (i1 > index) i1--; + if (i2 > index) i2--; + if (i3 > index) i3--; + + newIndicesList.add(i1); + newIndicesList.add(i2); + newIndicesList.add(i3); + } + + // 用重构后的新索引数组替换旧的 + this.indices = newIndicesList.stream().mapToInt(Integer::intValue).toArray(); + + // 最后,安全地从列表中移除顶点 return this.vertices.remove(index); } /** * 获取指定索引处的顶点。 - * - * @param index 索引 - * @return 顶点 */ public Vertex get(int index) { return this.vertices.get(index); @@ -97,8 +130,6 @@ public class VertexList implements Iterable { /** * 返回列表中的顶点数量。 - * - * @return 顶点数量 */ public int size() { return this.vertices.size(); @@ -106,53 +137,124 @@ public class VertexList implements Iterable { /** * 检查列表是否为空。 - * - * @return 如果为空则为 true */ public boolean isEmpty() { return this.vertices.isEmpty(); } /** - * 清空列表中的所有顶点。 + * [已重构] 清空列表中的所有顶点和索引。 */ public void clear() { this.vertices.clear(); + this.indices = new int[0]; } /** - * 返回内部列表的只读视图。 - * (注意:如果需要修改,请使用 add/remove 等方法) + * 返回内部顶点列表的副本。 * - * @return 顶点的列表 + * @return 顶点的列表副本 */ public List getVertices() { - return new ArrayList<>(vertices); // 返回一个副本以防止外部修改 + return new ArrayList<>(vertices); } - // --- Getter 和 Setter --- - /** - * 获取此列表的名称。 + * 根据标签过滤并返回顶点列表。 + * + * @return 过滤后的顶点列表 */ + public List getVertices(VertexTag tag) { + return vertices.stream() + .filter(vertex -> vertex.getTag() == tag) + .toList(); + } + + // --- Getter 和 Setter (已更新以支持索引) --- + public String getName() { return name; } - /** - * 设置此列表的名称。 - * - * @param name 新名称 - */ public void setName(String name) { this.name = Objects.requireNonNull(name, "Name cannot be null"); } - // --- 迭代器 --- + /** + * [新增] 获取索引数组的副本。 + * + * @return 索引数组的克隆 + */ + public int[] getIndices() { + return indices; + } /** - * 返回顶点的迭代器。 + * [新增] 获取仅由具有指定标签(Tag)的顶点组成的三角形索引。 + * + *

此方法会执行以下操作: + *

    + *
  1. 筛选出所有具有指定 {@code tag} 的顶点。
  2. + *
  3. 遍历原始索引数组中的所有三角形。
  4. + *
  5. 如果一个三角形的【全部三个顶点】都符合指定的 {@code tag},则保留该三角形。
  6. + *
  7. 将这些保留下来的三角形的索引,重映射到仅包含筛选后顶点的新索引空间中。
  8. + *
+ * + * @param tag 要筛选的顶点标签 + * @return 一个新的索引数组,其中的索引值对应于通过 {@code getVertices(tag)} 获取的顶点列表。 */ + public int[] getIndices(VertexTag tag) { + if (tag == null) { + return new int[0]; + } + + // 1. 快速找到所有带标签的顶点的原始索引,存入一个Set以便快速查找 + Set taggedOriginalIndices = new HashSet<>(); + for (int i = 0; i < this.vertices.size(); i++) { + if (this.vertices.get(i).getTag() == tag) { + taggedOriginalIndices.add(i); + } + } + + // 如果没有任何顶点带此标签,则不可能有相关三角形 + if (taggedOriginalIndices.isEmpty()) { + return new int[0]; + } + + // 2. 遍历所有三角形,只要有一个顶点的索引在Set中,就保留该三角形 + List newIndices = new ArrayList<>(); + for (int i = 0; i < this.indices.length; i += 3) { + int i1 = this.indices[i]; + int i2 = this.indices[i + 1]; + int i3 = this.indices[i + 2]; + + // 核心逻辑修改:从 && (AND) 改为 || (OR) + if (taggedOriginalIndices.contains(i1) || + taggedOriginalIndices.contains(i2) || + taggedOriginalIndices.contains(i3)) { + + // 添加原始索引,因为它们将用于原始的、完整的顶点列表 + newIndices.add(i1); + newIndices.add(i2); + newIndices.add(i3); + } + } + + return newIndices.stream().mapToInt(Integer::intValue).toArray(); + } + + /** + * [新增] 设置索引数组。 + * + * @param indices 新的索引数组 + */ + public void setIndices(int[] indices) { + this.indices = (indices != null) ? indices.clone() : new int[0]; + } + + + // --- 迭代器 --- + @Override public @NotNull Iterator iterator() { return this.vertices.iterator(); @@ -165,6 +267,7 @@ public class VertexList implements Iterable { return "VertexList{" + "name='" + name + '\'' + ", vertexCount=" + vertices.size() + + ", indexCount=" + indices.length + '}'; } @@ -174,12 +277,15 @@ public class VertexList implements Iterable { if (o == null || getClass() != o.getClass()) return false; VertexList that = (VertexList) o; return Objects.equals(vertices, that.vertices) && - Objects.equals(name, that.name); + Objects.equals(name, that.name) && + Arrays.equals(indices, that.indices); } @Override public int hashCode() { - return Objects.hash(vertices, name); + int result = Objects.hash(vertices, name); + result = 31 * result + Arrays.hashCode(indices); + return result; } public void set(int index, Vertex vertex) { 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 c72f9d1..ef720f1 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 @@ -11,21 +11,24 @@ import org.joml.Vector2f; import org.joml.Vector4f; import org.lwjgl.opengl.GL11; -import java.util.*; -import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; /** - * [MODIFIED] 顶点变形渲染工具 - * 该类现在渲染 Mesh2D 中被标记为 VertexTag.DEFORMATION 的一级顶点。 + * [已修改] 顶点变形渲染工具 (支持外部控制笼) + * 该类现在渲染: + * 1. 底层被变形网格的【三角剖分线框】。 + * 2. 位于上层的【外部控制笼】(控制点和连线)。 + * 这样可以直观地展示控制笼如何影响网格变形。 */ public class VertexDeformationRander extends RanderTools { - // [NEW] 定义可变形顶点的默认渲染大小 - private static final float DEFORMATION_VERTEX_SIZE = 8.0f; + private static final float DEFORMATION_VERTEX_SIZE = 6.0f; @Override public void init(Map algorithmEnabled) { - // [MODIFIED] 更新算法名称以反映新功能 algorithmEnabled.put("showDeformationVertices", false); algorithmEnabled.put("showDeformationVertexInfluence", true); } @@ -33,9 +36,14 @@ public class VertexDeformationRander extends RanderTools { @Override public boolean render(Matrix3f modelMatrix, Object renderContext) { if (renderContext instanceof Mesh2D mesh2D) { - // [MODIFIED] 检查新的算法开关并调用新的渲染方法 if (mesh2D.getStates("showDeformationVertices")) { - drawDeformationVertices(mesh2D); + RenderSystem.pushState(); + try { + mesh2D.setSolidShader(modelMatrix); + drawControlCageAndMesh(mesh2D); + } finally { + RenderSystem.popState(); + } return true; } } @@ -43,44 +51,28 @@ public class VertexDeformationRander extends RanderTools { } /** - * [MODIFIED] 重写此方法以渲染被标记为 DEFORMATION 的一级顶点。 - * @param mesh2D 要渲染的网格 + * [已修改] 此方法现在会先渲染底层网格的线框,然后再渲染上层的控制笼。 + * @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; // 没有可变形顶点,无需渲染 - } - + private void drawControlCageAndMesh(Mesh2D mesh2D) { + List controlCage = mesh2D.getDeformationControlVertices(); Tesselator t = Tesselator.getInstance(); BufferBuilder bb = t.getBuilder(); - RenderSystem.pushState(); try { RenderSystem.enableBlend(); RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); GL11.glDisable(GL11.GL_DEPTH_TEST); - - // 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); + drawMeshWireframe(bb, mesh2D); + if (controlCage != null && !controlCage.isEmpty()) { + List orderedCage = getOrderedVertices(controlCage); + drawConnectionLines(bb, orderedCage); + for (Vertex vertex : orderedCage) { + if (vertex == null){ + continue; + } + drawStyledVertex(bb, vertex.position.x, vertex.position.y, vertex.isSelected()); + } } } finally { RenderSystem.popState(); @@ -88,127 +80,113 @@ public class VertexDeformationRander extends RanderTools { } /** - * [MODIFIED] 此方法现在接收一个 List - * 绘制连接可变形顶点的线。 + * 绘制网格的线框,以显示其三角剖分结构。 + * 这就是“Vertex 的控制区域”的可视化。 + * @param bb BufferBuilder 实例 + * @param mesh 目标网格 */ - private void drawConnectionLines(BufferBuilder bb, List verts) { - if (verts.size() < 2) return; - - // 为了美观,我们对顶点进行排序以绘制一个简单的多边形轮廓 - 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); + private void drawMeshWireframe(BufferBuilder bb, Mesh2D mesh) { + List vertices = mesh.getActiveVertexList().getVertices(); + int[] indices = mesh.getActiveVertexList().getIndices(VertexTag.DEFORMATION); + if (vertices == null || vertices.isEmpty() || indices == null || indices.length == 0) { + return; } - bb.endImmediate(); + Vector4f wireframeColor = new Vector4f(0.5f, 0.5f, 0.5f, 0.4f); GL11.glLineWidth(1.0f); - - 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)); + for (int i = 0; i < indices.length; i += 3) { + int i1 = indices[i]; + int i2 = indices[i + 1]; + int i3 = indices[i + 2]; + Vertex v1 = vertices.get(i1); + Vertex v2 = vertices.get(i2); + Vertex v3 = vertices.get(i3); + bb.begin(GL11.GL_LINE_LOOP, 3); + bb.setColor(wireframeColor); + bb.vertex(v1.position.x, v1.position.y, 0f, 0f); + bb.vertex(v2.position.x, v2.position.y, 0f, 0f); + bb.vertex(v3.position.x, v3.position.y, 0f, 0f); + bb.endImmediate(); } } /** - * 根据输入点(任意顺序),生成一个“简单多边形”顺序: - * - 先计算重心 - * - 按相对于重心的 atan2 排序(极角排序) + * 绘制连接控制顶点的虚线。 */ - private java.util.List buildOrderedPolygon(java.util.List pts) { - if (pts == null || pts.size() <= 1) return new ArrayList<>(pts); + private void drawConnectionLines(BufferBuilder bb, List verts) { + if (verts == null || verts.size() < 2) return; + Vector4f lineColor = new Vector4f(0.1f, 0.1f, 0.1f, 0.85f); - // 计算重心 - 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; + for (int i = 0; i < verts.size(); i++) { + Vector2f a = verts.get(i).position; + Vector2f b = verts.get((i + 1) % verts.size()).position; // 使用 % 来处理首尾相连 + drawDashedLine(bb, a.x, a.y, b.x, b.y, 4, 4, lineColor); + } } - // --- 以下是无需修改的通用绘图和几何辅助方法 --- + /** + * 绘制自定义样式的顶点,并根据 'isSelected' 状态改变颜色。 + */ + private void drawStyledVertex(BufferBuilder bb, float cx, float cy, boolean isSelected) { + float halfSize = VertexDeformationRander.DEFORMATION_VERTEX_SIZE / 2.0f; + Vector4f fillColor = isSelected ? new Vector4f(1.0f, 0.3f, 0.3f, 1.0f) : new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); + bb.begin(GL11.GL_QUADS, 4); + bb.setColor(fillColor); + bb.vertex(cx - halfSize, cy - halfSize, 0f, 0f); + bb.vertex(cx + halfSize, cy - halfSize, 0f, 0f); + bb.vertex(cx + halfSize, cy + halfSize, 0f, 0f); + bb.vertex(cx - halfSize, cy + halfSize, 0f, 0f); + bb.endImmediate(); + + GL11.glLineWidth(1.5f); + bb.begin(GL11.GL_LINE_LOOP, 4); + bb.setColor(new Vector4f(0.0f, 0.0f, 0.0f, 1.0f)); + bb.vertex(cx - halfSize, cy - halfSize, 0f, 0f); + bb.vertex(cx + halfSize, cy - halfSize, 0f, 0f); + bb.vertex(cx + halfSize, cy + halfSize, 0f, 0f); + bb.vertex(cx - halfSize, cy + halfSize, 0f, 0f); + bb.endImmediate(); + GL11.glLineWidth(1.0f); + } + + /** + * 辅助方法,获取按极角排序后的顶点列表 (Vertex 对象)。 + */ + private List getOrderedVertices(List verts) { + if (verts == null || verts.size() <= 1) return new ArrayList<>(verts); + + Vector2f center = new Vector2f(0f, 0f); + for (Vertex v : verts) { center.add(v.position); } + center.div(verts.size()); + + List sortedVerts = new ArrayList<>(verts); + sortedVerts.sort(Comparator.comparingDouble(v -> Math.atan2(v.position.y - center.y, v.position.x - center.x))); + return sortedVerts; + } + + /** + * 绘制虚线的底层实现。 + */ 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; + float dx = x2 - x1, dy = y2 - y1; float total = (float)Math.sqrt(dx*dx + dy*dy); if (total < 1e-4f) return; - float nx = dx / total; - float ny = dy / total; - + float nx = dx / total, ny = dy / total; float pos = 0f; + GL11.glLineWidth(1.5f); // 让虚线稍微粗一点 while (pos < total) { float segStart = pos; float segEnd = Math.min(total, pos + segmentLen); if (segEnd > segStart) { - float sx = x1 + nx * segStart; - float sy = y1 + ny * segStart; - float ex = x1 + nx * segEnd; - float ey = y1 + ny * segEnd; + float sx = x1 + nx * segStart, sy = y1 + ny * segStart; + float ex = x1 + nx * segEnd, ey = y1 + ny * segEnd; bb.begin(GL11.GL_LINES, 2); bb.setColor(color); - bb.vertex(sx, sy, 0f, 0f); - bb.vertex(ex, ey, 0f, 0f); + bb.vertex(sx, sy, 0f, 0f); bb.vertex(ex, ey, 0f, 0f); bb.endImmediate(); } pos += segmentLen + gapLen; } - } - - private void drawCircleSolid(BufferBuilder bb, float cx, float cy, float radius, Vector4f color, int segments) { - if (radius <= 0f) return; - segments = Math.max(6, segments); - bb.begin(GL11.GL_TRIANGLE_FAN, segments + 2); - bb.setColor(color); - 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(color); - bb.vertex(x, y, 0f, 0f); - } - bb.endImmediate(); - } - - private void drawCircleGradient(BufferBuilder bb, float cx, float cy, float radius, Vector4f centerColor, Vector4f outerColor, int segments) { - if (radius <= 0f) return; - segments = Math.max(8, segments); - bb.begin(GL11.GL_TRIANGLE_FAN, segments + 2); - bb.setColor(centerColor); - 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(outerColor); - bb.vertex(x, y, 0f, 0f); - } - bb.endImmediate(); - } - - private void drawCircleOutline(BufferBuilder bb, float cx, float cy, float radius, Vector4f color, int segments) { - if (radius <= 0f) return; - segments = Math.max(8, segments); - bb.begin(GL11.GL_LINE_LOOP, segments); - bb.setColor(color); - 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(); + GL11.glLineWidth(1.0f); } } \ No newline at end of file