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