refactor(render):重构关键帧插值器以支持顶点变形- 更新 FrameInterpolator以直接操作 Mesh2D 的一级顶点
- 引入 deformationVertex 参数控制带 VertexTag.DEFORMATION 标签的顶点- 移除对 secondaryVertex 的旧支持及相关冗余代码 - 简化插值计算逻辑并提高角度单位转换容差 - 优化顶点目标计算方法并重命名为 DeformationVertexTarget - 清理无用的反射回退和安全读取机制- 移除 liquify 工具相关的顶点渲染快捷键控制 - 删除已废弃的 LiquifyTargetPartRander 类文件 -优化导入语句并更新相关类引用路径
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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) 增加轻微外发光(透明大边框)和阴影感以达到“现代”外观
|
||||
* <p>
|
||||
* 注意:本类依赖你工程中已有的 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 模型点击事件监听器接口
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,关闭顶点渲染");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.*;
|
||||
|
||||
@@ -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<ModelPart> parts = model.getParts();
|
||||
if (parts == null || parts.isEmpty()) return null;
|
||||
|
||||
for (ModelPart part : parts) {
|
||||
if (part != null && part.isVisible()) {
|
||||
java.util.List<Mesh2D> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<ModelPart, Vector2f> dragStartScales = new HashMap<>();
|
||||
private final Map<ModelPart, Float> dragStartRotations = new HashMap<>();
|
||||
private final Map<ModelPart, Vector2f> 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<ModelPart> 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<ModelPart> 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<ModelPart> 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:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<Integer> findIndicesForParam(ParametersManagement.Parameter fullParam, String paramId, AnimationParameter currentAnimationParameter) {
|
||||
List<Integer> indices = new ArrayList<>();
|
||||
if (fullParam == null || fullParam.paramId() == null || currentAnimationParameter == null) return indices;
|
||||
List<String> pids = fullParam.paramId();
|
||||
// (NEW) Get the list of associated animation parameters
|
||||
List<AnimationParameter> 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<Float> keyframes, List<Integer> 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<Integer> idxs = findIndicesForParam(fullParam, paramId, currentAnimationParameter);
|
||||
// ---- 计算 position/scale/pivot 的目标值 ----
|
||||
private static boolean computeVec2Target(ParametersManagement.Parameter fullParam, String paramId, float current, float[] out, AnimationParameter animParam) {
|
||||
List<Integer> idxs = findIndicesForParam(fullParam, paramId, animParam);
|
||||
if (idxs.isEmpty()) return false;
|
||||
List<AnimationParameter> animParams = fullParam.animationParameter();
|
||||
//List<Boolean> 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<Object> values = fullParam.value();
|
||||
if (values == null) return false;
|
||||
try {
|
||||
if (prevIndex != -1 && nextIndex != -1) {
|
||||
if (prevIndex == nextIndex) {
|
||||
float[] v = readVec2(values.get(prevIndex));
|
||||
out[0] = v[0]; out[1] = v[1];
|
||||
return true;
|
||||
} else {
|
||||
float[] prev = readVec2(values.get(prevIndex));
|
||||
float[] next = readVec2(values.get(nextIndex));
|
||||
float prevVal = fullParam.keyframe().get(prevIndex);
|
||||
float nextVal = fullParam.keyframe().get(nextIndex);
|
||||
float t = computeT(prevVal, nextVal, current);
|
||||
out[0] = prev[0] + t * (next[0] - prev[0]);
|
||||
out[1] = prev[1] + t * (next[1] - prev[1]);
|
||||
return true;
|
||||
}
|
||||
} else if (prevIndex != -1) {
|
||||
|
||||
if (prevIndex != -1 && nextIndex != -1) {
|
||||
if (prevIndex == nextIndex) {
|
||||
float[] v = readVec2(values.get(prevIndex));
|
||||
out[0] = v[0]; out[1] = v[1];
|
||||
return true;
|
||||
} else if (nextIndex != -1) {
|
||||
float[] v = readVec2(values.get(nextIndex));
|
||||
out[0] = v[0]; out[1] = v[1];
|
||||
return true;
|
||||
} else {
|
||||
// 精确匹配(兜底)
|
||||
for (int i : idxs) {
|
||||
if (i < 0 || i >= animParams.size()) continue;
|
||||
// 允许非 keyframe 的值作为实时覆盖
|
||||
float val = fullParam.keyframe().get(i);
|
||||
if (Float.compare(val, current) == 0) {
|
||||
float[] v = readVec2(values.get(i));
|
||||
out[0] = v[0]; out[1] = v[1];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean computeRotationTargetGeneric(ParametersManagement.Parameter fullParam, String paramId, float current, float[] outSingle, AnimationParameter currentAnimationParameter) {
|
||||
if (fullParam == null || outSingle == null) return false;
|
||||
// (MODIFIED) Pass currentAnimationParameter
|
||||
List<Integer> idxs = findIndicesForParam(fullParam, paramId, currentAnimationParameter);
|
||||
if (idxs.isEmpty()) return false;
|
||||
List<?> animParams = fullParam.animationParameter();
|
||||
List<Object> values = fullParam.value();
|
||||
int[] idx = findSurroundingKeyframesForIndices(fullParam.keyframe(), idxs, current);
|
||||
int prevIndex = idx[0], nextIndex = idx[1];
|
||||
|
||||
try {
|
||||
float target;
|
||||
if (prevIndex != -1 && nextIndex != -1) {
|
||||
if (prevIndex == nextIndex) {
|
||||
target = toFloat(values.get(prevIndex));
|
||||
} else {
|
||||
float p = toFloat(values.get(prevIndex));
|
||||
float q = toFloat(values.get(nextIndex));
|
||||
float prevVal = fullParam.keyframe().get(prevIndex);
|
||||
float nextVal = fullParam.keyframe().get(nextIndex);
|
||||
float t = computeT(prevVal, nextVal, current);
|
||||
float diff = normalizeAngle(q - p);
|
||||
target = p + diff * t;
|
||||
}
|
||||
} else if (prevIndex != -1) {
|
||||
target = toFloat(values.get(prevIndex));
|
||||
} else if (nextIndex != -1) {
|
||||
target = toFloat(values.get(nextIndex));
|
||||
} else {
|
||||
float found = Float.NaN;
|
||||
for (int i : idxs) {
|
||||
if (i < 0 || i >= animParams.size()) continue;
|
||||
float val = fullParam.keyframe().get(i);
|
||||
if (Float.compare(val, current) == 0) {
|
||||
found = toFloat(values.get(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Float.isNaN(found)) return false;
|
||||
target = found;
|
||||
}
|
||||
|
||||
outSingle[0] = normalizeAnimAngleUnits(target);
|
||||
float[] prev = readVec2(values.get(prevIndex));
|
||||
float[] next = readVec2(values.get(nextIndex));
|
||||
float t = computeT(fullParam.keyframe().get(prevIndex), fullParam.keyframe().get(nextIndex), current);
|
||||
out[0] = prev[0] + t * (next[0] - prev[0]);
|
||||
out[1] = prev[1] + t * (next[1] - prev[1]);
|
||||
return true;
|
||||
} catch (Throwable ignored) {}
|
||||
} else if (prevIndex != -1) {
|
||||
float[] v = readVec2(values.get(prevIndex));
|
||||
out[0] = v[0]; out[1] = v[1];
|
||||
return true;
|
||||
} else if (nextIndex != -1) {
|
||||
float[] v = readVec2(values.get(nextIndex));
|
||||
out[0] = v[0]; out[1] = v[1];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Secondary vertex 插值(为每个 vertex id 计算目标) ----
|
||||
// 返回列表:每个 SecondaryVertexTarget 表示 id -> 插值后的位置 或 标记为 deleted
|
||||
private static List<SecondaryVertexTarget> computeAllSecondaryVertexTargets(ParametersManagement.Parameter fullParam, String paramId, float current, AnimationParameter currentAnimationParameter) {
|
||||
List<SecondaryVertexTarget> results = new ArrayList<>();
|
||||
if (fullParam == null) return results;
|
||||
// ---- 计算 rotation 的目标值 ----
|
||||
private static boolean computeRotationTarget(ParametersManagement.Parameter fullParam, String paramId, float current, float[] out, AnimationParameter animParam) {
|
||||
List<Integer> idxs = findIndicesForParam(fullParam, paramId, animParam);
|
||||
if (idxs.isEmpty()) return false;
|
||||
|
||||
// (MODIFIED) Pass currentAnimationParameter
|
||||
List<Integer> idxs = findIndicesForParam(fullParam, paramId, currentAnimationParameter);
|
||||
if (idxs.isEmpty()) return results;
|
||||
|
||||
List<?> animParams = fullParam.animationParameter();
|
||||
int[] surrounding = findSurroundingKeyframesForIndices(fullParam.keyframe(), idxs, current);
|
||||
int prevIndex = surrounding[0], nextIndex = surrounding[1];
|
||||
List<Object> values = fullParam.value();
|
||||
if (animParams == null || values == null) return results;
|
||||
|
||||
// 按 vertex id 分组(包含 keyframe 与 非 keyframe,允许实时覆盖)
|
||||
Map<Integer, List<Integer>> idToIndices = new HashMap<>();
|
||||
for (int i : idxs) {
|
||||
if (i < 0 || i >= values.size() || i >= animParams.size()) continue;
|
||||
ParsedVertex pv = parseVertexValue(values.get(i));
|
||||
if (pv == null) continue;
|
||||
idToIndices.computeIfAbsent(pv.id, k -> new ArrayList<>()).add(i);
|
||||
}
|
||||
|
||||
// 对每个 id 单独计算 prev/next 并插值(包括处理删除标记)
|
||||
for (Map.Entry<Integer, List<Integer>> e : idToIndices.entrySet()) {
|
||||
int vid = e.getKey();
|
||||
List<Integer> list = e.getValue();
|
||||
if (list.isEmpty()) continue;
|
||||
|
||||
int prevIndex = -1, nextIndex = -1;
|
||||
float prevVal = Float.NEGATIVE_INFINITY, nextVal = Float.POSITIVE_INFINITY;
|
||||
for (int idx : list) {
|
||||
float val = fullParam.keyframe().get(idx);
|
||||
if (val <= current) {
|
||||
if (prevIndex == -1 || val >= prevVal) {
|
||||
prevIndex = idx;
|
||||
prevVal = val;
|
||||
}
|
||||
}
|
||||
if (val >= current) {
|
||||
if (nextIndex == -1 || val <= nextVal) {
|
||||
nextIndex = idx;
|
||||
nextVal = val;
|
||||
}
|
||||
}
|
||||
float target;
|
||||
if (prevIndex != -1 && nextIndex != -1) {
|
||||
if (prevIndex == nextIndex) {
|
||||
target = toFloat(values.get(prevIndex));
|
||||
} else {
|
||||
float p = toFloat(values.get(prevIndex));
|
||||
float q = toFloat(values.get(nextIndex));
|
||||
float t = computeT(fullParam.keyframe().get(prevIndex), fullParam.keyframe().get(nextIndex), current);
|
||||
target = p + t * normalizeAngle(q - p);
|
||||
}
|
||||
|
||||
try {
|
||||
// 优先 prev/next 插值或取值;若存在 pos==null 的条目(删除),则将其转为 deleted 标记
|
||||
if (prevIndex != -1 && nextIndex != -1) {
|
||||
if (prevIndex == nextIndex) {
|
||||
ParsedVertex pv = parseVertexValue(values.get(prevIndex));
|
||||
if (pv != null) {
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid;
|
||||
tgt.deleted = pv.deleted;
|
||||
tgt.x = pv.x;
|
||||
tgt.y = pv.y;
|
||||
results.add(tgt);
|
||||
}
|
||||
} else {
|
||||
ParsedVertex pv = parseVertexValue(values.get(prevIndex));
|
||||
ParsedVertex nv = parseVertexValue(values.get(nextIndex));
|
||||
if (pv == null || nv == null) {
|
||||
// 若任意一端为 null,则退化为非插值处理(尝试取有意义的一端)
|
||||
if (pv != null) {
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid; tgt.deleted = pv.deleted; tgt.x = pv.x; tgt.y = pv.y;
|
||||
results.add(tgt);
|
||||
} else if (nv != null) {
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid; tgt.deleted = nv.deleted; tgt.x = nv.x; tgt.y = nv.y;
|
||||
results.add(tgt);
|
||||
}
|
||||
} else {
|
||||
// 如果任意一侧为 deleted(pos==null),则不做插值(选择 prev 的删除/存在状态)
|
||||
if (pv.deleted || nv.deleted) {
|
||||
// 选择较靠近 current 的那端(这里优先 prev)
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid;
|
||||
tgt.deleted = pv.deleted;
|
||||
tgt.x = pv.x;
|
||||
tgt.y = pv.y;
|
||||
results.add(tgt);
|
||||
} else {
|
||||
float t = computeT(prevVal, nextVal, current);
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid;
|
||||
tgt.deleted = false;
|
||||
tgt.x = pv.x + t * (nv.x - pv.x);
|
||||
tgt.y = pv.y + t * (nv.y - pv.y);
|
||||
results.add(tgt);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (prevIndex != -1) {
|
||||
ParsedVertex pv = parseVertexValue(values.get(prevIndex));
|
||||
if (pv != null) {
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid; tgt.deleted = pv.deleted; tgt.x = pv.x; tgt.y = pv.y;
|
||||
results.add(tgt);
|
||||
}
|
||||
} else if (nextIndex != -1) {
|
||||
ParsedVertex nv = parseVertexValue(values.get(nextIndex));
|
||||
if (nv != null) {
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid; tgt.deleted = nv.deleted; tgt.x = nv.x; tgt.y = nv.y;
|
||||
results.add(tgt);
|
||||
}
|
||||
} else {
|
||||
// 兜底:查找与 current 相等的条目
|
||||
for (int idx : list) {
|
||||
float val = fullParam.keyframe().get(idx);
|
||||
if (Float.compare(val, current) == 0) {
|
||||
ParsedVertex pv = parseVertexValue(values.get(idx));
|
||||
if (pv != null) {
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid; tgt.deleted = pv.deleted; tgt.x = pv.x; tgt.y = pv.y;
|
||||
results.add(tgt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
} else if (prevIndex != -1) {
|
||||
target = toFloat(values.get(prevIndex));
|
||||
} else if (nextIndex != -1) {
|
||||
target = toFloat(values.get(nextIndex));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return results;
|
||||
out[0] = normalizeAnimAngleUnits(target);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 辅助结构
|
||||
private static class ParsedVertex {
|
||||
int id;
|
||||
// ---- [NEW] Deformation Vertex 插值 ----
|
||||
|
||||
// [MODIFIED] 辅助结构,现在使用顶点索引(idx)替代ID
|
||||
private static class DeformationVertexTarget {
|
||||
int idx;
|
||||
float x;
|
||||
float y;
|
||||
boolean deleted; // pos == null 表示删除
|
||||
ParsedVertex(int id, float x, float y, boolean deleted) { this.id = id; this.x = x; this.y = y; this.deleted = deleted; }
|
||||
boolean deleted = false; // pos == null 表示应取消变形标记
|
||||
}
|
||||
|
||||
private static ParsedVertex parseVertexValue(Object v) {
|
||||
if (v == null) return null;
|
||||
try {
|
||||
if (v instanceof Map) {
|
||||
Map<?,?> m = (Map<?,?>) v;
|
||||
Object idObj = m.get("id");
|
||||
// pos 可能为 null(表示删除)
|
||||
Object posObj = m.get("pos");
|
||||
if (idObj == null) return null;
|
||||
int id = (idObj instanceof Number) ? ((Number) idObj).intValue() : Integer.parseInt(String.valueOf(idObj));
|
||||
// [MODIFIED] 解析新的变形顶点数据格式 { "idx": int, "pos": [x, y] }
|
||||
private static DeformationVertexTarget parseDeformationVertexValue(Object v) {
|
||||
if (v instanceof Map) {
|
||||
Map<?,?> m = (Map<?,?>) v;
|
||||
Object idxObj = m.get("idx");
|
||||
Object posObj = m.get("pos");
|
||||
if (idxObj instanceof Number) {
|
||||
int idx = ((Number) idxObj).intValue();
|
||||
DeformationVertexTarget target = new DeformationVertexTarget();
|
||||
target.idx = idx;
|
||||
if (posObj == null) {
|
||||
return new ParsedVertex(id, 0f, 0f, true);
|
||||
target.deleted = true;
|
||||
} else {
|
||||
float[] p = readVec2(posObj);
|
||||
target.x = p[0];
|
||||
target.y = p[1];
|
||||
}
|
||||
float[] p = readVec2(posObj);
|
||||
return new ParsedVertex(id, p[0], p[1], false);
|
||||
} else if (v instanceof List) {
|
||||
List<?> l = (List<?>) v;
|
||||
if (l.size() >= 3) {
|
||||
Object idObj = l.get(0);
|
||||
if (!(idObj instanceof Number)) return null;
|
||||
int id = ((Number) idObj).intValue();
|
||||
float x = toFloat(l.get(1));
|
||||
float y = toFloat(l.get(2));
|
||||
return new ParsedVertex(id, x, y, false);
|
||||
} else if (l.size() == 2) {
|
||||
// [ id, null ] 之类(不常见)
|
||||
Object idObj = l.get(0);
|
||||
if (!(idObj instanceof Number)) return null;
|
||||
int id = ((Number) idObj).intValue();
|
||||
Object posObj = l.get(1);
|
||||
if (posObj == null) return new ParsedVertex(id, 0f, 0f, true);
|
||||
}
|
||||
} else {
|
||||
// 可能是自定义对象,尝试反射获取 id/pos(容错)
|
||||
try {
|
||||
Object idObj = v.getClass().getMethod("getId").invoke(v);
|
||||
Object px = v.getClass().getMethod("getX").invoke(v);
|
||||
Object py = v.getClass().getMethod("getY").invoke(v);
|
||||
int id = idObj instanceof Number ? ((Number) idObj).intValue() : Integer.parseInt(String.valueOf(idObj));
|
||||
return new ParsedVertex(id, toFloat(px), toFloat(py), false);
|
||||
} catch (Throwable ignored) {}
|
||||
return target;
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 用于输出 secondary vertex 结果
|
||||
private static class SecondaryVertexTarget {
|
||||
int id;
|
||||
float x;
|
||||
float y;
|
||||
boolean deleted = false;
|
||||
// [MODIFIED] 计算所有变形顶点的目标状态
|
||||
private static List<DeformationVertexTarget> computeAllDeformationVertexTargets(ParametersManagement.Parameter fullParam, String paramId, float current, AnimationParameter animParam) {
|
||||
List<DeformationVertexTarget> results = new ArrayList<>();
|
||||
List<Integer> allIndicesForParam = findIndicesForParam(fullParam, paramId, animParam);
|
||||
if (allIndicesForParam.isEmpty()) return results;
|
||||
|
||||
Map<Integer, List<Integer>> vertexIdxToParamIndices = new HashMap<>();
|
||||
for (int paramIdx : allIndicesForParam) {
|
||||
DeformationVertexTarget parsed = parseDeformationVertexValue(fullParam.value().get(paramIdx));
|
||||
if (parsed != null) {
|
||||
vertexIdxToParamIndices.computeIfAbsent(parsed.idx, k -> new ArrayList<>()).add(paramIdx);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<Integer, List<Integer>> entry : vertexIdxToParamIndices.entrySet()) {
|
||||
int vertexIdx = entry.getKey();
|
||||
List<Integer> paramIndices = entry.getValue();
|
||||
|
||||
int[] surrounding = findSurroundingKeyframesForIndices(fullParam.keyframe(), paramIndices, current);
|
||||
int prevParamIdx = surrounding[0], nextParamIdx = surrounding[1];
|
||||
|
||||
if (prevParamIdx != -1 && nextParamIdx != -1) {
|
||||
DeformationVertexTarget prevTarget = parseDeformationVertexValue(fullParam.value().get(prevParamIdx));
|
||||
DeformationVertexTarget nextTarget = parseDeformationVertexValue(fullParam.value().get(nextParamIdx));
|
||||
|
||||
if (prevTarget == null || nextTarget == null) continue;
|
||||
|
||||
if (prevParamIdx == nextParamIdx) {
|
||||
results.add(prevTarget);
|
||||
} else {
|
||||
// 如果任意一侧被删除,则不进行插值,而是取前一个关键帧的状态
|
||||
if (prevTarget.deleted || nextTarget.deleted) {
|
||||
results.add(prevTarget);
|
||||
} else {
|
||||
float t = computeT(fullParam.keyframe().get(prevParamIdx), fullParam.keyframe().get(nextParamIdx), current);
|
||||
DeformationVertexTarget interpolated = new DeformationVertexTarget();
|
||||
interpolated.idx = vertexIdx;
|
||||
interpolated.x = prevTarget.x + t * (nextTarget.x - prevTarget.x);
|
||||
interpolated.y = prevTarget.y + t * (nextTarget.y - prevTarget.y);
|
||||
results.add(interpolated);
|
||||
}
|
||||
}
|
||||
} else if (prevParamIdx != -1) {
|
||||
DeformationVertexTarget target = parseDeformationVertexValue(fullParam.value().get(prevParamIdx));
|
||||
if (target != null) results.add(target);
|
||||
} else if (nextParamIdx != -1) {
|
||||
DeformationVertexTarget target = parseDeformationVertexValue(fullParam.value().get(nextParamIdx));
|
||||
if (target != null) results.add(target);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SelectionTool 的四类操作(pivot/scale/rotate/position/secondaryVertex)按当前关键帧插值并应用到 parts。
|
||||
* 该方法应在 GL 上下文线程中被调用(即 glContextManager.executeInGLContext 内)。
|
||||
*
|
||||
* 对每个 part:先计算所有目标(如果存在),再一次性按 pivot->scale->rotation->position 的顺序应用,
|
||||
* 并在最后只做一次 updateMeshVertices/updateBounds。
|
||||
* 将变换操作按当前关键帧插值并应用到 parts。
|
||||
* 应在 GL 上下文线程中调用。
|
||||
*/
|
||||
public static void applyFrameInterpolations(ParametersManagement pm,
|
||||
List<ModelPart> parts,
|
||||
AnimationParameter currentAnimationParameter,
|
||||
Logger logger) {
|
||||
public static void applyFrameInterpolations(ParametersManagement pm, List<ModelPart> parts, AnimationParameter currentAnimationParameter, Logger logger) {
|
||||
if (pm == null || parts == null || parts.isEmpty() || currentAnimationParameter == null) return;
|
||||
float current = 0f;
|
||||
try {
|
||||
Object v = currentAnimationParameter.getValue();
|
||||
current = toFloat(v);
|
||||
} catch (Exception ex) {
|
||||
logger.debug("读取当前动画参数值失败,使用0作为默认值", ex);
|
||||
}
|
||||
|
||||
float current = toFloat(currentAnimationParameter.getValue());
|
||||
|
||||
for (ModelPart part : parts) {
|
||||
if (!pm.getParametersPanel().getSelectParameter().equals(currentAnimationParameter)) continue;
|
||||
|
||||
ParametersManagement.Parameter fullParam = pm.getModelPartParameters(part);
|
||||
if (fullParam == null) continue;
|
||||
|
||||
float[] targetPivot = null, targetScale = null, targetPosition = null;
|
||||
Float targetRotation = null;
|
||||
// [MODIFIED] 使用新的类型和名称
|
||||
List<DeformationVertexTarget> dvTargets = null;
|
||||
|
||||
float[] tmp2 = new float[2];
|
||||
if (computeVec2Target(fullParam, "pivot", current, tmp2, currentAnimationParameter)) targetPivot = tmp2.clone();
|
||||
if (computeVec2Target(fullParam, "scale", current, tmp2, currentAnimationParameter)) targetScale = tmp2.clone();
|
||||
if (computeVec2Target(fullParam, "position", current, tmp2, currentAnimationParameter)) targetPosition = tmp2.clone();
|
||||
|
||||
float[] tmp1 = new float[1];
|
||||
if (computeRotationTarget(fullParam, "rotate", current, tmp1, currentAnimationParameter)) targetRotation = tmp1[0];
|
||||
|
||||
// [MODIFIED] 计算可变形顶点的目标
|
||||
dvTargets = computeAllDeformationVertexTargets(fullParam, "deformationVertex", current, currentAnimationParameter);
|
||||
|
||||
if (targetPivot == null && targetScale == null && targetRotation == null && targetPosition == null && (dvTargets == null || dvTargets.isEmpty())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!pm.getParametersPanel().getSelectParameter().equals(currentAnimationParameter)){
|
||||
return;
|
||||
}
|
||||
// Full parameter record for this ModelPart (contains lists for all paramIds)
|
||||
ParametersManagement.Parameter fullParam = pm.getModelPartParameters(part);
|
||||
if (fullParam == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 目标容器(null 表示未设置)
|
||||
float[] targetPivot = null;
|
||||
float[] targetScale = null;
|
||||
Float targetRotation = null;
|
||||
float[] targetPosition = null;
|
||||
List<SecondaryVertexTarget> svTargets = null;
|
||||
|
||||
// pivot
|
||||
float[] tmp2 = new float[2];
|
||||
// (MODIFIED) Pass currentAnimationParameter
|
||||
if (computeVec2Target(fullParam, "pivot", current, tmp2, currentAnimationParameter)) {
|
||||
targetPivot = new float[]{tmp2[0], tmp2[1]};
|
||||
}
|
||||
|
||||
// scale
|
||||
// (MODIFIED) Pass currentAnimationParameter
|
||||
if (computeVec2Target(fullParam, "scale", current, tmp2, currentAnimationParameter)) {
|
||||
targetScale = new float[]{tmp2[0], tmp2[1]};
|
||||
}
|
||||
|
||||
// rotate
|
||||
float[] tmp1 = new float[1];
|
||||
// (MODIFIED) Pass currentAnimationParameter
|
||||
if (computeRotationTargetGeneric(fullParam, "rotate", current, tmp1, currentAnimationParameter)) {
|
||||
targetRotation = tmp1[0];
|
||||
}
|
||||
|
||||
// position
|
||||
// (MODIFIED) Pass currentAnimationParameter
|
||||
if (computeVec2Target(fullParam, "position", current, tmp2, currentAnimationParameter)) {
|
||||
targetPosition = new float[]{tmp2[0], tmp2[1]};
|
||||
}
|
||||
|
||||
// secondaryVertex: 为每个记录的 vertex id 计算目标位置(包含实时广播)
|
||||
// (MODIFIED) Pass currentAnimationParameter
|
||||
List<SecondaryVertexTarget> computedSV = computeAllSecondaryVertexTargets(fullParam, "secondaryVertex", current, currentAnimationParameter);
|
||||
if (computedSV != null && !computedSV.isEmpty()) {
|
||||
svTargets = computedSV;
|
||||
}
|
||||
|
||||
// 如果没有任何要修改的,跳过
|
||||
if (targetPivot == null && targetScale == null && targetRotation == null && targetPosition == null && (svTargets == null || svTargets.isEmpty())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 记录当前状态(用于没有 setRotation 方法时计算 delta)
|
||||
float currentRot = part.getRotation();
|
||||
|
||||
// 一次性应用:pivot -> scale -> rotation -> position(最小化中间更新)
|
||||
try {
|
||||
if (targetPivot != null) {
|
||||
part.setPivot(targetPivot[0], targetPivot[1]);
|
||||
if (targetPivot != null) part.setPivot(targetPivot[0], targetPivot[1]);
|
||||
if (targetScale != null) part.setScale(targetScale[0], targetScale[1]);
|
||||
if (targetPosition != null) part.setPosition(targetPosition[0], targetPosition[1]);
|
||||
if (targetRotation != null) {
|
||||
try {
|
||||
part.getClass().getMethod("setRotation", float.class).invoke(part, targetRotation);
|
||||
} catch (NoSuchMethodException e) {
|
||||
part.rotate(normalizeAngle(targetRotation - part.getRotation()));
|
||||
}
|
||||
}
|
||||
|
||||
if (targetScale != null) {
|
||||
part.setScale(targetScale[0], targetScale[1]);
|
||||
}
|
||||
|
||||
if (targetRotation != null) {
|
||||
try {
|
||||
Method setRotation = part.getClass().getMethod("setRotation", float.class);
|
||||
setRotation.invoke(part, targetRotation);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
float delta = normalizeAngle(targetRotation - currentRot);
|
||||
part.rotate(delta);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
if (targetPosition != null) {
|
||||
part.setPosition(targetPosition[0], targetPosition[1]);
|
||||
}
|
||||
|
||||
// 处理 secondary vertex:对每个计算出的目标,找到对应 mesh 与 vertex id,设置顶点位置或删除
|
||||
if (svTargets != null && !svTargets.isEmpty()) {
|
||||
try {
|
||||
List<com.chuangzhou.vivid2D.render.model.util.Mesh2D> meshes = part.getMeshes();
|
||||
if (meshes != null) {
|
||||
for (SecondaryVertexTarget s : svTargets) {
|
||||
boolean appliedGlobal = false;
|
||||
for (com.chuangzhou.vivid2D.render.model.util.Mesh2D mesh : meshes) {
|
||||
if (mesh == null) continue;
|
||||
try {
|
||||
boolean applied = false;
|
||||
// 优先尝试 mesh.getSecondaryVertices()
|
||||
try {
|
||||
List<com.chuangzhou.vivid2D.render.model.util.SecondaryVertex> svs =
|
||||
(List<com.chuangzhou.vivid2D.render.model.util.SecondaryVertex>) mesh.getClass().getMethod("getSecondaryVertices").invoke(mesh);
|
||||
if (svs != null) {
|
||||
com.chuangzhou.vivid2D.render.model.util.SecondaryVertex found = null;
|
||||
for (com.chuangzhou.vivid2D.render.model.util.SecondaryVertex sv : svs) {
|
||||
if (sv != null && sv.getId() == s.id) {
|
||||
found = sv;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found != null) {
|
||||
if (s.deleted) {
|
||||
// 尝试通过 mesh 提供的 remove 方法
|
||||
try {
|
||||
mesh.getClass().getMethod("removeSecondaryVertex", com.chuangzhou.vivid2D.render.model.util.SecondaryVertex.class)
|
||||
.invoke(mesh, found);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
// 没有 remove 方法则尝试 mesh.removeSecondaryVertex(found) via known signature
|
||||
try {
|
||||
mesh.getClass().getMethod("removeSecondaryVertexById", int.class).invoke(mesh, s.id);
|
||||
} catch (Throwable ignore) {
|
||||
// 最后作为保险:尝试直接从 list 删除(不推荐,但做容错)
|
||||
try {
|
||||
svs.remove(found);
|
||||
} catch (Throwable ignore2) {}
|
||||
}
|
||||
} catch (Throwable ignore) {}
|
||||
} else {
|
||||
found.setPosition(s.x, s.y);
|
||||
}
|
||||
applied = true;
|
||||
}
|
||||
}
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
// 忽略:没有该方法
|
||||
} catch (Throwable ignore2) {
|
||||
// 忽略运行时异常
|
||||
}
|
||||
|
||||
// 回退策略:尝试 getSecondaryVertexById(int)
|
||||
if (!applied) {
|
||||
try {
|
||||
Object svObj = mesh.getClass().getMethod("getSecondaryVertexById", int.class).invoke(mesh, s.id);
|
||||
if (svObj instanceof com.chuangzhou.vivid2D.render.model.util.SecondaryVertex) {
|
||||
com.chuangzhou.vivid2D.render.model.util.SecondaryVertex found = (com.chuangzhou.vivid2D.render.model.util.SecondaryVertex) svObj;
|
||||
if (s.deleted) {
|
||||
try {
|
||||
mesh.getClass().getMethod("removeSecondaryVertex", com.chuangzhou.vivid2D.render.model.util.SecondaryVertex.class)
|
||||
.invoke(mesh, found);
|
||||
} catch (Throwable ignore) {}
|
||||
} else {
|
||||
found.setPosition(s.x, s.y);
|
||||
}
|
||||
applied = true;
|
||||
}
|
||||
} catch (NoSuchMethodException ignoreMethod) {
|
||||
// 忽略
|
||||
} catch (Throwable ignore3) {}
|
||||
}
|
||||
|
||||
if (applied) {
|
||||
appliedGlobal = true;
|
||||
break;
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
// [MODIFIED] 应用变形顶点更改
|
||||
if (dvTargets != null && !dvTargets.isEmpty()) {
|
||||
for (Mesh2D mesh : part.getMeshes()) {
|
||||
if (mesh == null) continue;
|
||||
boolean meshModified = false;
|
||||
for (DeformationVertexTarget target : dvTargets) {
|
||||
if (target.idx >= 0 && target.idx < mesh.getVertexCount()) {
|
||||
Vertex vertex = mesh.getVertexInstance(target.idx);
|
||||
if (target.deleted) {
|
||||
// "删除"操作意味着取消其变形资格
|
||||
if (vertex.getTag() == VertexTag.DEFORMATION) {
|
||||
vertex.setTag(VertexTag.DEFAULT);
|
||||
meshModified = true;
|
||||
}
|
||||
// 未找到对应 id 的 mesh/vertex 则忽略(容错)
|
||||
if (!appliedGlobal) {
|
||||
// 可选: 记录日志方便调试
|
||||
logger.debug("未找到二级顶点 id={} 的对应 mesh/vertex", s.id);
|
||||
} else {
|
||||
// 更新位置并确保其为可变形顶点
|
||||
vertex.position.set(target.x, target.y);
|
||||
if (vertex.getTag() != VertexTag.DEFORMATION) {
|
||||
vertex.setTag(VertexTag.DEFORMATION);
|
||||
}
|
||||
meshModified = true;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
|
||||
// 最后统一刷新一次顶点与 bounds,避免频繁刷新导致与交互冲突
|
||||
try {
|
||||
part.updateMeshVertices();
|
||||
if (part.getMeshes() != null) {
|
||||
for (Object m : part.getMeshes()) {
|
||||
try {
|
||||
if (m instanceof com.chuangzhou.vivid2D.render.model.util.Mesh2D) {
|
||||
((com.chuangzhou.vivid2D.render.model.util.Mesh2D) m).updateBounds();
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
|
||||
} catch (Throwable t) {
|
||||
logger.debug("应用目标变换时失败(single part): {}", t.getMessage());
|
||||
if (meshModified) {
|
||||
// 如果顶点数据被修改,需要标记mesh为dirty
|
||||
mesh.markDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception ex) {
|
||||
logger.error("FrameInterpolator 在应用插值时发生异常", ex);
|
||||
// 统一刷新
|
||||
part.updateMeshVertices();
|
||||
for (Mesh2D mesh : part.getMeshes()) {
|
||||
if (mesh != null) mesh.updateBounds();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("在对部件 '{}' 应用插值时发生异常", part.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||
import org.joml.Vector2f;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import org.joml.Vector2f;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -2,14 +2,12 @@ package com.chuangzhou.vivid2D.render.awt.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.util.PsdParser;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
|
||||
1843
src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java
Normal file
1843
src/main/java/com/chuangzhou/vivid2D/render/model/Mesh2D.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
package com.chuangzhou.vivid2D.render.model;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData;
|
||||
import com.chuangzhou.vivid2D.render.model.data.ModelData;
|
||||
import com.chuangzhou.vivid2D.render.model.data.ModelMetadata;
|
||||
import com.chuangzhou.vivid2D.render.model.util.*;
|
||||
@@ -8,8 +7,6 @@ import com.chuangzhou.vivid2D.util.ModelDataJsonConverter;
|
||||
import org.joml.Matrix3f;
|
||||
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package com.chuangzhou.vivid2D.render.model;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal;
|
||||
import com.chuangzhou.vivid2D.render.model.util.*;
|
||||
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Deformer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Vertex;
|
||||
import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Vector2f;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
|
||||
/**
|
||||
* 2D模型部件,支持层级变换和变形器
|
||||
@@ -44,15 +45,16 @@ public class ModelPart {
|
||||
// ==================== 变形系统 ====================
|
||||
private final List<Deformer> deformers;
|
||||
private final List<LiquifyStroke> liquifyStrokes = new ArrayList<>();
|
||||
private final Map<String, AnimationParameter> parameters;
|
||||
|
||||
// ==================== 状态标记 ====================
|
||||
private boolean transformDirty;
|
||||
private boolean boundsDirty;
|
||||
private boolean pivotInitialized;
|
||||
|
||||
private final List<ModelEvent> events = new CopyOnWriteArrayList<>();
|
||||
private final List<ModelEvent> events = new LinkedList<>();
|
||||
private boolean inMultiSelectionOperation = false;
|
||||
private boolean startLiquefy =false;
|
||||
private final Map<String, AnimationParameter> parameters = new LinkedHashMap<>();
|
||||
|
||||
// ====== 液化模式枚举 ======
|
||||
public enum LiquifyMode {
|
||||
@@ -66,31 +68,6 @@ public class ModelPart {
|
||||
TURBULENCE // 湍流(噪声扰动)
|
||||
}
|
||||
|
||||
public AnimationParameter createParameter(String id, float min, float max, float defaultValue) {
|
||||
AnimationParameter param = new AnimationParameter(id, min, max, defaultValue);
|
||||
parameters.put(id, param);
|
||||
return param;
|
||||
}
|
||||
|
||||
public AnimationParameter getParameter(String id) {
|
||||
return parameters.get(id);
|
||||
}
|
||||
|
||||
public Map<String, AnimationParameter> getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public void addParameter(AnimationParameter param) {
|
||||
parameters.put(param.getId(), param);
|
||||
}
|
||||
|
||||
public void setParameterValue(String paramId, float value) {
|
||||
AnimationParameter param = parameters.get(paramId);
|
||||
if (param != null) {
|
||||
param.setValue(value);
|
||||
markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
|
||||
@@ -103,18 +80,25 @@ public class ModelPart {
|
||||
this.children = new ArrayList<>();
|
||||
this.meshes = new ArrayList<>();
|
||||
this.deformers = new ArrayList<>();
|
||||
|
||||
// 初始化变换属性
|
||||
this.position = new Vector2f();
|
||||
this.rotation = 0.0f;
|
||||
this.scale = new Vector2f(1.0f, 1.0f);
|
||||
this.localTransform = new Matrix3f();
|
||||
this.worldTransform = new Matrix3f();
|
||||
|
||||
// 初始化渲染属性
|
||||
this.visible = true;
|
||||
this.blendMode = BlendMode.NORMAL;
|
||||
this.opacity = 1.0f;
|
||||
parameters = new HashMap<>();
|
||||
|
||||
// 标记需要更新
|
||||
this.transformDirty = true;
|
||||
this.boundsDirty = true;
|
||||
|
||||
updateLocalTransform();
|
||||
// 初始时 worldTransform = localTransform(无父节点时)
|
||||
recomputeWorldTransformRecursive();
|
||||
}
|
||||
|
||||
@@ -128,7 +112,7 @@ public class ModelPart {
|
||||
|
||||
private void triggerEvent(String eventName) {
|
||||
for (ModelEvent event : events) {
|
||||
event.trigger(eventName, this);
|
||||
event.trigger(eventName,this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +120,7 @@ public class ModelPart {
|
||||
* 设置液化状态
|
||||
*/
|
||||
public void setStartLiquefy(boolean startLiquefy) {
|
||||
this.startLiquefy = startLiquefy;
|
||||
|
||||
// 同步到所有网格
|
||||
for (Mesh2D mesh : meshes) {
|
||||
@@ -424,7 +409,6 @@ public class ModelPart {
|
||||
child.markTransformDirty();
|
||||
// 确保子节点的 worldTransform 立即更新
|
||||
child.recomputeWorldTransformRecursive();
|
||||
triggerEvent("children");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -437,7 +421,6 @@ public class ModelPart {
|
||||
child.markTransformDirty();
|
||||
child.recomputeWorldTransformRecursive();
|
||||
}
|
||||
triggerEvent("children");
|
||||
return removed;
|
||||
}
|
||||
|
||||
@@ -544,20 +527,20 @@ public class ModelPart {
|
||||
if (stroke == null || stroke.points == null) return;
|
||||
LiquifyMode mode = stroke.mode != null ? stroke.mode : LiquifyMode.PUSH;
|
||||
for (LiquifyPoint p : stroke.points) {
|
||||
applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, mode, stroke.iterations, true);
|
||||
applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, mode, stroke.iterations,true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对当前部件下所有网格应用液化笔效果(类似 Photoshop 的液化工具)。
|
||||
* 请在使用前注册在使用addLiquifyStroke方法注册LiquifyStroke
|
||||
* <p>
|
||||
*
|
||||
* 注意:
|
||||
* - brushCenter 使用世界坐标(与 ModelPart 的世界坐标体系一致)。
|
||||
* - radius 为画笔半径(像素),strength 为强度(建议范围 0.0 - 1.0,数值越大效果越强)。
|
||||
* - mode 选择液化操作类型。
|
||||
* - iterations 为迭代次数(>0),可用来让效果更平滑,默认 1 次即可。
|
||||
* <p>
|
||||
*
|
||||
* 该方法会直接修改 mesh 的顶点并更新其边界(mesh.updateBounds)。
|
||||
*/
|
||||
public void applyLiquify(Vector2f brushCenter, float radius, float strength, LiquifyMode mode, int iterations, boolean createVertices) {
|
||||
@@ -945,7 +928,7 @@ public class ModelPart {
|
||||
float brushArea = brushAreaWidth * brushAreaHeight;
|
||||
|
||||
// 根据画笔大小和网格复杂度计算顶点密度
|
||||
int targetVertexCount = Math.max(4, Math.min(20, (int) (brushArea / (localRadius * localRadius * 0.5f))));
|
||||
int targetVertexCount = Math.max(4, Math.min(20, (int)(brushArea / (localRadius * localRadius * 0.5f))));
|
||||
|
||||
// 在画笔区域内均匀添加顶点
|
||||
boolean addedVertices = addUniformVerticesInArea(mesh, newVertices, newUVs, newIndices,
|
||||
@@ -1569,9 +1552,7 @@ public class ModelPart {
|
||||
public void setPosition(float x, float y) {
|
||||
// 防止递归调用
|
||||
if (inMultiSelectionOperation) {
|
||||
float deltaX = x - position.x;
|
||||
float deltaY = y - position.y;
|
||||
|
||||
// 直接执行单选择辑,避免递归
|
||||
position.set(x, y);
|
||||
markTransformDirty();
|
||||
updateLocalTransform();
|
||||
@@ -1582,42 +1563,28 @@ public class ModelPart {
|
||||
mesh.setPivot(worldPivot.x, worldPivot.y);
|
||||
}
|
||||
|
||||
syncSecondaryVerticesForPart(this, deltaX, deltaY);
|
||||
updateMeshVertices();
|
||||
triggerEvent("position");
|
||||
return;
|
||||
}
|
||||
|
||||
float oldX = position.x;
|
||||
float oldY = position.y;
|
||||
|
||||
// 多选状态下移动
|
||||
// 如果是多选状态下的移动,使用多选移动方法
|
||||
if (isInMultiSelection() && !getSelectedMeshes().isEmpty()) {
|
||||
float dx = x - oldX;
|
||||
float dy = y - oldY;
|
||||
Vector2f currentPos = getPosition();
|
||||
float dx = x - currentPos.x;
|
||||
float dy = y - currentPos.y;
|
||||
|
||||
// 设置标志防止递归
|
||||
inMultiSelectionOperation = true;
|
||||
try {
|
||||
moveSelectedMeshes(dx, dy); // 这里会调用 setPosition,但被 inMultiSelectionOperation 拦截
|
||||
moveSelectedMeshes(dx, dy);
|
||||
} finally {
|
||||
inMultiSelectionOperation = false;
|
||||
}
|
||||
|
||||
// 更新自身位置
|
||||
position.set(x, y);
|
||||
markTransformDirty();
|
||||
updateLocalTransform();
|
||||
recomputeWorldTransformRecursive();
|
||||
|
||||
for (Mesh2D mesh : meshes) {
|
||||
Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, mesh.getOriginalPivot());
|
||||
mesh.setPivot(worldPivot.x, worldPivot.y);
|
||||
}
|
||||
|
||||
triggerEvent("position");
|
||||
return;
|
||||
}
|
||||
|
||||
// 单选逻辑
|
||||
// 原有单选择辑
|
||||
position.set(x, y);
|
||||
markTransformDirty();
|
||||
updateLocalTransform();
|
||||
@@ -1628,157 +1595,44 @@ public class ModelPart {
|
||||
mesh.setPivot(worldPivot.x, worldPivot.y);
|
||||
}
|
||||
|
||||
float deltaX = x - oldX;
|
||||
float deltaY = y - oldY;
|
||||
|
||||
syncSecondaryVerticesForPart(this, deltaX, deltaY);
|
||||
updateMeshVertices();
|
||||
triggerEvent("position");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 同步部件下所有网格的二级顶点位置
|
||||
*/
|
||||
private void syncSecondaryVerticesForPart(ModelPart part, float deltaX, float deltaY) {
|
||||
if (part == null) return;
|
||||
|
||||
List<Mesh2D> meshes = part.getMeshes();
|
||||
if (meshes == null) return;
|
||||
|
||||
for (Mesh2D mesh : meshes) {
|
||||
if (mesh != null && mesh.isVisible() && mesh.getSecondaryVertexCount() > 0) {
|
||||
|
||||
List<SecondaryVertex> secondaryVertices = mesh.getSecondaryVertices();
|
||||
if (secondaryVertices != null) {
|
||||
|
||||
// 遍历所有顶点,逐个调用 moveSecondaryVertex
|
||||
for (SecondaryVertex vertex : secondaryVertices) {
|
||||
|
||||
// 【修正 1:避免双重平移和状态冲突】
|
||||
// 仅对未锁定/未固定的顶点执行局部坐标平移。
|
||||
// 锁定的顶点不应被工具的同步逻辑移动,它们应该随 ModelPart 的世界变换移动。
|
||||
if (!vertex.isLocked() && !vertex.isPinned()) {
|
||||
|
||||
// 计算顶点的新局部坐标 (position + delta)
|
||||
float newX = vertex.getPosition().x + deltaX;
|
||||
float newY = vertex.getPosition().y + deltaY;
|
||||
|
||||
// 使用 moveSecondaryVertex 方法
|
||||
mesh.moveSecondaryVertex(vertex, newX, newY);
|
||||
// 注意:mesh.moveSecondaryVertex 内部会触发形变计算和 markDirty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
part.setPosition(part.getPosition());
|
||||
|
||||
// 递归处理子部件
|
||||
for (ModelPart child : part.getChildren()) {
|
||||
syncSecondaryVerticesForPart(child, deltaX, deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有网格的顶点位置以反映当前变换
|
||||
*/
|
||||
public void updateMeshVertices() {
|
||||
// 确保世界变换是最新的
|
||||
if (transformDirty) {
|
||||
updateLocalTransform();
|
||||
recomputeWorldTransformRecursive();
|
||||
}
|
||||
|
||||
// 对每个网格应用当前的世界变换
|
||||
recomputeWorldTransformRecursive();
|
||||
for (Mesh2D mesh : meshes) {
|
||||
updateMeshVertices(mesh);
|
||||
if (mesh == null) continue;
|
||||
for (Vertex vertex : mesh.getActiveVertexList()) {
|
||||
Vector2f localPoint = vertex.originalPosition;
|
||||
Vector2f worldPoint = Matrix3fUtils.transformPoint(this.worldTransform, localPoint);
|
||||
vertex.position.set(worldPoint);
|
||||
}
|
||||
mesh.applyDeformation();
|
||||
mesh.markDirty();
|
||||
}
|
||||
|
||||
// 递归更新子部件的网格
|
||||
for (ModelPart child : children) {
|
||||
child.updateMeshVertices();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 强制更新单个网格的顶点位置
|
||||
*/
|
||||
private void updateMeshVertices(Mesh2D mesh) {
|
||||
if (mesh == null) return;
|
||||
// 确保 worldTransform 是最新的
|
||||
if (transformDirty) {
|
||||
updateLocalTransform();
|
||||
recomputeWorldTransformRecursive();
|
||||
|
||||
// 获取原始顶点数据(局部坐标)
|
||||
float[] originalVertices = mesh.getOriginalVertices();
|
||||
if (originalVertices == null || originalVertices.length == 0) {
|
||||
logger.warn("网格 {} 没有原始顶点数据,无法更新变换", mesh.getName());
|
||||
return;
|
||||
}
|
||||
// 1) 让 mesh 自己把局部顶点一次性转换成渲染缓存(世界坐标)
|
||||
mesh.syncRenderVerticesFromLocal(this.worldTransform);
|
||||
// 2) 同步 pivot(不改原始局部数据)
|
||||
try {
|
||||
Vector2f origPivot = mesh.getOriginalPivot();
|
||||
if (origPivot != null) {
|
||||
Vector2f worldPivot = Matrix3fUtils.transformPoint(this.worldTransform, origPivot);
|
||||
mesh.setPivot(worldPivot.x, worldPivot.y);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("更新网格pivot时出错: {}", e.getMessage());
|
||||
}
|
||||
// 3) 更新木偶控制点显示位置(仅 display/world pos)
|
||||
//updatePuppetPinsPosition(mesh);
|
||||
// 4) 更新二级顶点的 worldPosition 缓存(仅 display/world pos,不修改局部变形数据)
|
||||
updateSecondaryVerticesWorldPosition(mesh);
|
||||
// 5) 标记 mesh 需要重新渲染(渲染器应使用 mesh.getVerticesForUpload() 来上传 VBO)
|
||||
mesh.markDirty();
|
||||
mesh.setBakedToWorld(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新二级顶点的原始局部位置与当前局部位置(当 ModelPart 的 worldTransform 改变时调用)
|
||||
* 保证 SecondaryVertex 在变换后仍然用局部坐标表示(用于变形计算),同时更新 worldPosition 缓存用于显示。
|
||||
*/
|
||||
private void updateSecondaryVerticesWorldPosition(Mesh2D mesh) {
|
||||
if (mesh == null) return;
|
||||
List<SecondaryVertex> secondaryVertices = mesh.getSecondaryVertices();
|
||||
if (secondaryVertices == null || secondaryVertices.isEmpty()) return;
|
||||
if (transformDirty) {
|
||||
updateLocalTransform();
|
||||
recomputeWorldTransformRecursive();
|
||||
}
|
||||
boolean hasMirror = hasMirrorTransform(this.worldTransform);
|
||||
for (SecondaryVertex vertex : secondaryVertices) {
|
||||
Vector2f localPos = vertex.getPosition();
|
||||
Vector2f adjustedPos = localPos;
|
||||
if (hasMirror) {
|
||||
adjustedPos = new Vector2f(-localPos.x, localPos.y);
|
||||
}
|
||||
Vector2f worldPos = Matrix3fUtils.transformPoint(this.worldTransform, adjustedPos);
|
||||
vertex.setWorldPosition(worldPos);
|
||||
vertex.setRenderPosition(worldPos.x, worldPos.y);
|
||||
}
|
||||
|
||||
logger.debug("更新了 {} 个二级顶点的位置(处理镜像:{})",
|
||||
secondaryVertices.size(), hasMirror);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查变换矩阵是否包含镜像(负缩放)
|
||||
*/
|
||||
private boolean hasMirrorTransform(Matrix3f transform) {
|
||||
// 检查X轴缩放因子的符号
|
||||
float scaleX = (float)Math.sqrt(transform.m00 * transform.m00 + transform.m10 * transform.m10);
|
||||
|
||||
// 通过行列式检查镜像
|
||||
float determinant = transform.m00 * transform.m11 - transform.m01 * transform.m10;
|
||||
return determinant < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新木偶控制点的位置
|
||||
*/
|
||||
private void updatePuppetPinsPosition(Mesh2D mesh) {
|
||||
if (mesh == null) return;
|
||||
|
||||
List<PuppetPin> puppetPins = mesh.getPuppetPins();
|
||||
if (puppetPins.isEmpty()) return;
|
||||
|
||||
// 确保世界变换是最新的
|
||||
if (transformDirty) {
|
||||
@@ -1786,21 +1640,38 @@ public class ModelPart {
|
||||
recomputeWorldTransformRecursive();
|
||||
}
|
||||
|
||||
for (PuppetPin pin : puppetPins) {
|
||||
// 获取控制点的原始局部位置(保持不变!)
|
||||
Vector2f originalLocalPos = pin.getOriginalPosition();
|
||||
int vertexCount = originalVertices.length / 2;
|
||||
|
||||
// 将原始局部位置变换到世界坐标(用于显示)
|
||||
Vector2f worldPos = Matrix3fUtils.transformPoint(worldTransform, originalLocalPos);
|
||||
// 应用当前世界变换到每个顶点 - 添加边界检查
|
||||
for (int i = 0; i < vertexCount; i++) {
|
||||
if (i * 2 + 1 >= originalVertices.length) {
|
||||
logger.warn("顶点索引 {} 超出原始顶点数组范围", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 只更新控制点的显示位置,保持原始局部位置不变
|
||||
pin.setPosition(worldPos.x, worldPos.y);
|
||||
Vector2f localPoint = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]);
|
||||
Vector2f worldPoint = Matrix3fUtils.transformPoint(worldTransform, localPoint);
|
||||
|
||||
logger.trace("更新木偶控制点显示位置: 局部({}, {}) -> 世界({}, {})",
|
||||
originalLocalPos.x, originalLocalPos.y, worldPos.x, worldPos.y);
|
||||
// 检查目标索引是否有效
|
||||
if (i < mesh.getVertexCount()) {
|
||||
mesh.setVertex(i, worldPoint.x, worldPoint.y);
|
||||
} else {
|
||||
logger.warn("顶点索引 {} 超出网格顶点范围 (总顶点数: {})", i, mesh.getVertexCount());
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("同步更新了 {} 个木偶控制点的显示位置", puppetPins.size());
|
||||
// 同步 mesh 的原始局部 pivot -> 当前世界 pivot
|
||||
try {
|
||||
Vector2f origPivot = mesh.getOriginalPivot();
|
||||
Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, origPivot);
|
||||
mesh.setPivot(worldPivot.x, worldPivot.y);
|
||||
} catch (Exception e) {
|
||||
logger.warn("更新网格pivot时出错: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 标记网格需要更新
|
||||
mesh.markDirty();
|
||||
mesh.setBakedToWorld(true);
|
||||
}
|
||||
|
||||
public void setPosition(Vector2f pos) {
|
||||
@@ -1828,6 +1699,7 @@ public class ModelPart {
|
||||
Vector2f movedWorldPivot = new Vector2f(oldWorldPivot.x + dx, oldWorldPivot.y + dy);
|
||||
// 将位移后的世界 pivot 逆变换回新的局部坐标系(即新的 originalPivot)
|
||||
Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, movedWorldPivot);
|
||||
|
||||
mesh.setOriginalPivot(newLocalOriginalPivot);
|
||||
mesh.setPivot(movedWorldPivot.x, movedWorldPivot.y);
|
||||
}
|
||||
@@ -1836,37 +1708,6 @@ public class ModelPart {
|
||||
updateMeshVertices();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新木偶控制点的原始位置
|
||||
*/
|
||||
private void updatePuppetPinsOriginalPosition(Mesh2D mesh, Matrix3f oldWorldTransform, float dx, float dy) {
|
||||
if (mesh == null) return;
|
||||
|
||||
List<PuppetPin> puppetPins = mesh.getPuppetPins();
|
||||
if (puppetPins.isEmpty()) return;
|
||||
|
||||
for (PuppetPin pin : puppetPins) {
|
||||
// 获取控制点的当前原始位置(局部坐标)
|
||||
Vector2f currentOriginalPos = pin.getOriginalPosition();
|
||||
|
||||
// 将原始位置变换到旧的世界坐标系
|
||||
Vector2f oldWorldPos = Matrix3fUtils.transformPoint(oldWorldTransform, currentOriginalPos);
|
||||
|
||||
// 在世界坐标系中应用位移
|
||||
Vector2f newWorldPos = new Vector2f(oldWorldPos.x + dx, oldWorldPos.y + dy);
|
||||
|
||||
// 将新的世界位置逆变换回新的局部坐标系
|
||||
Vector2f newLocalOriginalPos = Matrix3fUtils.transformPointInverse(this.worldTransform, newWorldPos);
|
||||
|
||||
// 更新控制点的原始位置
|
||||
pin.setOriginalPosition(newLocalOriginalPos.x, newLocalOriginalPos.y);
|
||||
|
||||
logger.trace("更新木偶控制点原始位置: 旧局部({}, {}) -> 新局部({}, {})",
|
||||
currentOriginalPos.x, currentOriginalPos.y, newLocalOriginalPos.x, newLocalOriginalPos.y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 移动部件
|
||||
*/
|
||||
@@ -1896,7 +1737,7 @@ public class ModelPart {
|
||||
return;
|
||||
}
|
||||
|
||||
// 原有单选择辑 - 修复:确保网格顶点被更新
|
||||
// 原有单选择辑
|
||||
Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform);
|
||||
this.rotation = radians;
|
||||
markTransformDirty();
|
||||
@@ -1908,10 +1749,6 @@ public class ModelPart {
|
||||
Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot);
|
||||
mesh.setOriginalPivot(newLocalOriginalPivot);
|
||||
mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot));
|
||||
|
||||
// 更新木偶控制点和二级顶点
|
||||
updatePuppetPinsOriginalPositionForTransform(mesh, oldWorldTransform);
|
||||
updateSecondaryVerticesWorldPosition(mesh);
|
||||
}
|
||||
|
||||
updateMeshVertices();
|
||||
@@ -1923,16 +1760,9 @@ public class ModelPart {
|
||||
*/
|
||||
public void rotate(float deltaRadians) {
|
||||
this.rotation += deltaRadians;
|
||||
Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform);
|
||||
markTransformDirty();
|
||||
updateLocalTransform();
|
||||
recomputeWorldTransformRecursive();
|
||||
for (Mesh2D mesh : meshes) {
|
||||
Vector2f oldWorldPivot = Matrix3fUtils.transformPoint(oldWorldTransform, mesh.getOriginalPivot());
|
||||
Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot);
|
||||
mesh.setOriginalPivot(newLocalOriginalPivot);
|
||||
mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot));
|
||||
}
|
||||
updateMeshVertices();
|
||||
triggerEvent("rotation");
|
||||
}
|
||||
@@ -1950,7 +1780,7 @@ public class ModelPart {
|
||||
return;
|
||||
}
|
||||
|
||||
// 原有单选择辑 - 修复:确保网格顶点被更新
|
||||
// 原有单选择辑
|
||||
Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform);
|
||||
this.scaleX = sx;
|
||||
this.scaleY = sy;
|
||||
@@ -1964,40 +1794,12 @@ public class ModelPart {
|
||||
Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot);
|
||||
mesh.setOriginalPivot(newLocalOriginalPivot);
|
||||
mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot));
|
||||
|
||||
// 更新木偶控制点和二级顶点
|
||||
updatePuppetPinsOriginalPositionForTransform(mesh, oldWorldTransform);
|
||||
updateSecondaryVerticesWorldPosition(mesh);
|
||||
}
|
||||
|
||||
updateMeshVertices();
|
||||
triggerEvent("scale");
|
||||
}
|
||||
|
||||
/**
|
||||
* 为变换操作更新木偶控制点的原始位置
|
||||
*/
|
||||
private void updatePuppetPinsOriginalPositionForTransform(Mesh2D mesh, Matrix3f oldWorldTransform) {
|
||||
if (mesh == null) return;
|
||||
|
||||
List<PuppetPin> puppetPins = mesh.getPuppetPins();
|
||||
if (puppetPins.isEmpty()) return;
|
||||
|
||||
for (PuppetPin pin : puppetPins) {
|
||||
// 获取控制点的当前原始位置(局部坐标)
|
||||
Vector2f currentOriginalPos = pin.getOriginalPosition();
|
||||
|
||||
// 将原始位置变换到旧的世界坐标系
|
||||
Vector2f oldWorldPos = Matrix3fUtils.transformPoint(oldWorldTransform, currentOriginalPos);
|
||||
|
||||
// 将旧的世界位置逆变换回新的局部坐标系
|
||||
Vector2f newLocalOriginalPos = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPos);
|
||||
|
||||
// 更新控制点的原始位置
|
||||
pin.setOriginalPosition(newLocalOriginalPos.x, newLocalOriginalPos.y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void setScale(float uniformScale) {
|
||||
// 记录旧的世界变换,用于计算 pivot 的相对位置
|
||||
@@ -2018,7 +1820,6 @@ public class ModelPart {
|
||||
mesh.setOriginalPivot(newLocalOriginalPivot);
|
||||
// 同时更新 mesh 的当前 pivot 到新的世界坐标
|
||||
mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot));
|
||||
updateSecondaryVerticesWorldPosition(mesh);
|
||||
}
|
||||
|
||||
updateMeshVertices();
|
||||
@@ -2069,90 +1870,40 @@ public class ModelPart {
|
||||
public void addMesh(Mesh2D mesh) {
|
||||
if (mesh == null) return;
|
||||
|
||||
// 确保拷贝保留原始的纹理引用(copy() 已处理)
|
||||
//mesh.setTexture(mesh.getTexture());
|
||||
mesh.setModelPart(this);
|
||||
// 确保本节点的 worldTransform 是最新的
|
||||
recomputeWorldTransformRecursive();
|
||||
|
||||
// 1. 保存局部顶点到 originalVertices
|
||||
float[] localVertices = mesh.getVertices().clone();
|
||||
mesh.setOriginalVertices(localVertices);
|
||||
// 2. 计算世界坐标并写入 *renderVertices*,而不是
|
||||
// 保存拷贝的原始(局部)顶点供后续重算 world 顶点使用
|
||||
float[] originalVertices = mesh.getVertices().clone();
|
||||
mesh.setOriginalVertices(originalVertices);
|
||||
// 把 originalPivot 保存在 mesh 中(setMeshData 已经初始化 originalPivot)
|
||||
// 将每个顶点从本地空间变换到世界空间(烘焙到 world)
|
||||
int vc = mesh.getVertexCount();
|
||||
for (int i = 0; i < vc; i++) {
|
||||
Vector2f local = new Vector2f(localVertices[i * 2], localVertices[i * 2 + 1]);
|
||||
Vector2f local = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]);
|
||||
Vector2f worldPt = Matrix3fUtils.transformPoint(this.worldTransform, local);
|
||||
// mesh.setVertex(i, worldPt.x, worldPt.y);
|
||||
mesh.setRenderVertex(i, worldPt.x, worldPt.y);
|
||||
mesh.setVertex(i, worldPt.x, worldPt.y);
|
||||
}
|
||||
// 3. 同步 pivot
|
||||
|
||||
// 同步 originalPivot -> world pivot(如果 originalPivot 有意义)
|
||||
try {
|
||||
Vector2f origPivot = mesh.getOriginalPivot();
|
||||
Vector2f worldPivot = Matrix3fUtils.transformPoint(this.worldTransform, origPivot);
|
||||
mesh.setPivot(worldPivot.x, worldPivot.y); // 现在这个会成功(因为步骤1的修复)
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
initializePuppetPinsPosition(mesh);
|
||||
mesh.setPivot(worldPivot.x, worldPivot.y);
|
||||
} catch (Exception ignored) { }
|
||||
|
||||
// 标记为已烘焙到世界坐标(语义上明确),并确保 bounds/dirty 状态被正确刷新
|
||||
mesh.setBakedToWorld(true);
|
||||
|
||||
// 确保 GPU 数据在下一次绘制时会被上传(如果当前在渲染线程,也可以直接 uploadToGPU)
|
||||
mesh.markDirty();
|
||||
|
||||
// 将拷贝加入到本部件
|
||||
meshes.add(mesh);
|
||||
boundsDirty = true;
|
||||
triggerEvent("meshes");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新木偶控制点的位置(修复移动问题)
|
||||
*/
|
||||
private void updatePuppetPinsForMovement(Mesh2D mesh, Matrix3f oldWorldTransform, float dx, float dy) {
|
||||
if (mesh == null) return;
|
||||
|
||||
List<PuppetPin> puppetPins = mesh.getPuppetPins();
|
||||
if (puppetPins.isEmpty()) return;
|
||||
|
||||
for (PuppetPin pin : puppetPins) {
|
||||
// 获取控制点的当前位置(世界坐标)
|
||||
Vector2f currentWorldPos = pin.getPosition();
|
||||
|
||||
// 应用相同的位移到控制点
|
||||
Vector2f newWorldPos = new Vector2f(currentWorldPos.x + dx, currentWorldPos.y + dy);
|
||||
|
||||
// 更新控制点的位置
|
||||
pin.setPosition(newWorldPos.x, newWorldPos.y);
|
||||
|
||||
// 同时更新控制点的原始位置,保持一致性
|
||||
Vector2f currentOriginalPos = pin.getOriginalPosition();
|
||||
Vector2f newOriginalPos = new Vector2f(currentOriginalPos.x + dx, currentOriginalPos.y + dy);
|
||||
pin.setOriginalPosition(newOriginalPos.x, newOriginalPos.y);
|
||||
|
||||
logger.trace("移动木偶控制点: ({}, {}) -> ({}, {})",
|
||||
currentWorldPos.x, currentWorldPos.y, newWorldPos.x, newWorldPos.y);
|
||||
}
|
||||
|
||||
logger.debug("移动时同步更新了 {} 个木偶控制点", puppetPins.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化木偶控制点的位置
|
||||
*/
|
||||
private void initializePuppetPinsPosition(Mesh2D mesh) {
|
||||
if (mesh == null) return;
|
||||
|
||||
List<PuppetPin> puppetPins = mesh.getPuppetPins();
|
||||
if (puppetPins.isEmpty()) return;
|
||||
|
||||
for (PuppetPin pin : puppetPins) {
|
||||
// 获取控制点的原始局部位置
|
||||
Vector2f originalLocalPos = pin.getOriginalPosition();
|
||||
|
||||
// 将原始局部位置变换到世界坐标
|
||||
Vector2f worldPos = Matrix3fUtils.transformPoint(this.worldTransform, originalLocalPos);
|
||||
|
||||
// 设置控制点的当前位置
|
||||
pin.setPosition(worldPos.x, worldPos.y);
|
||||
|
||||
// 同时保存当前位置为原始位置(确保一致性)
|
||||
pin.saveAsOriginal();
|
||||
}
|
||||
|
||||
logger.debug("初始化了 {} 个木偶控制点的位置", puppetPins.size());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2169,7 +1920,7 @@ public class ModelPart {
|
||||
// 由于 Mesh2D 的 originalPivot 已经存储了其在 ModelPart 局部坐标系中的相对位置,
|
||||
// 我们可以直接将 ModelPart 的新 pivot 赋值给 Mesh2D 的 originalPivot
|
||||
// 然后再通过变换更新 Mesh2D 的实际 pivot
|
||||
if (!mesh.setOriginalPivot(new Vector2f(x, y))) {
|
||||
if (!mesh.setOriginalPivot(new Vector2f(x, y))){
|
||||
return false;
|
||||
}
|
||||
// Mesh2D 的实际 pivot 应该根据 ModelPart 的世界变换来计算
|
||||
@@ -2219,6 +1970,33 @@ public class ModelPart {
|
||||
return new ArrayList<>(meshes);
|
||||
}
|
||||
|
||||
// ==================== 参数管理 ====================
|
||||
public AnimationParameter createParameter(String id, float min, float max, float defaultValue) {
|
||||
AnimationParameter param = new AnimationParameter(id, min, max, defaultValue);
|
||||
parameters.put(id, param);
|
||||
return param;
|
||||
}
|
||||
|
||||
public AnimationParameter getParameter(String id) {
|
||||
return parameters.get(id);
|
||||
}
|
||||
|
||||
public void addParameter(AnimationParameter param) {
|
||||
parameters.put(param.getId(), param);
|
||||
}
|
||||
|
||||
public void setParameterValue(String paramId, float value) {
|
||||
AnimationParameter param = parameters.get(paramId);
|
||||
if (param != null) {
|
||||
param.setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
public float getParameterValue(String paramId) {
|
||||
AnimationParameter param = parameters.get(paramId);
|
||||
return param != null ? param.getValue() : 0.0f;
|
||||
}
|
||||
|
||||
// ==================== 变形器管理 ====================
|
||||
|
||||
/**
|
||||
@@ -2283,27 +2061,19 @@ public class ModelPart {
|
||||
* 获取世界空间中的包围盒
|
||||
*/
|
||||
public BoundingBox getWorldBounds() {
|
||||
BoundingBox worldBounds = new BoundingBox();
|
||||
|
||||
for (Mesh2D mesh : meshes) {
|
||||
// 确保网格的世界边界是最新的
|
||||
BoundingBox meshWorldBounds = mesh.getWorldBounds();
|
||||
if (meshWorldBounds != null && meshWorldBounds.isValid()) {
|
||||
worldBounds.expand(meshWorldBounds);
|
||||
}
|
||||
if (boundsDirty) {
|
||||
updateBounds();
|
||||
}
|
||||
|
||||
// 如果没有有效边界,使用局部边界作为备选
|
||||
if (!worldBounds.isValid()) {
|
||||
for (Mesh2D mesh : meshes) {
|
||||
BoundingBox meshBounds = mesh.getBounds();
|
||||
if (meshBounds != null && meshBounds.isValid()) {
|
||||
// 变换到世界空间
|
||||
Vector2f min = localToWorld(new Vector2f(meshBounds.getMinX(), meshBounds.getMinY()));
|
||||
Vector2f max = localToWorld(new Vector2f(meshBounds.getMaxX(), meshBounds.getMaxY()));
|
||||
worldBounds.expand(min.x, min.y);
|
||||
worldBounds.expand(max.x, max.y);
|
||||
}
|
||||
BoundingBox worldBounds = new BoundingBox();
|
||||
for (Mesh2D mesh : meshes) {
|
||||
BoundingBox meshBounds = mesh.getBounds();
|
||||
if (meshBounds != null) {
|
||||
// 变换到世界空间
|
||||
Vector2f min = localToWorld(new Vector2f(meshBounds.getMinX(), meshBounds.getMinY()));
|
||||
Vector2f max = localToWorld(new Vector2f(meshBounds.getMaxX(), meshBounds.getMaxY()));
|
||||
worldBounds.expand(min.x, min.y);
|
||||
worldBounds.expand(max.x, max.y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2373,7 +2143,6 @@ public class ModelPart {
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
this.visible = visible;
|
||||
triggerEvent("visible");
|
||||
}
|
||||
|
||||
public BlendMode getBlendMode() {
|
||||
@@ -2382,24 +2151,19 @@ public class ModelPart {
|
||||
|
||||
public void setBlendMode(BlendMode blendMode) {
|
||||
this.blendMode = blendMode;
|
||||
triggerEvent("blendMode");
|
||||
}
|
||||
|
||||
public float getOpacity() {
|
||||
return opacity;
|
||||
}
|
||||
|
||||
public float getScaleX() {
|
||||
return scaleX;
|
||||
public float getScaleX() { return scaleX; }
|
||||
public float getScaleY() { return scaleY; }
|
||||
public Map<String, AnimationParameter> getParameters() {
|
||||
return Collections.unmodifiableMap(parameters);
|
||||
}
|
||||
|
||||
public float getScaleY() {
|
||||
return scaleY;
|
||||
}
|
||||
|
||||
public void setOpacity(float opacity) {
|
||||
this.opacity = Math.max(0.0f, Math.min(1.0f, opacity));
|
||||
triggerEvent("opacity");
|
||||
}
|
||||
|
||||
public List<Deformer> getDeformers() {
|
||||
@@ -2412,8 +2176,7 @@ public class ModelPart {
|
||||
public float y;
|
||||
public float pressure = 1.0f;
|
||||
|
||||
public LiquifyPoint() {
|
||||
}
|
||||
public LiquifyPoint() {}
|
||||
|
||||
public LiquifyPoint(float x, float y) {
|
||||
this.x = x;
|
||||
@@ -2426,17 +2189,9 @@ public class ModelPart {
|
||||
this.pressure = pressure;
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public float getPressure() {
|
||||
return pressure;
|
||||
}
|
||||
public float getX() { return x; }
|
||||
public float getY() { return y; }
|
||||
public float getPressure() { return pressure; }
|
||||
}
|
||||
|
||||
// ====== 液化笔划数据结构(包含点序列与笔划参数),提供 getter 以便反射读取 ======
|
||||
@@ -2447,8 +2202,7 @@ public class ModelPart {
|
||||
public int iterations = 1;
|
||||
public List<LiquifyPoint> points = new ArrayList<>();
|
||||
|
||||
public LiquifyStroke() {
|
||||
}
|
||||
public LiquifyStroke() {}
|
||||
|
||||
public LiquifyStroke(LiquifyMode mode, float radius, float strength, int iterations) {
|
||||
this.mode = mode;
|
||||
@@ -2457,25 +2211,11 @@ public class ModelPart {
|
||||
this.iterations = iterations;
|
||||
}
|
||||
|
||||
public String getMode() {
|
||||
return mode.name();
|
||||
} // PartData 反射时读取字符串也可
|
||||
|
||||
public float getRadius() {
|
||||
return radius;
|
||||
}
|
||||
|
||||
public float getStrength() {
|
||||
return strength;
|
||||
}
|
||||
|
||||
public int getIterations() {
|
||||
return iterations;
|
||||
}
|
||||
|
||||
public List<LiquifyPoint> getPoints() {
|
||||
return points;
|
||||
}
|
||||
public String getMode() { return mode.name(); } // PartData 反射时读取字符串也可
|
||||
public float getRadius() { return radius; }
|
||||
public float getStrength() { return strength; }
|
||||
public int getIterations() { return iterations; }
|
||||
public List<LiquifyPoint> getPoints() { return points; }
|
||||
|
||||
public void addPoint(float x, float y, float pressure) {
|
||||
this.points.add(new LiquifyPoint(x, y, pressure));
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
package com.chuangzhou.vivid2D.render.model.data;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.PuppetPin;
|
||||
import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex;
|
||||
import org.joml.Vector2f;
|
||||
import org.joml.Vector4f;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class MeshData implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
// 基础网格数据
|
||||
public String name;
|
||||
public float[] vertices;
|
||||
public float[] uvs;
|
||||
@@ -28,49 +17,9 @@ public class MeshData implements Serializable {
|
||||
public boolean visible;
|
||||
public int drawMode;
|
||||
|
||||
// 原始顶点数据(用于变形恢复)
|
||||
public float[] originalVertices;
|
||||
public float[] renderVertices = null;
|
||||
public String modelPartName = null; // 关联的ModelPart名称
|
||||
public boolean isSuspension = false;
|
||||
public Vector2f previewPoint = null;
|
||||
|
||||
// 变换相关
|
||||
public Vector2f pivot;
|
||||
public Vector2f originalPivot;
|
||||
|
||||
// 选择状态
|
||||
public boolean selected;
|
||||
public boolean bakedToWorld;
|
||||
|
||||
// ==================== 二级顶点支持 ====================
|
||||
public List<SecondaryVertexData> secondaryVertices = new ArrayList<>();
|
||||
public boolean showSecondaryVertices = false;
|
||||
public Vector4f secondaryVertexColor = new Vector4f(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
public Vector4f selectedSecondaryVertexColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
public float secondaryVertexSize = 6.0f;
|
||||
public Integer selectedSecondaryVertexId = null;
|
||||
|
||||
// ==================== 木偶工具支持 ====================
|
||||
public List<PuppetPinData> puppetPins = new ArrayList<>();
|
||||
public Integer selectedPuppetPinId = null;
|
||||
public boolean showPuppetPins = true;
|
||||
public Vector4f puppetPinColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
public Vector4f selectedPuppetPinColor = new Vector4f(1.0f, 1.0f, 0.0f, 1.0f);
|
||||
public float puppetPinSize = 8.0f;
|
||||
|
||||
// ==================== 液化状态支持 ====================
|
||||
public boolean showLiquifyOverlay = false;
|
||||
public Vector4f liquifyOverlayColor = new Vector4f(1.0f, 0.5f, 0.0f, 0.3f);
|
||||
|
||||
// ==================== 渲染状态 ====================
|
||||
public boolean isRenderVertices = false;
|
||||
|
||||
public MeshData() {
|
||||
this.visible = true;
|
||||
this.drawMode = Mesh2D.TRIANGLES;
|
||||
this.pivot = new Vector2f(0, 0);
|
||||
this.originalPivot = new Vector2f(0, 0);
|
||||
}
|
||||
|
||||
public MeshData(Mesh2D mesh) {
|
||||
@@ -81,164 +30,19 @@ public class MeshData implements Serializable {
|
||||
this.indices = mesh.getIndices();
|
||||
this.visible = mesh.isVisible();
|
||||
this.drawMode = mesh.getDrawMode();
|
||||
this.selected = mesh.isSelected();
|
||||
this.bakedToWorld = mesh.isBakedToWorld();
|
||||
this.isRenderVertices = mesh.isRenderVertices();
|
||||
|
||||
// 保存原始顶点数据
|
||||
this.originalVertices = mesh.getOriginalVertices();
|
||||
this.renderVertices = mesh.getRenderVertices(); // 获取渲染顶点副本
|
||||
this.isSuspension = mesh.isSuspension();
|
||||
this.previewPoint = mesh.getPreviewPoint() != null ? new Vector2f(mesh.getPreviewPoint()) : null;
|
||||
if (mesh.getModelPart() != null) {
|
||||
this.modelPartName = mesh.getModelPart().getName();
|
||||
}
|
||||
|
||||
// 保存变换数据
|
||||
this.pivot = new Vector2f(mesh.getPivot());
|
||||
this.originalPivot = new Vector2f(mesh.getOriginalPivot());
|
||||
|
||||
// 保存二级顶点数据
|
||||
saveSecondaryVertices(mesh);
|
||||
|
||||
// 保存木偶控制点数据
|
||||
savePuppetPins(mesh);
|
||||
|
||||
// 保存液化状态
|
||||
this.showLiquifyOverlay = mesh.showLiquifyOverlay;
|
||||
this.liquifyOverlayColor = new Vector4f(mesh.getLiquifyOverlayColor());
|
||||
|
||||
if (mesh.getTexture() != null) {
|
||||
this.textureName = mesh.getTexture().getName();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存二级顶点数据
|
||||
*/
|
||||
private void saveSecondaryVertices(Mesh2D mesh) {
|
||||
List<SecondaryVertex> vertices = mesh.getSecondaryVertices();
|
||||
SecondaryVertex selectedVertex = mesh.getSelectedSecondaryVertex();
|
||||
|
||||
for (SecondaryVertex vertex : vertices) {
|
||||
SecondaryVertexData data = new SecondaryVertexData(vertex);
|
||||
secondaryVertices.add(data);
|
||||
|
||||
// 记录选中的二级顶点ID
|
||||
if (selectedVertex != null && selectedVertex.getId() == vertex.getId()) {
|
||||
selectedSecondaryVertexId = vertex.getId();
|
||||
}
|
||||
}
|
||||
|
||||
this.showSecondaryVertices = mesh.isShowSecondaryVertices();
|
||||
this.secondaryVertexColor = new Vector4f(mesh.secondaryVertexColor);
|
||||
this.selectedSecondaryVertexColor = new Vector4f(mesh.selectedSecondaryVertexColor);
|
||||
this.secondaryVertexSize = mesh.secondaryVertexSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存木偶控制点数据
|
||||
*/
|
||||
private void savePuppetPins(Mesh2D mesh) {
|
||||
List<PuppetPin> pins = mesh.getPuppetPins();
|
||||
PuppetPin selectedPin = mesh.getSelectedPuppetPin();
|
||||
|
||||
for (PuppetPin pin : pins) {
|
||||
PuppetPinData data = new PuppetPinData(pin);
|
||||
puppetPins.add(data);
|
||||
|
||||
// 记录选中的木偶控制点ID
|
||||
if (selectedPin != null && selectedPin.getId() == pin.getId()) {
|
||||
selectedPuppetPinId = pin.getId();
|
||||
}
|
||||
}
|
||||
|
||||
this.showPuppetPins = mesh.getShowPuppetPins();
|
||||
this.puppetPinColor = new Vector4f(mesh.getPuppetPinColor());
|
||||
this.selectedPuppetPinColor = new Vector4f(mesh.getSelectedPuppetPinColor());
|
||||
this.puppetPinSize = mesh.getPuppetPinSize();
|
||||
}
|
||||
|
||||
public Mesh2D toMesh2D() {
|
||||
Mesh2D mesh = new Mesh2D(name, vertices, uvs, indices);
|
||||
mesh.setVisible(visible);
|
||||
mesh.setDrawMode(drawMode);
|
||||
mesh.setSelected(selected);
|
||||
mesh.setBakedToWorld(bakedToWorld);
|
||||
mesh.setRenderVertices(isRenderVertices);
|
||||
|
||||
// 恢复原始顶点数据
|
||||
if (originalVertices != null) {
|
||||
mesh.setOriginalVertices(originalVertices);
|
||||
}
|
||||
|
||||
// 恢复变换数据
|
||||
mesh.setPivot(pivot);
|
||||
mesh.setOriginalPivot(originalPivot);
|
||||
|
||||
// 恢复二级顶点
|
||||
restoreSecondaryVertices(mesh);
|
||||
|
||||
mesh.setSuspension(this.isSuspension);
|
||||
if (this.previewPoint != null) {
|
||||
mesh.setPreviewPoint(new Vector2f(this.previewPoint));
|
||||
}
|
||||
|
||||
// 恢复木偶控制点
|
||||
restorePuppetPins(mesh);
|
||||
|
||||
// 恢复液化状态
|
||||
mesh.setShowLiquifyOverlay(showLiquifyOverlay);
|
||||
mesh.getLiquifyOverlayColor().set(liquifyOverlayColor);
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复二级顶点数据
|
||||
*/
|
||||
private void restoreSecondaryVertices(Mesh2D mesh) {
|
||||
for (SecondaryVertexData data : secondaryVertices) {
|
||||
SecondaryVertex vertex = mesh.addSecondaryVertex(data.position.x, data.position.y, data.uv.x, data.uv.y);
|
||||
vertex.setId(data.id);
|
||||
vertex.setOriginalPosition(data.originalPosition);
|
||||
|
||||
// 恢复选中状态
|
||||
if (selectedSecondaryVertexId != null && data.id == selectedSecondaryVertexId) {
|
||||
mesh.setSelectedSecondaryVertex(vertex);
|
||||
}
|
||||
}
|
||||
|
||||
mesh.setShowSecondaryVertices(showSecondaryVertices);
|
||||
mesh.setSecondaryVertexColor(secondaryVertexColor);
|
||||
mesh.setSelectedSecondaryVertexColor(selectedSecondaryVertexColor);
|
||||
mesh.setSecondaryVertexSize(secondaryVertexSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复木偶控制点数据
|
||||
*/
|
||||
private void restorePuppetPins(Mesh2D mesh) {
|
||||
for (PuppetPinData data : puppetPins) {
|
||||
PuppetPin pin = mesh.addPuppetPin(data.position.x, data.position.y, data.uv.x, data.uv.y);
|
||||
pin.setId(data.id);
|
||||
pin.setOriginalPosition(data.originalPosition);
|
||||
pin.setInfluenceRadius(data.influenceRadius);
|
||||
pin.setName(data.name);
|
||||
|
||||
// 恢复权重映射
|
||||
pin.getWeightMap().putAll(data.weightMap);
|
||||
|
||||
// 恢复选中状态
|
||||
if (selectedPuppetPinId != null && data.id == selectedPuppetPinId) {
|
||||
mesh.setSelectedPuppetPin(pin);
|
||||
}
|
||||
}
|
||||
|
||||
mesh.setShowPuppetPins(showPuppetPins);
|
||||
// 注意:木偶控制点颜色等设置需要在Mesh2D中添加相应的setter方法
|
||||
}
|
||||
|
||||
public MeshData copy() {
|
||||
MeshData copy = new MeshData();
|
||||
copy.name = this.name;
|
||||
@@ -248,187 +52,6 @@ public class MeshData implements Serializable {
|
||||
copy.textureName = this.textureName;
|
||||
copy.visible = this.visible;
|
||||
copy.drawMode = this.drawMode;
|
||||
copy.selected = this.selected;
|
||||
copy.bakedToWorld = this.bakedToWorld;
|
||||
copy.isRenderVertices = this.isRenderVertices;
|
||||
|
||||
copy.renderVertices = this.renderVertices != null ? this.renderVertices.clone() : null;
|
||||
copy.modelPartName = this.modelPartName;
|
||||
copy.isSuspension = this.isSuspension;
|
||||
copy.previewPoint = this.previewPoint != null ? new Vector2f(this.previewPoint) : null;
|
||||
|
||||
// 复制原始顶点数据
|
||||
copy.originalVertices = this.originalVertices != null ? this.originalVertices.clone() : null;
|
||||
|
||||
// 复制变换数据
|
||||
copy.pivot = new Vector2f(this.pivot);
|
||||
copy.originalPivot = new Vector2f(this.originalPivot);
|
||||
|
||||
// 复制二级顶点数据
|
||||
copy.secondaryVertices = new ArrayList<>();
|
||||
for (SecondaryVertexData data : this.secondaryVertices) {
|
||||
copy.secondaryVertices.add(data.copy());
|
||||
}
|
||||
copy.showSecondaryVertices = this.showSecondaryVertices;
|
||||
copy.secondaryVertexColor = new Vector4f(this.secondaryVertexColor);
|
||||
copy.selectedSecondaryVertexColor = new Vector4f(this.selectedSecondaryVertexColor);
|
||||
copy.secondaryVertexSize = this.secondaryVertexSize;
|
||||
copy.selectedSecondaryVertexId = this.selectedSecondaryVertexId;
|
||||
|
||||
// 复制木偶控制点数据
|
||||
copy.puppetPins = new ArrayList<>();
|
||||
for (PuppetPinData data : this.puppetPins) {
|
||||
copy.puppetPins.add(data.copy());
|
||||
}
|
||||
copy.selectedPuppetPinId = this.selectedPuppetPinId;
|
||||
copy.showPuppetPins = this.showPuppetPins;
|
||||
copy.puppetPinColor = new Vector4f(this.puppetPinColor);
|
||||
copy.selectedPuppetPinColor = new Vector4f(this.selectedPuppetPinColor);
|
||||
copy.puppetPinSize = this.puppetPinSize;
|
||||
|
||||
// 复制液化状态
|
||||
copy.showLiquifyOverlay = this.showLiquifyOverlay;
|
||||
copy.liquifyOverlayColor = new Vector4f(this.liquifyOverlayColor);
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
// ==================== 内部数据类 ====================
|
||||
|
||||
/**
|
||||
* 二级顶点数据类(可序列化)
|
||||
*/
|
||||
public static class SecondaryVertexData implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public int id;
|
||||
public Vector2f position;
|
||||
public Vector2f originalPosition;
|
||||
public Vector2f uv;
|
||||
public boolean selected;
|
||||
public ControlShapeData controlShape;
|
||||
|
||||
public SecondaryVertexData() {
|
||||
this.controlShape = new ControlShapeData();
|
||||
}
|
||||
public SecondaryVertexData(SecondaryVertex vertex) {
|
||||
this.id = vertex.getId();
|
||||
this.position = vertex.getPosition();
|
||||
this.originalPosition = vertex.getOriginalPosition();
|
||||
this.uv = vertex.getUV();
|
||||
this.selected = vertex.isSelected();
|
||||
this.controlShape = createControlShapeData(vertex.getControlShape());
|
||||
}
|
||||
|
||||
private static ControlShapeData createControlShapeData(SecondaryVertex.ControlShape controlShape) {
|
||||
return new ControlShapeData(controlShape);
|
||||
}
|
||||
|
||||
|
||||
public SecondaryVertexData copy() {
|
||||
SecondaryVertexData copy = new SecondaryVertexData();
|
||||
copy.id = this.id;
|
||||
copy.position = new Vector2f(this.position);
|
||||
copy.originalPosition = new Vector2f(this.originalPosition);
|
||||
copy.uv = new Vector2f(this.uv);
|
||||
copy.selected = this.selected;
|
||||
copy.controlShape = this.controlShape.copy();
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制点形状和约束数据类(可序列化),用于封装二级顶点控制属性。
|
||||
*/
|
||||
public static class ControlShapeData implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
public int shapeId;
|
||||
public boolean pinned;
|
||||
public boolean locked;
|
||||
public Vector2f minControlPoint;
|
||||
public Vector2f maxControlPoint;
|
||||
|
||||
public ControlShapeData() {
|
||||
this.minControlPoint = new Vector2f(0, 0);
|
||||
this.maxControlPoint = new Vector2f(0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数:从 SecondaryVertex.ControlShape 对象创建数据
|
||||
*/
|
||||
public ControlShapeData(SecondaryVertex.ControlShape controlShape) {
|
||||
this.shapeId = controlShape.getShapeId();
|
||||
this.pinned = controlShape.isPinned();
|
||||
this.locked = controlShape.isLocked();
|
||||
this.minControlPoint = controlShape.getMinControlPoint();
|
||||
this.maxControlPoint = controlShape.getMaxControlPoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 ControlShapeData 的深拷贝
|
||||
*/
|
||||
public ControlShapeData copy() {
|
||||
ControlShapeData copy = new ControlShapeData();
|
||||
copy.shapeId = this.shapeId;
|
||||
copy.pinned = this.pinned;
|
||||
copy.locked = this.locked;
|
||||
copy.minControlPoint = new Vector2f(this.minControlPoint);
|
||||
copy.maxControlPoint = new Vector2f(this.maxControlPoint);
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ControlShapeData{id=%d, pinned=%s, locked=%s, min=(%.2f, %.2f), max=(%.2f, %.2f)}",
|
||||
shapeId, pinned, locked, minControlPoint.x, minControlPoint.y, maxControlPoint.x, maxControlPoint.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 木偶控制点数据类(可序列化)
|
||||
*/
|
||||
public static class PuppetPinData implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public int id;
|
||||
public Vector2f position;
|
||||
public Vector2f originalPosition;
|
||||
public Vector2f uv;
|
||||
public float influenceRadius;
|
||||
public boolean selected;
|
||||
public String name;
|
||||
public Map<Integer, Float> weightMap;
|
||||
|
||||
public PuppetPinData() {
|
||||
this.weightMap = new HashMap<>();
|
||||
}
|
||||
|
||||
public PuppetPinData(PuppetPin pin) {
|
||||
this();
|
||||
this.id = pin.getId();
|
||||
this.position = new Vector2f(pin.getPosition());
|
||||
this.originalPosition = new Vector2f(pin.getOriginalPosition());
|
||||
this.uv = new Vector2f(pin.getUV());
|
||||
this.influenceRadius = pin.getInfluenceRadius();
|
||||
this.selected = pin.isSelected();
|
||||
this.name = pin.getName();
|
||||
this.weightMap = new HashMap<>(pin.getWeightMap());
|
||||
}
|
||||
|
||||
public PuppetPinData copy() {
|
||||
PuppetPinData copy = new PuppetPinData();
|
||||
copy.id = this.id;
|
||||
copy.position = new Vector2f(this.position);
|
||||
copy.originalPosition = new Vector2f(this.originalPosition);
|
||||
copy.uv = new Vector2f(this.uv);
|
||||
copy.influenceRadius = this.influenceRadius;
|
||||
copy.selected = this.selected;
|
||||
copy.name = this.name;
|
||||
copy.weightMap = new HashMap<>(this.weightMap);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.chuangzhou.vivid2D.render.model.data;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.*;
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.render.model.data;
|
||||
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Deformer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.chuangzhou.vivid2D.render.model.transform;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.Deformer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.chuangzhou.vivid2D.render.model.transform;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.Deformer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.SaveVector2f;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.chuangzhou.vivid2D.render.model.transform;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.Deformer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.chuangzhou.vivid2D.render.model.transform;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.Deformer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SecondaryVertex {
|
||||
|
||||
private final Vector2f position;
|
||||
private final Vector2f originalPosition;
|
||||
private final Vector2f uv;
|
||||
|
||||
private int id;
|
||||
private static int nextId = 0;
|
||||
private boolean selected = false;
|
||||
|
||||
private final transient Vector2f worldPosition = new Vector2f();
|
||||
private final transient Vector2f renderPosition = new Vector2f();
|
||||
|
||||
private final ControlShape controlShape;
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void setSelected(boolean b) {
|
||||
this.selected = b;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public static class ControlShape {
|
||||
private final int shapeId;
|
||||
private static int nextShapeId = 0;
|
||||
private boolean pinned = false;
|
||||
private boolean locked = false;
|
||||
private final Vector2f minControlPoint = new Vector2f(0, 0);
|
||||
private final Vector2f maxControlPoint = new Vector2f(0, 0);
|
||||
private final List<SecondaryVertex> controlVertices = new ArrayList<>();
|
||||
|
||||
public ControlShape() {
|
||||
this.shapeId = nextShapeId++;
|
||||
}
|
||||
|
||||
public int getShapeId() {
|
||||
return shapeId;
|
||||
}
|
||||
|
||||
public boolean isPinned() {
|
||||
return pinned;
|
||||
}
|
||||
|
||||
public void setPinned(boolean pinned) {
|
||||
this.pinned = pinned;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return locked;
|
||||
}
|
||||
|
||||
public void setLocked(boolean locked) {
|
||||
this.locked = locked;
|
||||
}
|
||||
|
||||
public Vector2f getMinControlPoint() {
|
||||
return new Vector2f(minControlPoint);
|
||||
}
|
||||
|
||||
public void setMinControlPoint(Vector2f minControlPoint) {
|
||||
this.minControlPoint.set(minControlPoint);
|
||||
}
|
||||
|
||||
public Vector2f getMaxControlPoint() {
|
||||
return new Vector2f(maxControlPoint);
|
||||
}
|
||||
|
||||
public void setMaxControlPoint(Vector2f maxControlPoint) {
|
||||
this.maxControlPoint.set(maxControlPoint);
|
||||
}
|
||||
|
||||
public List<SecondaryVertex> getControlVertices() {
|
||||
return controlVertices;
|
||||
}
|
||||
|
||||
public void addControlVertex(SecondaryVertex vertex) {
|
||||
if (vertex != null && !controlVertices.contains(vertex)) {
|
||||
controlVertices.add(vertex);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeControlVertex(SecondaryVertex vertex) {
|
||||
controlVertices.remove(vertex);
|
||||
}
|
||||
|
||||
public void clearControlVertices() {
|
||||
controlVertices.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("ControlShape{id=%d, pinned=%s, locked=%s, min=(%.2f, %.2f), max=(%.2f, %.2f), vertCount=%d}",
|
||||
shapeId, pinned, locked, minControlPoint.x, minControlPoint.y, maxControlPoint.x, maxControlPoint.y, controlVertices.size());
|
||||
}
|
||||
}
|
||||
|
||||
public SecondaryVertex(float x, float y, float u, float v) {
|
||||
this.position = new Vector2f(x, y);
|
||||
this.originalPosition = new Vector2f(x, y);
|
||||
this.uv = new Vector2f(u, v);
|
||||
this.id = nextId++;
|
||||
this.controlShape = new ControlShape();
|
||||
}
|
||||
|
||||
public Vector2f getPosition() {
|
||||
return new Vector2f(position);
|
||||
}
|
||||
|
||||
public void setPosition(float x, float y) {
|
||||
this.position.set(x, y);
|
||||
}
|
||||
|
||||
public void setPosition(Vector2f position) {
|
||||
this.position.set(position);
|
||||
}
|
||||
|
||||
public void move(float dx, float dy) {
|
||||
this.position.add(dx, dy);
|
||||
}
|
||||
|
||||
public Vector2f getOriginalPosition() {
|
||||
return new Vector2f(originalPosition);
|
||||
}
|
||||
|
||||
public void setOriginalPosition(Vector2f originalPosition) {
|
||||
this.originalPosition.set(originalPosition);
|
||||
}
|
||||
|
||||
public void resetToOriginal() {
|
||||
this.position.set(originalPosition);
|
||||
}
|
||||
|
||||
public void saveAsOriginal() {
|
||||
this.originalPosition.set(position);
|
||||
}
|
||||
|
||||
|
||||
public Vector2f getUV() {
|
||||
return new Vector2f(uv);
|
||||
}
|
||||
|
||||
public void setUV(float u, float v) {
|
||||
this.uv.set(u, v);
|
||||
}
|
||||
|
||||
public void setWorldPosition(Vector2f p) {
|
||||
if (p == null) return;
|
||||
this.worldPosition.set(p);
|
||||
}
|
||||
|
||||
public Vector2f getRenderPosition() {
|
||||
return new Vector2f(renderPosition);
|
||||
}
|
||||
|
||||
public void setRenderPosition(float x, float y) {
|
||||
this.renderPosition.set(x, y);
|
||||
}
|
||||
|
||||
public void setRenderPosition(Vector2f p) {
|
||||
if (p == null) return;
|
||||
this.renderPosition.set(p);
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isPinned() {
|
||||
return controlShape.isPinned();
|
||||
}
|
||||
|
||||
public void setPinned(boolean pinned) {
|
||||
this.controlShape.setPinned(pinned);
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return controlShape.isLocked();
|
||||
}
|
||||
|
||||
public void setLocked(boolean locked) {
|
||||
this.controlShape.setLocked(locked);
|
||||
}
|
||||
|
||||
public ControlShape getControlShape() {
|
||||
return controlShape;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SecondaryVertex that = (SecondaryVertex) o;
|
||||
return id == that.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("SecondaryVertex{id=%d, position=(%.2f, %.2f), uv=(%.2f, %.2f), pinned=%s, locked=%s, shape=%s}",
|
||||
id, position.x, position.y, uv.x, uv.y, isPinned(), isLocked(), controlShape.toString());
|
||||
}
|
||||
|
||||
public static void resetNextId(int newNextId) {
|
||||
nextId = newNextId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 封装一个2D顶点,包含位置、UV坐标和原始位置。
|
||||
*
|
||||
* @author Gemini
|
||||
*/
|
||||
public class Vertex {
|
||||
|
||||
public Vector2f position; // 当前顶点位置 (x, y)
|
||||
public Vector2f uv; // UV坐标 (u, v)
|
||||
public Vector2f originalPosition; // 原始顶点位置 (用于变形)
|
||||
private VertexTag tag;
|
||||
private boolean selected;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param x 顶点 x 坐标
|
||||
* @param y 顶点 y 坐标
|
||||
* @param u 顶点 u 坐标
|
||||
* @param v 顶点 v 坐标
|
||||
*/
|
||||
public Vertex(float x, float y, float u, float v) {
|
||||
this.position = new Vector2f(x, y);
|
||||
this.uv = new Vector2f(u, v);
|
||||
this.originalPosition = new Vector2f(x, y); // 初始时,原始位置=当前位置
|
||||
this.tag = VertexTag.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param position 顶点位置
|
||||
* @param uv UV坐标
|
||||
*/
|
||||
public Vertex(Vector2f position, Vector2f uv) {
|
||||
this.position = new Vector2f(position);
|
||||
this.uv = new Vector2f(uv);
|
||||
this.originalPosition = new Vector2f(position);
|
||||
this.tag = VertexTag.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数(用于复制)
|
||||
*
|
||||
* @param position 顶点位置
|
||||
* @param uv UV坐标
|
||||
* @param originalPosition 原始位置
|
||||
*/
|
||||
public Vertex(Vector2f position, Vector2f uv, Vector2f originalPosition) {
|
||||
this.position = new Vector2f(position);
|
||||
this.uv = new Vector2f(uv);
|
||||
this.originalPosition = new Vector2f(originalPosition);
|
||||
this.tag = VertexTag.DEFAULT;
|
||||
}
|
||||
|
||||
public VertexTag getTag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public void setTag(VertexTag tag) {
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置为原始位置
|
||||
*/
|
||||
public void resetToOriginal() {
|
||||
this.position.set(this.originalPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前位置为新的原始位置
|
||||
*/
|
||||
public void saveAsOriginal() {
|
||||
this.originalPosition.set(this.position);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置顶点是否被选中(在渲染中被调用)
|
||||
* @param selected 是否被选中
|
||||
*/
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取顶点是否被选中
|
||||
* @return 是否被选中
|
||||
*/
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建此顶点的深拷贝
|
||||
*/
|
||||
public Vertex copy() {
|
||||
return new Vertex(this.position, this.uv, this.originalPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Vertex vertex = (Vertex) o;
|
||||
return Objects.equals(position, vertex.position) &&
|
||||
Objects.equals(uv, vertex.uv) &&
|
||||
Objects.equals(originalPosition, vertex.originalPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(position, uv, originalPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Vertex{" +
|
||||
"pos=" + position +
|
||||
", uv=" + uv +
|
||||
", orig=" + originalPosition +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 一个带有名称和标签的 Vertex 集合。
|
||||
*
|
||||
* @author Gemini
|
||||
*/
|
||||
public class VertexList implements Iterable<Vertex> {
|
||||
|
||||
public final List<Vertex> vertices;
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 默认构造函数。
|
||||
* 名称默认为 "KongFuZi",标签默认为 "DEFAULT"。
|
||||
*/
|
||||
public VertexList() {
|
||||
this.vertices = new ArrayList<>();
|
||||
this.name = "KongFuZi";
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param name 此列表的名称
|
||||
* @param tag 此列表的标签 (VertexTag)
|
||||
*/
|
||||
public VertexList(String name) {
|
||||
this.vertices = new ArrayList<>();
|
||||
this.name = Objects.requireNonNull(name, "Name cannot be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param name 此列表的名称
|
||||
* @param tag 此列表的标签
|
||||
* @param initialVertices 用于初始化列表的顶点集合
|
||||
*/
|
||||
public VertexList(String name, Collection<Vertex> initialVertices) {
|
||||
this(name);
|
||||
if (initialVertices != null) {
|
||||
this.vertices.addAll(initialVertices);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 列表管理 ---
|
||||
|
||||
/**
|
||||
* 向列表末尾添加一个顶点。
|
||||
*
|
||||
* @param vertex 要添加的顶点
|
||||
*/
|
||||
public void add(Vertex vertex) {
|
||||
if (vertex != null) {
|
||||
this.vertices.add(vertex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除列表中的指定顶点。
|
||||
*
|
||||
* @param vertex 要移除的顶点
|
||||
* @return 如果成功移除则为 true
|
||||
*/
|
||||
public boolean remove(Vertex vertex) {
|
||||
return this.vertices.remove(vertex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定索引处的顶点。
|
||||
*
|
||||
* @param index 要移除的索引
|
||||
* @return 被移除的顶点
|
||||
*/
|
||||
public Vertex remove(int index) {
|
||||
return this.vertices.remove(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定索引处的顶点。
|
||||
*
|
||||
* @param index 索引
|
||||
* @return 顶点
|
||||
*/
|
||||
public Vertex get(int index) {
|
||||
return this.vertices.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回列表中的顶点数量。
|
||||
*
|
||||
* @return 顶点数量
|
||||
*/
|
||||
public int size() {
|
||||
return this.vertices.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查列表是否为空。
|
||||
*
|
||||
* @return 如果为空则为 true
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return this.vertices.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空列表中的所有顶点。
|
||||
*/
|
||||
public void clear() {
|
||||
this.vertices.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回内部列表的只读视图。
|
||||
* (注意:如果需要修改,请使用 add/remove 等方法)
|
||||
*
|
||||
* @return 顶点的列表
|
||||
*/
|
||||
public List<Vertex> getVertices() {
|
||||
return new ArrayList<>(vertices); // 返回一个副本以防止外部修改
|
||||
}
|
||||
|
||||
// --- Getter 和 Setter ---
|
||||
|
||||
/**
|
||||
* 获取此列表的名称。
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置此列表的名称。
|
||||
*
|
||||
* @param name 新名称
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = Objects.requireNonNull(name, "Name cannot be null");
|
||||
}
|
||||
|
||||
// --- 迭代器 ---
|
||||
|
||||
/**
|
||||
* 返回顶点的迭代器。
|
||||
*/
|
||||
@Override
|
||||
public @NotNull Iterator<Vertex> iterator() {
|
||||
return this.vertices.iterator();
|
||||
}
|
||||
|
||||
// --- Object 方法 ---
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "VertexList{" +
|
||||
"name='" + name + '\'' +
|
||||
", vertexCount=" + vertices.size() +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
VertexList that = (VertexList) o;
|
||||
return Objects.equals(vertices, that.vertices) &&
|
||||
Objects.equals(name, that.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(vertices, name);
|
||||
}
|
||||
|
||||
public void set(int index, Vertex vertex) {
|
||||
this.vertices.set(index, vertex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
/**
|
||||
* 定义 VertexList 的标签类型。
|
||||
*/
|
||||
public enum VertexTag {
|
||||
/**
|
||||
* 用于变形的顶点
|
||||
*/
|
||||
DEFORMATION,
|
||||
|
||||
/**
|
||||
* 默认的顶点
|
||||
*/
|
||||
DEFAULT,
|
||||
|
||||
/**
|
||||
* 其他类型的顶点
|
||||
*/
|
||||
OTHER
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.chuangzhou.vivid2D.render.model.util.manager;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.Tool;
|
||||
import com.chuangzhou.vivid2D.render.model.util.tools.RanderTools;
|
||||
import org.joml.Matrix3f;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -12,6 +13,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
* 负责管理Tool和RanderTools之间的绑定关系
|
||||
*/
|
||||
public class RanderToolsManager {
|
||||
private static Logger logger = org.slf4j.LoggerFactory.getLogger(RanderToolsManager.class);
|
||||
|
||||
// 存储Tool和RanderTools的绑定关系
|
||||
private final Map<Tool, RanderTools> toolRanderToolsMap;
|
||||
@@ -74,7 +76,8 @@ public class RanderToolsManager {
|
||||
successCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("渲染工具执行失败: " + randerTools.getClass().getSimpleName() + " - " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
logger.info("渲染工具执行失败: {} - {}", randerTools.getClass().getSimpleName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
return successCount;
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util.tools;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.TextRenderer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator;
|
||||
import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement;
|
||||
import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Vector4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* LiquifyTargetPartRander - 优化版(针对 isRenderVertices 大网格卡顿做了多项性能优化)
|
||||
*
|
||||
* 优化点:
|
||||
* 1. 对大网格(顶点数超过阈值)降低绘制细节:不绘制内部三角线,采样绘制顶点(点与编号减少频率)。
|
||||
* 2. 将多次 begin()/endImmediate() 批次化:尽量合并连续的线段绘制到单次 begin/end。
|
||||
* 3. 降低每顶点的内存分配与 API 调用(把文本渲染请求收集、复用局部变量)。
|
||||
* 4. dashed line 绘制改为在小段数量内合并绘制或直接绘制单条轻量直线以减少 draw call。
|
||||
* 5. 对 circle/point 的 segment 数做自适应限制(越大网格越少细分)。
|
||||
*/
|
||||
public class LiquifyTargetPartRander extends RanderTools {
|
||||
private final Vector4f liquifyOverlayColor = new Vector4f(1.0f, 0.5f, 0.0f, 0.3f); // 半透明橙色
|
||||
|
||||
// 可调阈值(根据你的机器/场景调小或调大)
|
||||
private static final int LARGE_VERTEX_THRESHOLD = 1500; // 超过则使用低质量绘制
|
||||
private static final int TARGET_VERTEX_DRAW = 300; // 大网格时目标绘制的顶点数量
|
||||
|
||||
@Override
|
||||
public void init(Map<String, Boolean> algorithmEnabled) {
|
||||
algorithmEnabled.put("showLiquifyOverlay", false);
|
||||
algorithmEnabled.put("isRenderVertices", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean render(Matrix3f modelMatrix,Object renderContext) {
|
||||
if (renderContext instanceof Mesh2D mesh2D) {
|
||||
drawLiquifyOverlay(mesh2D, modelMatrix);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主渲染入口。收集文本命令到 pendingTexts,绘制完成后 popState,再把文本绘制出来(外部渲染)
|
||||
*/
|
||||
private void drawLiquifyOverlay(Mesh2D mesh2D, Matrix3f modelMatrix) {
|
||||
if (!isAlgorithmEnabled("showLiquifyOverlay") || !mesh2D.isShowLiquifyOverlay()) return;
|
||||
|
||||
List<TextItem> pendingTexts = new ArrayList<>();
|
||||
|
||||
RenderSystem.pushState();
|
||||
try {
|
||||
ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader");
|
||||
if (solidShader != null && solidShader.programId != 0) {
|
||||
solidShader.use();
|
||||
int modelLoc = solidShader.getUniformLocation("uModelMatrix");
|
||||
if (modelLoc != -1) RenderSystem.uniformMatrix3(modelLoc, modelMatrix);
|
||||
}
|
||||
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
Tesselator t = Tesselator.getInstance();
|
||||
BufferBuilder bb = t.getBuilder();
|
||||
|
||||
BoundingBox bounds = mesh2D.getBounds();
|
||||
if (bounds == null || !bounds.isValid()) return;
|
||||
|
||||
if (!isAlgorithmEnabled("isRenderVertices")) {
|
||||
// 普通模式:美观渲染(轻填充 + 多层轮廓)
|
||||
Vector4f shadowCol = new Vector4f(0f, 0f, 0f, 0.08f);
|
||||
drawRoundedBoundingFill(bb, bounds, shadowCol, 6);
|
||||
|
||||
Vector4f innerFill = new Vector4f(liquifyOverlayColor.x, liquifyOverlayColor.y, liquifyOverlayColor.z, 0.06f);
|
||||
drawRoundedBoundingFill(bb, bounds, innerFill, 4);
|
||||
|
||||
float[] alphas = new float[]{0.42f, 0.24f, 0.12f};
|
||||
float[] expand = new float[]{6f, 3f, 0f};
|
||||
for (int i = 0; i < alphas.length; i++) {
|
||||
Vector4f stroke = new Vector4f(liquifyOverlayColor.x, liquifyOverlayColor.y, liquifyOverlayColor.z, alphas[i]);
|
||||
drawRoundedBoundingOutline(bb, bounds, stroke, Math.max(1f, 2.5f - i), expand[i]);
|
||||
}
|
||||
|
||||
collectLiquifyStatusText(mesh2D, pendingTexts);
|
||||
|
||||
} else {
|
||||
// isRenderVertices 模式:性能优先 + 少量视觉信息
|
||||
Vector4f outline = new Vector4f(liquifyOverlayColor.x, liquifyOverlayColor.y, liquifyOverlayColor.z, 0.16f);
|
||||
drawRoundedBoundingOutline(bb, bounds, outline, 1.0f, 0f);
|
||||
|
||||
// 优化点:根据顶点数量选择低/高质量绘制
|
||||
int vertexCount = mesh2D.getRenderVertices() == null ? 0 : mesh2D.getRenderVertices().length / 2;
|
||||
boolean large = vertexCount > LARGE_VERTEX_THRESHOLD;
|
||||
|
||||
// 边框:始终绘制(合并一次 begin/end)
|
||||
drawOutlineOnce(bb, mesh2D);
|
||||
|
||||
// 内部线:大网格时跳过以减轻负担
|
||||
if (!large && mesh2D.getIndices() != null && mesh2D.getIndices().length >= 3) {
|
||||
// 合并内线绘制(批量)
|
||||
drawIndexedInternalLinesBatch(bb, mesh2D);
|
||||
}
|
||||
|
||||
// 顶点点标记:采样绘制以降低数量
|
||||
drawLiquifyVertexPointsOptimized(mesh2D, bb, pendingTexts, large, vertexCount);
|
||||
drawCompactLiquifyBadge(mesh2D, bb, bounds);
|
||||
}
|
||||
|
||||
} finally {
|
||||
RenderSystem.popState();
|
||||
}
|
||||
|
||||
// 延迟绘制文本(在外层安全)
|
||||
if (!pendingTexts.isEmpty()) {
|
||||
TextRenderer tr = ModelRender.getTextRenderer();
|
||||
if (tr != null) {
|
||||
for (TextItem ti : pendingTexts) {
|
||||
try {
|
||||
ModelRender.renderText(ti.text, ti.x, ti.y, ti.color);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 辅助方法 ==========
|
||||
|
||||
private void drawRoundedBoundingFill(BufferBuilder bb, BoundingBox bounds, Vector4f color, float padding) {
|
||||
float minX = bounds.getMinX() - padding;
|
||||
float minY = bounds.getMinY() - padding;
|
||||
float maxX = bounds.getMaxX() + padding;
|
||||
float maxY = bounds.getMaxY() + padding;
|
||||
|
||||
bb.begin(GL11.GL_TRIANGLES, 6);
|
||||
bb.setColor(color);
|
||||
bb.vertex(minX, minY, 0f, 0f);
|
||||
bb.vertex(maxX, minY, 0f, 0f);
|
||||
bb.vertex(maxX, maxY, 0f, 0f);
|
||||
|
||||
bb.vertex(maxX, maxY, 0f, 0f);
|
||||
bb.vertex(minX, maxY, 0f, 0f);
|
||||
bb.vertex(minX, minY, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
}
|
||||
|
||||
private void drawRoundedBoundingOutline(BufferBuilder bb, BoundingBox bounds, Vector4f color, float lineWidth, float padding) {
|
||||
float minX = bounds.getMinX() - padding;
|
||||
float minY = bounds.getMinY() - padding;
|
||||
float maxX = bounds.getMaxX() + padding;
|
||||
float maxY = bounds.getMaxY() + padding;
|
||||
|
||||
float lw = Math.max(1.0f, Math.min(lineWidth, 4.0f)); // 限制线宽,避免驱动开销
|
||||
GL11.glLineWidth(lw);
|
||||
bb.begin(GL11.GL_LINE_LOOP, 4);
|
||||
bb.setColor(color);
|
||||
bb.vertex(minX, minY, 0f, 0f);
|
||||
bb.vertex(maxX, minY, 0f, 0f);
|
||||
bb.vertex(maxX, maxY, 0f, 0f);
|
||||
bb.vertex(minX, maxY, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
GL11.glLineWidth(1.0f);
|
||||
}
|
||||
|
||||
// 合并绘制外轮廓(单次 begin/end)
|
||||
private void drawOutlineOnce(BufferBuilder bb, Mesh2D mesh2D) {
|
||||
if (mesh2D.getRenderVertices() == null || mesh2D.getRenderVertices().length < 4) return;
|
||||
Vector4f OUTER_LINE = new Vector4f(1f, 0.85f, 0.35f, 0.12f);
|
||||
int count = mesh2D.getRenderVertices().length / 2;
|
||||
GL11.glLineWidth(1.0f);
|
||||
bb.begin(GL11.GL_LINE_LOOP, count);
|
||||
bb.setColor(OUTER_LINE);
|
||||
float[] verts = mesh2D.getRenderVertices();
|
||||
for (int i = 0; i < count; i++) {
|
||||
int base = i * 2;
|
||||
bb.vertex(verts[base], verts[base + 1], 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
}
|
||||
|
||||
// 批量绘制内部索引线(只在小网格下使用,避免大量 drawcalls)
|
||||
private void drawIndexedInternalLinesBatch(BufferBuilder bb, Mesh2D mesh2D) {
|
||||
int[] idx = mesh2D.getIndices();
|
||||
if (idx == null || idx.length < 3) return;
|
||||
Vector4f innerLine = new Vector4f(1f, 0.6f, 0.0f, 0.06f);
|
||||
// 每三角产生3条线 => 最多 idx.length lines. 合并为一次 GL_LINES 绘制
|
||||
int lines = (idx.length / 3) * 6; // 每三角 6 顶点(3 条线)
|
||||
bb.begin(GL11.GL_LINES, lines);
|
||||
bb.setColor(innerLine);
|
||||
float[] verts = mesh2D.getRenderVertices();
|
||||
for (int i = 0; i < idx.length; i += 3) {
|
||||
int i1 = idx[i];
|
||||
int i2 = idx[i + 1];
|
||||
int i3 = idx[i + 2];
|
||||
// line i1-i2
|
||||
bb.vertex(verts[i1 * 2], verts[i1 * 2 + 1], 0f, 0f);
|
||||
bb.vertex(verts[i2 * 2], verts[i2 * 2 + 1], 0f, 0f);
|
||||
// line i2-i3
|
||||
bb.vertex(verts[i2 * 2], verts[i2 * 2 + 1], 0f, 0f);
|
||||
bb.vertex(verts[i3 * 2], verts[i3 * 2 + 1], 0f, 0f);
|
||||
// line i3-i1
|
||||
bb.vertex(verts[i3 * 2], verts[i3 * 2 + 1], 0f, 0f);
|
||||
bb.vertex(verts[i1 * 2], verts[i1 * 2 + 1], 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
}
|
||||
|
||||
// 顶点点标记 - 优化版:大网格时采样绘制、减少 segment、合并操作
|
||||
private void drawLiquifyVertexPointsOptimized(Mesh2D mesh2D, BufferBuilder bb, List<TextItem> pendingTexts, boolean large, int vertexCount) {
|
||||
if (mesh2D.getRenderVertices() == null) return;
|
||||
|
||||
final float BASE_SIZE = Math.max(1.5f, mesh2D.secondaryVertexSize * (large ? 0.35f : 0.6f));
|
||||
final Vector4f FILL = new Vector4f(1f, 0.6f, 0.15f, large ? 0.10f : 0.18f);
|
||||
final Vector4f STROKE = new Vector4f(1f, 1f, 1f, large ? 0.75f : 0.9f);
|
||||
|
||||
float[] verts = mesh2D.getRenderVertices();
|
||||
|
||||
// 采样步长,保证绘制点数量不会超过 TARGET_VERTEX_DRAW(越大网格步长越大)
|
||||
int step = 1;
|
||||
if (large && vertexCount > TARGET_VERTEX_DRAW) {
|
||||
step = Math.max(1, vertexCount / TARGET_VERTEX_DRAW);
|
||||
}
|
||||
|
||||
// 批量绘制填充圆(尽量减少 begin/end)
|
||||
// 我们使用较低分段数(segments)以降低每个圆的开销
|
||||
int segments = large ? 8 : 12;
|
||||
|
||||
// 为减少 draw calls:先填充所有圆(多个 TRIANGLE_FAN 批次合并不现实,因为每个中心不同)
|
||||
// 但我们至少减少每个圆的 segments 和避免不必要的计算
|
||||
for (int i = 0; i < vertexCount; i += step) {
|
||||
int base = i * 2;
|
||||
float x = verts[base];
|
||||
float y = verts[base + 1];
|
||||
|
||||
drawHollowCircleOptimized(bb, x, y, BASE_SIZE, FILL, STROKE, segments);
|
||||
}
|
||||
|
||||
// 编号仅在非常小网格或明确需要时绘制(避免大量文本)
|
||||
boolean showIndex = false;
|
||||
if (showIndex && !large) {
|
||||
TextRenderer tr = ModelRender.getTextRenderer();
|
||||
if (tr != null) {
|
||||
for (int i = 0; i < vertexCount; i += 1) {
|
||||
int base = i * 2;
|
||||
float x = verts[base];
|
||||
float y = verts[base + 1];
|
||||
String numberText = String.valueOf(i);
|
||||
float textWidth = tr.getTextWidth(numberText);
|
||||
float textX = x + 6.0f;
|
||||
float textY = y - 4.0f;
|
||||
bb.begin(GL11.GL_TRIANGLES, 6);
|
||||
bb.setColor(new Vector4f(0.06f, 0.06f, 0.06f, 0.7f));
|
||||
bb.vertex(textX - 2, textY - 8, 0f, 0f);
|
||||
bb.vertex(textX + textWidth + 2, textY - 8, 0f, 0f);
|
||||
bb.vertex(textX + textWidth + 2, textY + 2, 0f, 0f);
|
||||
bb.vertex(textX + textWidth + 2, textY + 2, 0f, 0f);
|
||||
bb.vertex(textX - 2, textY + 2, 0f, 0f);
|
||||
bb.vertex(textX - 2, textY - 8, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
pendingTexts.add(new TextItem(numberText, textX, textY, new Vector4f(1f,1f,1f,0.88f)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更轻量的空心圆绘制(segments 少)
|
||||
private void drawHollowCircleOptimized(BufferBuilder bb, float cx, float cy, float radius, Vector4f fill, Vector4f stroke, int segments) {
|
||||
if (radius <= 0f) return;
|
||||
segments = Math.max(6, Math.min(segments, 16));
|
||||
// 填充(每个圆仍需一次 TRIANGLE_FAN)
|
||||
bb.begin(GL11.GL_TRIANGLE_FAN, segments + 2);
|
||||
bb.setColor(fill);
|
||||
bb.vertex(cx, cy, 0f, 0f);
|
||||
for (int i = 0; i <= segments; i++) {
|
||||
double ang = 2.0 * Math.PI * i / segments;
|
||||
float x = cx + (float) Math.cos(ang) * radius;
|
||||
float y = cy + (float) Math.sin(ang) * radius;
|
||||
bb.setColor(fill);
|
||||
bb.vertex(x, y, 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
|
||||
// 描边(LINE_LOOP)
|
||||
bb.begin(GL11.GL_LINE_LOOP, segments);
|
||||
bb.setColor(stroke);
|
||||
for (int i = 0; i < segments; i++) {
|
||||
double ang = 2.0 * Math.PI * i / segments;
|
||||
float x = cx + (float) Math.cos(ang) * radius;
|
||||
float y = cy + (float) Math.sin(ang) * radius;
|
||||
bb.vertex(x, y, 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
}
|
||||
|
||||
// compact badge(小徽章)
|
||||
private void drawCompactLiquifyBadge(Mesh2D mesh2D, BufferBuilder bb, BoundingBox bounds) {
|
||||
float badgeX = bounds.getMinX() + (bounds.getWidth() * 0.06f);
|
||||
float badgeY = bounds.getMaxY() - (bounds.getHeight() * 0.06f);
|
||||
float r = Math.min(bounds.getWidth(), bounds.getHeight()) * 0.035f;
|
||||
r = Math.max(4f, Math.min(r, 18f));
|
||||
|
||||
int seg = 12;
|
||||
bb.begin(GL11.GL_TRIANGLE_FAN, seg + 1);
|
||||
bb.setColor(new Vector4f(1f, 0.6f, 0.1f, 0.62f));
|
||||
bb.vertex(badgeX, badgeY, 0f, 0f);
|
||||
for (int i = 0; i <= seg; i++) {
|
||||
float ang = (float) (i * 2 * Math.PI / seg);
|
||||
bb.vertex(badgeX + (float)Math.cos(ang) * r, badgeY + (float)Math.sin(ang) * r, 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
|
||||
drawSimpleBrushIcon(bb, badgeX, badgeY, r * 0.9f);
|
||||
}
|
||||
|
||||
private void drawSimpleBrushIcon(BufferBuilder bb, float centerX, float centerY, float size) {
|
||||
float iconSize = Math.max(1f, size * 0.6f);
|
||||
bb.begin(GL11.GL_LINES, 2);
|
||||
bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 0.9f));
|
||||
bb.vertex(centerX - iconSize * 0.3f, centerY - iconSize * 0.5f, 0f, 0f);
|
||||
bb.vertex(centerX, centerY + iconSize * 0.3f, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
bb.begin(GL11.GL_TRIANGLES, 3);
|
||||
bb.setColor(new Vector4f(1.0f, 0.3f, 0.0f, 0.9f));
|
||||
bb.vertex(centerX, centerY + iconSize * 0.3f, 0f, 0f);
|
||||
bb.vertex(centerX - iconSize * 0.2f, centerY + iconSize * 0.6f, 0f, 0f);
|
||||
bb.vertex(centerX + iconSize * 0.2f, centerY + iconSize * 0.6f, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
}
|
||||
|
||||
// 收集文本(在 pushState 内只收集,不渲染)
|
||||
private void collectLiquifyStatusText(Mesh2D mesh2D, List<TextItem> pendingTexts) {
|
||||
BoundingBox bounds = mesh2D.getBounds();
|
||||
if (bounds == null || !bounds.isValid()) return;
|
||||
String liquifyText = "Liquify";
|
||||
String hintText = "Ctrl: Show Vertices";
|
||||
TextRenderer tr = ModelRender.getTextRenderer();
|
||||
if (tr == null) return;
|
||||
|
||||
float centerX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f;
|
||||
float textY = bounds.getMaxY() + 20.0f;
|
||||
float textWidth = tr.getTextWidth("LIQUIFY MODE");
|
||||
float textX = centerX - textWidth / 2.0f;
|
||||
pendingTexts.add(new TextItem("LIQUIFY MODE", textX, textY, new Vector4f(1.0f, 0.8f, 0.0f, 1.0f)));
|
||||
|
||||
float indicatorX = bounds.getMaxX() + Math.max(bounds.getWidth(), bounds.getHeight()) * 0.08f;
|
||||
float indicatorY = bounds.getMaxY() - Math.max(bounds.getWidth(), bounds.getHeight()) * 0.06f;
|
||||
float tx = indicatorX - tr.getTextWidth(liquifyText) / 2f;
|
||||
float ty = indicatorY - Math.max(bounds.getWidth(), bounds.getHeight()) * 0.05f - 6f;
|
||||
pendingTexts.add(new TextItem(liquifyText, tx, ty, new Vector4f(1f, 0.84f, 0.36f, 0.88f)));
|
||||
pendingTexts.add(new TextItem(hintText, tx, ty + 14f, new Vector4f(0.8f, 0.8f, 0.8f, 0.65f)));
|
||||
}
|
||||
|
||||
// 临时文本项
|
||||
private record TextItem(String text, float x, float y, Vector4f color) { }
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util.tools;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.TextRenderer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.PuppetPin;
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator;
|
||||
import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement;
|
||||
import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Vector2f;
|
||||
import org.joml.Vector4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class PuppetDeformationRander extends RanderTools{
|
||||
|
||||
// 影响范围渲染颜色
|
||||
private final Vector4f influenceRangeColor = new Vector4f(0.3f, 0.3f, 1.0f, 0.3f); // 半透明蓝色
|
||||
private final Vector4f influenceBorderColor = new Vector4f(0.1f, 0.1f, 0.8f, 0.6f); // 边框颜色
|
||||
|
||||
@Override
|
||||
public void init(Map<String, Boolean> algorithmEnabled) {
|
||||
algorithmEnabled.put("showPuppetPins", false);
|
||||
algorithmEnabled.put("showInfluenceRanges", true); // 控制是否显示影响范围
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean render(Matrix3f modelMatrix, Object renderContext) {
|
||||
if (renderContext instanceof Mesh2D mesh2D) {
|
||||
drawPuppetPins(mesh2D, modelMatrix);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制木偶控制点
|
||||
*/
|
||||
private void drawPuppetPins(Mesh2D mesh2D, Matrix3f modelMatrix) {
|
||||
if (!isAlgorithmEnabled("showPuppetPins") || mesh2D.getPuppetPins().isEmpty()) return;
|
||||
|
||||
RenderSystem.pushState();
|
||||
try {
|
||||
ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader");
|
||||
if (solidShader != null && solidShader.programId != 0) {
|
||||
solidShader.use();
|
||||
|
||||
// 设置模型矩阵
|
||||
int modelLoc = solidShader.getUniformLocation("uModelMatrix");
|
||||
if (modelLoc != -1) {
|
||||
RenderSystem.uniformMatrix3(modelLoc, modelMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
Tesselator t = Tesselator.getInstance();
|
||||
BufferBuilder bb = t.getBuilder();
|
||||
|
||||
// 绘制控制点影响范围
|
||||
if (isAlgorithmEnabled("showInfluenceRanges")) {
|
||||
drawPuppetPinInfluenceRanges(mesh2D, bb);
|
||||
}
|
||||
|
||||
// 绘制控制点
|
||||
for (PuppetPin pin : mesh2D.getPuppetPins()) {
|
||||
Vector2f position = pin.getPosition();
|
||||
Vector4f color = pin.isSelected() ? mesh2D.getSelectedPuppetPinColor() : mesh2D.getPuppetPinColor();
|
||||
|
||||
drawPuppetPin(bb, position.x, position.y, color, mesh2D.getPuppetPinSize(), pin.isSelected());
|
||||
|
||||
// 为选中的控制点绘制信息
|
||||
if (pin.isSelected()) {
|
||||
drawPuppetPinInfo(mesh2D, bb, pin, position.x, position.y);
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
RenderSystem.popState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制控制点影响范围 - 显示实际的权重分布
|
||||
*/
|
||||
private void drawPuppetPinInfluenceRanges(Mesh2D mesh2D, BufferBuilder bb) {
|
||||
for (PuppetPin pin : mesh2D.getPuppetPins()) {
|
||||
Vector2f position = pin.getPosition();
|
||||
float radius = pin.getInfluenceRadius();
|
||||
|
||||
// 获取控制点的权重映射
|
||||
Map<Integer, Float> weightMap = pin.getWeightMap();
|
||||
if (weightMap == null || weightMap.isEmpty()) continue;
|
||||
|
||||
// 绘制影响范围的网格可视化 - 使用预计算的权重
|
||||
drawInfluenceGridFromWeightMap(mesh2D, bb, pin, position, radius, weightMap);
|
||||
|
||||
// 绘制影响范围边界
|
||||
drawInfluenceBoundary(bb, position, radius);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据预计算的权重映射绘制影响范围
|
||||
*/
|
||||
private void drawInfluenceGridFromWeightMap(Mesh2D mesh2D, BufferBuilder bb, PuppetPin pin,
|
||||
Vector2f position, float radius, Map<Integer, Float> weightMap) {
|
||||
// 创建网格来可视化影响范围
|
||||
int gridSize = 12; // 减少网格细分以提高性能
|
||||
float cellSize = radius * 2 / gridSize;
|
||||
|
||||
for (int i = 0; i < gridSize; i++) {
|
||||
for (int j = 0; j < gridSize; j++) {
|
||||
float x = position.x - radius + i * cellSize + cellSize / 2;
|
||||
float y = position.y - radius + j * cellSize + cellSize / 2;
|
||||
|
||||
// 计算该点到控制点的距离
|
||||
float dx = x - position.x;
|
||||
float dy = y - position.y;
|
||||
float distance = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance <= radius) {
|
||||
// 使用预计算的权重函数,确保与变形算法一致
|
||||
float weight = calculateWeight(distance, radius, pin.getFalloffType());
|
||||
|
||||
// 根据权重设置颜色强度
|
||||
Vector4f cellColor = new Vector4f(
|
||||
influenceRangeColor.x,
|
||||
influenceRangeColor.y,
|
||||
influenceRangeColor.z,
|
||||
influenceRangeColor.w * weight
|
||||
);
|
||||
|
||||
// 绘制网格单元
|
||||
drawInfluenceCell(bb, x, y, cellSize * 0.8f, cellColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制影响范围边界
|
||||
*/
|
||||
private void drawInfluenceBoundary(BufferBuilder bb, Vector2f position, float radius) {
|
||||
// 绘制边界圆圈
|
||||
bb.begin(GL11.GL_LINE_LOOP, 32);
|
||||
bb.setColor(influenceBorderColor);
|
||||
|
||||
for (int i = 0; i < 32; i++) {
|
||||
float angle = (float) (i * 2 * Math.PI / 32);
|
||||
float x = position.x + (float) Math.cos(angle) * radius;
|
||||
float y = position.y + (float) Math.sin(angle) * radius;
|
||||
bb.vertex(x, y, 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制影响范围网格单元
|
||||
*/
|
||||
private void drawInfluenceCell(BufferBuilder bb, float x, float y, float size, Vector4f color) {
|
||||
float halfSize = size / 2;
|
||||
|
||||
bb.begin(GL11.GL_TRIANGLES, 6);
|
||||
bb.setColor(color);
|
||||
|
||||
// 绘制小方块
|
||||
bb.vertex(x - halfSize, y - halfSize, 0f, 0f);
|
||||
bb.vertex(x + halfSize, y - halfSize, 0f, 0f);
|
||||
bb.vertex(x + halfSize, y + halfSize, 0f, 0f);
|
||||
|
||||
bb.vertex(x + halfSize, y + halfSize, 0f, 0f);
|
||||
bb.vertex(x - halfSize, y + halfSize, 0f, 0f);
|
||||
bb.vertex(x - halfSize, y - halfSize, 0f, 0f);
|
||||
|
||||
bb.endImmediate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算权重(与木偶变形算法保持一致)
|
||||
*/
|
||||
private float calculateWeight(float distance, float radius, PuppetPin.FalloffType falloffType) {
|
||||
if (distance >= radius) return 0.0f;
|
||||
|
||||
float normalizedDistance = distance / radius;
|
||||
|
||||
switch (falloffType) {
|
||||
case LINEAR:
|
||||
return 1.0f - normalizedDistance;
|
||||
case SMOOTH:
|
||||
return (float) (1.0f - Math.pow(normalizedDistance, 2));
|
||||
case SHARP:
|
||||
return (float) (1.0f - Math.pow(normalizedDistance, 0.5f));
|
||||
case CONSTANT:
|
||||
return 1.0f;
|
||||
default:
|
||||
return 1.0f - normalizedDistance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制木偶控制点(更醒目的样式)
|
||||
*/
|
||||
private void drawPuppetPin(BufferBuilder bb, float x, float y, Vector4f color, float size, boolean selected) {
|
||||
float halfSize = size / 2;
|
||||
|
||||
if (selected) {
|
||||
// 选中的控制点:带圆圈的十字
|
||||
bb.begin(GL11.GL_LINES, 4);
|
||||
bb.setColor(color);
|
||||
bb.vertex(x - halfSize, y, 0f, 0f);
|
||||
bb.vertex(x + halfSize, y, 0f, 0f);
|
||||
bb.vertex(x, y - halfSize, 0f, 0f);
|
||||
bb.vertex(x, y + halfSize, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
|
||||
// 外圈圆圈
|
||||
bb.begin(GL11.GL_LINE_LOOP, 16);
|
||||
bb.setColor(color);
|
||||
float circleSize = size * 1.5f;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
float angle = (float) (i * 2 * Math.PI / 16);
|
||||
float cx = x + (float) Math.cos(angle) * circleSize;
|
||||
float cy = y + (float) Math.sin(angle) * circleSize;
|
||||
bb.vertex(cx, cy, 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
} else {
|
||||
// 普通控制点:实心圆圈
|
||||
bb.begin(GL11.GL_TRIANGLE_FAN, 16);
|
||||
bb.setColor(color);
|
||||
bb.vertex(x, y, 0f, 0f); // 中心点
|
||||
for (int i = 0; i <= 16; i++) {
|
||||
float angle = (float) (i * 2 * Math.PI / 16);
|
||||
float cx = x + (float) Math.cos(angle) * halfSize;
|
||||
float cy = y + (float) Math.sin(angle) * halfSize;
|
||||
bb.vertex(cx, cy, 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
|
||||
// 边框
|
||||
bb.begin(GL11.GL_LINE_LOOP, 16);
|
||||
bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
for (int i = 0; i < 16; i++) {
|
||||
float angle = (float) (i * 2 * Math.PI / 16);
|
||||
float cx = x + (float) Math.cos(angle) * halfSize;
|
||||
float cy = y + (float) Math.sin(angle) * halfSize;
|
||||
bb.vertex(cx, cy, 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制控制点信息
|
||||
*/
|
||||
private void drawPuppetPinInfo(Mesh2D mesh2D, BufferBuilder bb, PuppetPin pin, float x, float y) {
|
||||
String infoText = pin.getName() + " (R:" + (int) pin.getInfluenceRadius() + ")";
|
||||
TextRenderer textRenderer = ModelRender.getTextRenderer();
|
||||
if (textRenderer != null) {
|
||||
float textWidth = textRenderer.getTextWidth(infoText);
|
||||
float textX = x + mesh2D.getPuppetPinSize() + 5.0f;
|
||||
float textY = y - 6.0f;
|
||||
bb.begin(GL11.GL_TRIANGLES, 6);
|
||||
bb.setColor(new Vector4f(0.1f, 0.1f, 0.1f, 0.8f));
|
||||
bb.vertex(textX - 3, textY - 10, 0f, 0f);
|
||||
bb.vertex(textX + textWidth + 3, textY - 10, 0f, 0f);
|
||||
bb.vertex(textX + textWidth + 3, textY + 4, 0f, 0f);
|
||||
bb.vertex(textX + textWidth + 3, textY + 4, 0f, 0f);
|
||||
bb.vertex(textX - 3, textY + 4, 0f, 0f);
|
||||
bb.vertex(textX - 3, textY - 10, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
ModelRender.renderText(infoText, textX, textY, new Vector4f(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,400 +1,143 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util.tools;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.TextRenderer;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Vertex;
|
||||
import com.chuangzhou.vivid2D.render.model.util.VertexTag;
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator;
|
||||
import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement;
|
||||
import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Vector2f;
|
||||
import org.joml.Vector4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* [MODIFIED] 顶点变形渲染工具
|
||||
* 该类现在渲染 Mesh2D 中被标记为 VertexTag.DEFORMATION 的一级顶点。
|
||||
*/
|
||||
public class VertexDeformationRander extends RanderTools {
|
||||
|
||||
// [NEW] 定义可变形顶点的默认渲染大小
|
||||
private static final float DEFORMATION_VERTEX_SIZE = 8.0f;
|
||||
|
||||
@Override
|
||||
public void init(Map<String, Boolean> algorithmEnabled) {
|
||||
algorithmEnabled.put("showSecondaryVertices", false);
|
||||
algorithmEnabled.put("showSecondaryVertexInfluence", true);
|
||||
// [MODIFIED] 更新算法名称以反映新功能
|
||||
algorithmEnabled.put("showDeformationVertices", false);
|
||||
algorithmEnabled.put("showDeformationVertexInfluence", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean render(Matrix3f modelMatrix, Object renderContext) {
|
||||
if (renderContext instanceof Mesh2D mesh2D){
|
||||
drawSecondaryVertices(mesh2D, modelMatrix);
|
||||
return true;
|
||||
if (renderContext instanceof Mesh2D mesh2D) {
|
||||
// [MODIFIED] 检查新的算法开关并调用新的渲染方法
|
||||
if (mesh2D.getStates("showDeformationVertices")) {
|
||||
drawDeformationVertices(mesh2D);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void drawSecondaryVertices(Mesh2D mesh2D, Matrix3f modelMatrix) {
|
||||
if (!isAlgorithmEnabled("showSecondaryVertices") || mesh2D.getSecondaryVertices().isEmpty() || !mesh2D.isShowSecondaryVertices()) return;
|
||||
/**
|
||||
* [MODIFIED] 重写此方法以渲染被标记为 DEFORMATION 的一级顶点。
|
||||
* @param mesh2D 要渲染的网格
|
||||
*/
|
||||
private void drawDeformationVertices(Mesh2D mesh2D) {
|
||||
// 1. 筛选出所有可变形的顶点
|
||||
List<Vertex> deformationVertices = mesh2D.getActiveVertexList().vertices.stream()
|
||||
.filter(v -> v.getTag() == VertexTag.DEFORMATION)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (deformationVertices.isEmpty()) {
|
||||
return; // 没有可变形顶点,无需渲染
|
||||
}
|
||||
|
||||
Tesselator t = Tesselator.getInstance();
|
||||
BufferBuilder bb = t.getBuilder();
|
||||
|
||||
RenderSystem.pushState();
|
||||
try {
|
||||
|
||||
ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader");
|
||||
if (solidShader != null && solidShader.programId != 0) {
|
||||
solidShader.use();
|
||||
int modelLoc = solidShader.getUniformLocation("uModelMatrix");
|
||||
if (modelLoc != -1) RenderSystem.uniformMatrix3(modelLoc, modelMatrix);
|
||||
}
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
GL11.glEnable(GL11.GL_LINE_SMOOTH);
|
||||
GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST);
|
||||
Tesselator t = Tesselator.getInstance();
|
||||
BufferBuilder bb = t.getBuilder();
|
||||
java.util.List<SecondaryVertex> verts = mesh2D.getSecondaryVertices();
|
||||
GL11.glDisable(GL11.GL_DEPTH_TEST);
|
||||
|
||||
// 1) 绘制所有控制点连线(按存储顺序),但使用 LINE_LOOP 以保证闭合(如果需要闭合)
|
||||
if (verts.size() >= 2) {
|
||||
// 如果顶点存储顺序不能保证多边形闭合,仍能作为参考线显示
|
||||
bb.begin(GL11.GL_LINE_STRIP, verts.size());
|
||||
bb.setColor(new Vector4f(1f, 1f, 1f, 0.12f));
|
||||
for (SecondaryVertex v : verts) {
|
||||
Vector2f p = v.getPosition();
|
||||
bb.vertex(p.x, p.y, 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
// 2. 绘制连接线
|
||||
drawConnectionLines(bb, deformationVertices);
|
||||
|
||||
// 3. 绘制每个可变形顶点
|
||||
for (Vertex vertex : deformationVertices) {
|
||||
Vector2f p = vertex.position;
|
||||
|
||||
// 定义颜色
|
||||
Vector4f centerCol = new Vector4f(0.1f, 0.9f, 0.2f, 0.7f); // 中心亮绿色
|
||||
Vector4f outerCol = new Vector4f(0.0f, 0.5f, 0.1f, 0.3f); // 外部深绿色
|
||||
Vector4f outlineCol = new Vector4f(1.0f, 1.0f, 1.0f, 0.9f); // 白色轮廓
|
||||
|
||||
// 绘制顶点视觉效果
|
||||
drawCircleGradient(bb, p.x, p.y, DEFORMATION_VERTEX_SIZE, centerCol, outerCol, 16);
|
||||
drawCircleOutline(bb, p.x, p.y, DEFORMATION_VERTEX_SIZE, outlineCol, 16);
|
||||
drawCircleOutline(bb, p.x, p.y, DEFORMATION_VERTEX_SIZE * 0.5f, new Vector4f(1.0f, 1.0f, 1.0f, 0.5f), 12);
|
||||
}
|
||||
|
||||
// 2) 收集被选中的点(以及可能的 preview),并构建一个**按极角排序的简单多边形**
|
||||
java.util.List<Vector2f> selPts = new java.util.ArrayList<>();
|
||||
for (SecondaryVertex sv : verts) if (sv.isSelected()) selPts.add(sv.getPosition());
|
||||
Vector2f preview = mesh2D.getPreviewPoint();
|
||||
if ((selPts.isEmpty() || selPts.size() < 3) && preview != null) {
|
||||
// 当选中点少于3时,把 preview 当作临时点
|
||||
selPts.add(preview);
|
||||
}
|
||||
|
||||
if (!selPts.isEmpty()) {
|
||||
// 去重
|
||||
java.util.Set<String> seen = new java.util.HashSet<>();
|
||||
java.util.List<Vector2f> uniq = new java.util.ArrayList<>();
|
||||
for (Vector2f v : selPts) {
|
||||
String k = String.format("%.6f_%.6f", v.x, v.y);
|
||||
if (!seen.contains(k)) {
|
||||
seen.add(k);
|
||||
uniq.add(new Vector2f(v.x, v.y));
|
||||
}
|
||||
}
|
||||
|
||||
// 生成一个按极角排序的多边形(相对于重心)
|
||||
java.util.List<Vector2f> poly = buildOrderedPolygon(uniq);
|
||||
|
||||
// 如果自交则替换为凸包(保证无自交)
|
||||
if (polygonIsSelfIntersecting(poly)) {
|
||||
poly = convexHull(poly);
|
||||
}
|
||||
|
||||
// 需要至少3点才能绘制填充多边形
|
||||
if (poly.size() >= 3) {
|
||||
drawSelectionPolygon(bb, poly);
|
||||
} else if (poly.size() == 2) {
|
||||
// 两点时绘制一条闭合的短线(可视化)
|
||||
bb.begin(GL11.GL_LINES, 2);
|
||||
bb.setColor(new Vector4f(0.95f, 0.6f, 0.15f, 0.28f));
|
||||
bb.vertex(poly.get(0).x, poly.get(0).y, 0f, 0f);
|
||||
bb.vertex(poly.get(1).x, poly.get(1).y, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
} else if (poly.size() == 1) {
|
||||
// 单点:画一个小圆做提示(在 draw loop 后会再次画点)
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 如果启用影响范围显示,显示 controlShape 的包围盒(针对被选中的顶点)
|
||||
if (isAlgorithmEnabled("showSecondaryVertexInfluence")) {
|
||||
for (SecondaryVertex vertex : verts) {
|
||||
if (vertex.isSelected()) {
|
||||
Vector2f min = vertex.getControlShape().getMinControlPoint();
|
||||
Vector2f max = vertex.getControlShape().getMaxControlPoint();
|
||||
Vector4f baseColor = mesh2D.selectedSecondaryVertexColor;
|
||||
if (max.x > min.x || max.y > min.y) {
|
||||
drawInfluenceBox(bb, min, max, baseColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 绘制二级顶点的点样式(阴影、渐变、高光、边框、pin/lock、编号)
|
||||
for (SecondaryVertex vertex : verts) {
|
||||
Vector2f position = vertex.getPosition();
|
||||
Vector4f baseColor = vertex.isSelected() ? mesh2D.selectedSecondaryVertexColor : mesh2D.secondaryVertexColor;
|
||||
float size = mesh2D.secondaryVertexSize;
|
||||
drawCircleSolid(bb, position.x + 2f, position.y - 2f, size * 0.8f, new Vector4f(0f, 0f, 0f, 0.22f), 20);
|
||||
Vector4f centerCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, Math.min(1.0f, baseColor.w + 0.15f));
|
||||
Vector4f outerCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, baseColor.w * 0.9f);
|
||||
drawCircleGradient(bb, position.x, position.y, size * 0.9f, centerCol, outerCol, 28);
|
||||
drawCircleSolid(bb, position.x - size * 0.12f, position.y + size * 0.12f, size * 0.22f,
|
||||
new Vector4f(1f, 1f, 1f, 0.75f), 12);
|
||||
drawCircleOutline(bb, position.x, position.y, size * 0.95f, new Vector4f(1f, 1f, 1f, 0.9f), 28);
|
||||
drawPinLockIcon(bb, vertex, position.x, position.y, size);
|
||||
if (vertex.isSelected()) drawVertexId(mesh2D, bb, vertex.getId(), position.x, position.y, size);
|
||||
}
|
||||
|
||||
} finally {
|
||||
GL11.glDisable(GL11.GL_LINE_SMOOTH);
|
||||
RenderSystem.popState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据输入点(任意顺序),生成一个“简单多边形”顺序:
|
||||
* - 先计算重心
|
||||
* - 按相对于重心的 atan2 排序(极角排序)
|
||||
* 该方法不会尝试消除自交,调用方可以在需要时再用 polygonIsSelfIntersecting() 检查并 fallback 到 convexHull()
|
||||
* [MODIFIED] 此方法现在接收一个 List<Vertex>
|
||||
* 绘制连接可变形顶点的线。
|
||||
*/
|
||||
private java.util.List<Vector2f> buildOrderedPolygon(java.util.List<Vector2f> pts) {
|
||||
java.util.List<Vector2f> out = new ArrayList<>();
|
||||
if (pts == null || pts.isEmpty()) return out;
|
||||
// 去重
|
||||
java.util.Set<String> seen = new java.util.HashSet<>();
|
||||
java.util.List<Vector2f> uniq = new ArrayList<>();
|
||||
for (Vector2f p : pts) {
|
||||
String k = String.format("%.6f_%.6f", p.x, p.y);
|
||||
if (!seen.contains(k)) { seen.add(k); uniq.add(new Vector2f(p.x, p.y)); }
|
||||
}
|
||||
if (uniq.size() <= 1) return new ArrayList<>(uniq);
|
||||
|
||||
// 计算重心
|
||||
Vector2f cen = new Vector2f(0f, 0f);
|
||||
for (Vector2f v : uniq) { cen.x += v.x; cen.y += v.y; }
|
||||
cen.x /= uniq.size(); cen.y /= uniq.size();
|
||||
|
||||
// 按角度排序
|
||||
uniq.sort(Comparator.comparingDouble(a -> Math.atan2(a.y - cen.y, a.x - cen.x)));
|
||||
|
||||
// 如果排序后首尾非常接近但方向不一致,尝试修正:确保多边形非自交的简单方式是返回它并由调用方处理
|
||||
return uniq;
|
||||
}
|
||||
|
||||
private boolean polygonIsSelfIntersecting(java.util.List<Vector2f> poly) {
|
||||
if (poly == null) return false;
|
||||
int n = poly.size();
|
||||
if (n < 4) return false;
|
||||
for (int i = 0; i < n; i++) {
|
||||
Vector2f a1 = poly.get(i);
|
||||
Vector2f a2 = poly.get((i + 1) % n);
|
||||
for (int j = i + 1; j < n; j++) {
|
||||
if (Math.abs(i - j) <= 1 || (i == 0 && j == n - 1)) continue;
|
||||
Vector2f b1 = poly.get(j);
|
||||
Vector2f b2 = poly.get((j + 1) % n);
|
||||
if (segmentsIntersect(a1.x, a1.y, a2.x, a2.y, b1.x, b1.y, b2.x, b2.y)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private java.util.List<Vector2f> convexHull(java.util.List<Vector2f> points) {
|
||||
java.util.List<Vector2f> pts = new java.util.ArrayList<>();
|
||||
if (points == null || points.isEmpty()) return pts;
|
||||
|
||||
java.util.Set<String> seen = new java.util.HashSet<>();
|
||||
for (Vector2f p : points) {
|
||||
String k = String.format("%.6f_%.6f", p.x, p.y);
|
||||
if (!seen.contains(k)) {
|
||||
seen.add(k);
|
||||
pts.add(new Vector2f(p.x, p.y));
|
||||
}
|
||||
}
|
||||
if (pts.size() <= 1) return new java.util.ArrayList<>(pts);
|
||||
|
||||
pts.sort((a, b) -> {
|
||||
int cmp = Float.compare(a.x, b.x);
|
||||
if (cmp != 0) return cmp;
|
||||
return Float.compare(a.y, b.y);
|
||||
});
|
||||
|
||||
java.util.List<Vector2f> lower = new java.util.ArrayList<>();
|
||||
for (Vector2f p : pts) {
|
||||
while (lower.size() >= 2) {
|
||||
Vector2f p1 = lower.get(lower.size() - 2);
|
||||
Vector2f p2 = lower.get(lower.size() - 1);
|
||||
if (orient(p1.x, p1.y, p2.x, p2.y, p.x, p.y) <= 0) {
|
||||
lower.remove(lower.size() - 1);
|
||||
} else break;
|
||||
}
|
||||
lower.add(p);
|
||||
}
|
||||
|
||||
java.util.List<Vector2f> upper = new java.util.ArrayList<>();
|
||||
for (int i = pts.size() - 1; i >= 0; i--) {
|
||||
Vector2f p = pts.get(i);
|
||||
while (upper.size() >= 2) {
|
||||
Vector2f p1 = upper.get(upper.size() - 2);
|
||||
Vector2f p2 = upper.get(upper.size() - 1);
|
||||
if (orient(p1.x, p1.y, p2.x, p2.y, p.x, p.y) <= 0) {
|
||||
upper.remove(upper.size() - 1);
|
||||
} else break;
|
||||
}
|
||||
upper.add(p);
|
||||
}
|
||||
|
||||
lower.remove(lower.size() - 1);
|
||||
upper.remove(upper.size() - 1);
|
||||
lower.addAll(upper);
|
||||
return lower;
|
||||
}
|
||||
|
||||
private boolean segmentsIntersect(double x1, double y1, double x2, double y2,
|
||||
double x3, double y3, double x4, double y4) {
|
||||
if (Math.max(x1, x2) < Math.min(x3, x4) || Math.max(x3, x4) < Math.min(x1, x2) ||
|
||||
Math.max(y1, y2) < Math.min(y3, y4) || Math.max(y3, y4) < Math.min(y1, y2)) {
|
||||
return false;
|
||||
}
|
||||
double d1 = orient(x3, y3, x4, y4, x1, y1);
|
||||
double d2 = orient(x3, y3, x4, y4, x2, y2);
|
||||
double d3 = orient(x1, y1, x2, y2, x3, y3);
|
||||
double d4 = orient(x1, y1, x2, y2, x4, y4);
|
||||
|
||||
if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
|
||||
((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Math.abs(d1) < 1e-10 && onSegment(x3, y3, x4, y4, x1, y1)) return true;
|
||||
if (Math.abs(d2) < 1e-10 && onSegment(x3, y3, x4, y4, x2, y2)) return true;
|
||||
if (Math.abs(d3) < 1e-10 && onSegment(x1, y1, x2, y2, x3, y3)) return true;
|
||||
if (Math.abs(d4) < 1e-10 && onSegment(x1, y1, x2, y2, x4, y4)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private double orient(double ax, double ay, double bx, double by, double cx, double cy) {
|
||||
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
||||
}
|
||||
|
||||
private boolean onSegment(double ax, double ay, double bx, double by, double px, double py) {
|
||||
double minx = Math.min(ax, bx) - 1e-8;
|
||||
double maxx = Math.max(ax, bx) + 1e-8;
|
||||
double miny = Math.min(ay, by) - 1e-8;
|
||||
double maxy = Math.max(ay, by) + 1e-8;
|
||||
return px >= minx && px <= maxx && py >= miny && py <= maxy &&
|
||||
Math.abs(orient(ax, ay, bx, by, px, py)) < 1e-8;
|
||||
}
|
||||
|
||||
|
||||
private void drawSelectionPolygon(BufferBuilder bb, java.util.List<Vector2f> poly) {
|
||||
if (poly == null || poly.size() < 3) return;
|
||||
|
||||
Vector4f fillCol = new Vector4f(0.95f, 0.6f, 0.15f, 0.06f);
|
||||
Vector4f edgeCol = new Vector4f(0.95f, 0.6f, 0.15f, 0.28f);
|
||||
|
||||
// 填充(使用 TRIANGLE_FAN)
|
||||
bb.begin(GL11.GL_TRIANGLE_FAN, poly.size() + 2);
|
||||
bb.setColor(fillCol);
|
||||
float cx = 0f, cy = 0f;
|
||||
for (Vector2f v : poly) { cx += v.x; cy += v.y; }
|
||||
cx /= poly.size(); cy /= poly.size();
|
||||
bb.vertex(cx, cy, 0f, 0f);
|
||||
for (Vector2f v : poly) {
|
||||
bb.setColor(fillCol);
|
||||
bb.vertex(v.x, v.y, 0f, 0f);
|
||||
}
|
||||
// 闭合回首点
|
||||
Vector2f first = poly.get(0);
|
||||
bb.setColor(fillCol);
|
||||
bb.vertex(first.x, first.y, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
|
||||
// 额外绘制多个渐变环用于视觉加强(可选)
|
||||
int rings = 3;
|
||||
float ringStep = 8f;
|
||||
for (int r = 1; r <= rings; r++) {
|
||||
float out = r * ringStep;
|
||||
java.util.List<Vector2f> expanded = new java.util.ArrayList<>();
|
||||
int n = poly.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
Vector2f prev = poly.get((i - 1 + n) % n);
|
||||
Vector2f curr = poly.get(i);
|
||||
Vector2f next = poly.get((i + 1) % n);
|
||||
float ex = next.x - prev.x;
|
||||
float ey = next.y - prev.y;
|
||||
float nx = -ey;
|
||||
float ny = ex;
|
||||
float len = (float)Math.sqrt(nx*nx + ny*ny) + 1e-9f;
|
||||
nx /= len; ny /= len;
|
||||
expanded.add(new Vector2f(curr.x + nx * out, curr.y + ny * out));
|
||||
}
|
||||
float alpha = 0.06f * (1f - (float)r / (rings + 1));
|
||||
Vector4f ringCol = new Vector4f(fillCol.x, fillCol.y, fillCol.z, alpha);
|
||||
bb.begin(GL11.GL_TRIANGLE_FAN, expanded.size() + 2);
|
||||
bb.setColor(ringCol);
|
||||
bb.vertex(cx, cy, 0f, 0f);
|
||||
for (Vector2f v : expanded) {
|
||||
bb.setColor(ringCol);
|
||||
bb.vertex(v.x, v.y, 0f, 0f);
|
||||
}
|
||||
bb.setColor(ringCol);
|
||||
bb.vertex(expanded.get(0).x, expanded.get(0).y, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
}
|
||||
|
||||
// 边框(使用 LINE_LOOP 保证闭合)
|
||||
bb.begin(GL11.GL_LINE_LOOP, poly.size());
|
||||
bb.setColor(edgeCol);
|
||||
for (Vector2f v : poly) {
|
||||
bb.vertex(v.x, v.y, 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
}
|
||||
|
||||
private void drawPredictedOutline(BufferBuilder bb, float[] predictedVertices, Mesh2D mesh2D) {
|
||||
if (predictedVertices == null || predictedVertices.length < 4) return;
|
||||
|
||||
bb.begin(GL11.GL_LINE_LOOP, predictedVertices.length / 2);
|
||||
bb.setColor(new Vector4f(0.95f, 0.6f, 0.15f, 0.28f));
|
||||
for (int i = 0; i < predictedVertices.length; i += 2) {
|
||||
bb.vertex(predictedVertices[i], predictedVertices[i + 1], 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
|
||||
int step = Math.max(1, (predictedVertices.length / 2) / 40);
|
||||
for (int i = 0; i < predictedVertices.length; i += 2 * step) {
|
||||
int j = (i + 2 * step) % predictedVertices.length;
|
||||
bb.begin(GL11.GL_LINES, 2);
|
||||
bb.setColor(new Vector4f(0.95f, 0.6f, 0.15f, 0.12f));
|
||||
bb.vertex(predictedVertices[i], predictedVertices[i + 1], 0f, 0f);
|
||||
bb.vertex(predictedVertices[j], predictedVertices[j + 1], 0f, 0f);
|
||||
bb.endImmediate();
|
||||
}
|
||||
|
||||
float psize = mesh2D.secondaryVertexSize * 0.6f;
|
||||
for (int i = 0; i < predictedVertices.length; i += 2) {
|
||||
drawCircleSolid(bb, predictedVertices[i], predictedVertices[i + 1], psize, new Vector4f(0.95f, 0.6f, 0.15f, 0.9f), 10);
|
||||
drawCircleOutline(bb, predictedVertices[i], predictedVertices[i + 1], psize * 0.9f, new Vector4f(1f,1f,1f,0.85f), 10);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawConnectionLines(BufferBuilder bb, Mesh2D mesh2D) {
|
||||
java.util.List<SecondaryVertex> verts = mesh2D.getSecondaryVertices();
|
||||
private void drawConnectionLines(BufferBuilder bb, List<Vertex> verts) {
|
||||
if (verts.size() < 2) return;
|
||||
|
||||
GL11.glLineWidth(1.0f);
|
||||
bb.begin(GL11.GL_LINE_STRIP, verts.size());
|
||||
bb.setColor(new Vector4f(1f, 1f, 1f, 0.12f));
|
||||
for (SecondaryVertex v : verts) {
|
||||
Vector2f p = v.getPosition();
|
||||
// 为了美观,我们对顶点进行排序以绘制一个简单的多边形轮廓
|
||||
List<Vector2f> positions = verts.stream().map(v -> v.position).collect(Collectors.toList());
|
||||
List<Vector2f> orderedPositions = buildOrderedPolygon(positions);
|
||||
|
||||
if (orderedPositions.size() < 2) return;
|
||||
|
||||
GL11.glLineWidth(1.5f);
|
||||
bb.begin(GL11.GL_LINE_LOOP, orderedPositions.size());
|
||||
bb.setColor(new Vector4f(1f, 1f, 1f, 0.25f));
|
||||
for (Vector2f p : orderedPositions) {
|
||||
bb.vertex(p.x, p.y, 0f, 0f);
|
||||
}
|
||||
bb.endImmediate();
|
||||
GL11.glLineWidth(1.0f);
|
||||
|
||||
for (int i = 0; i < verts.size(); i++) {
|
||||
SecondaryVertex a = verts.get(i);
|
||||
SecondaryVertex b = verts.get((i + 1) % verts.size());
|
||||
drawDashedLine(bb, a.getPosition().x, a.getPosition().y, b.getPosition().x, b.getPosition().y, 6, 3, new Vector4f(1f,1f,1f,0.06f));
|
||||
for (int i = 0; i < orderedPositions.size(); i++) {
|
||||
Vector2f a = orderedPositions.get(i);
|
||||
Vector2f b = orderedPositions.get((i + 1) % orderedPositions.size());
|
||||
drawDashedLine(bb, a.x, a.y, b.x, b.y, 6, 4, new Vector4f(1f,1f,1f,0.1f));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据输入点(任意顺序),生成一个“简单多边形”顺序:
|
||||
* - 先计算重心
|
||||
* - 按相对于重心的 atan2 排序(极角排序)
|
||||
*/
|
||||
private java.util.List<Vector2f> buildOrderedPolygon(java.util.List<Vector2f> pts) {
|
||||
if (pts == null || pts.size() <= 1) return new ArrayList<>(pts);
|
||||
|
||||
// 计算重心
|
||||
Vector2f cen = new Vector2f(0f, 0f);
|
||||
for (Vector2f v : pts) { cen.add(v); }
|
||||
cen.div(pts.size());
|
||||
|
||||
// 创建副本并按角度排序
|
||||
List<Vector2f> sortedPts = new ArrayList<>(pts);
|
||||
sortedPts.sort(Comparator.comparingDouble(p -> Math.atan2(p.y - cen.y, p.x - cen.x)));
|
||||
|
||||
return sortedPts;
|
||||
}
|
||||
|
||||
// --- 以下是无需修改的通用绘图和几何辅助方法 ---
|
||||
|
||||
private void drawDashedLine(BufferBuilder bb, float x1, float y1, float x2, float y2, float segmentLen, float gapLen, Vector4f color) {
|
||||
float dx = x2 - x1;
|
||||
float dy = y2 - y1;
|
||||
@@ -422,46 +165,6 @@ public class VertexDeformationRander extends RanderTools {
|
||||
}
|
||||
}
|
||||
|
||||
private void drawPinLockIcon(BufferBuilder bb, SecondaryVertex v, float px, float py, float size) {
|
||||
float iconSize = size * 0.9f;
|
||||
float ix = px + size * 0.9f;
|
||||
float iy = py + size * 0.2f;
|
||||
|
||||
if (v.isPinned()) {
|
||||
bb.begin(GL11.GL_TRIANGLES, 6);
|
||||
bb.setColor(new Vector4f(0.95f, 0.75f, 0.2f, 0.95f));
|
||||
bb.vertex(ix - iconSize*0.12f, iy - iconSize*0.3f, 0f, 0f);
|
||||
bb.vertex(ix + iconSize*0.12f, iy - iconSize*0.3f, 0f, 0f);
|
||||
bb.vertex(ix + iconSize*0.12f, iy + iconSize*0.15f, 0f, 0f);
|
||||
|
||||
bb.vertex(ix + iconSize*0.12f, iy + iconSize*0.15f, 0f, 0f);
|
||||
bb.vertex(ix - iconSize*0.12f, iy + iconSize*0.15f, 0f, 0f);
|
||||
bb.vertex(ix - iconSize*0.12f, iy - iconSize*0.3f, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
|
||||
drawCircleSolid(bb, ix, iy + iconSize*0.25f, iconSize*0.18f, new Vector4f(1f,1f,1f,0.9f), 12);
|
||||
}
|
||||
|
||||
if (v.isLocked()) {
|
||||
float lx = ix + iconSize * 0.6f;
|
||||
float ly = iy;
|
||||
float w = iconSize * 0.8f;
|
||||
float h = iconSize * 0.6f;
|
||||
bb.begin(GL11.GL_TRIANGLES, 6);
|
||||
bb.setColor(new Vector4f(0.16f,0.16f,0.16f,0.95f));
|
||||
bb.vertex(lx - w/2, ly - h/2, 0f, 0f);
|
||||
bb.vertex(lx + w/2, ly - h/2, 0f, 0f);
|
||||
bb.vertex(lx + w/2, ly + h/2, 0f, 0f);
|
||||
|
||||
bb.vertex(lx + w/2, ly + h/2, 0f, 0f);
|
||||
bb.vertex(lx - w/2, ly + h/2, 0f, 0f);
|
||||
bb.vertex(lx - w/2, ly - h/2, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
|
||||
drawCircleGradient(bb, lx, ly - h*0.15f, w*0.35f, new Vector4f(1f,1f,1f,0.95f), new Vector4f(1f,1f,1f,0.6f), 10);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawCircleSolid(BufferBuilder bb, float cx, float cy, float radius, Vector4f color, int segments) {
|
||||
if (radius <= 0f) return;
|
||||
segments = Math.max(6, segments);
|
||||
@@ -508,74 +211,4 @@ public class VertexDeformationRander extends RanderTools {
|
||||
}
|
||||
bb.endImmediate();
|
||||
}
|
||||
|
||||
private void drawInfluenceBox(BufferBuilder bb, Vector2f min, Vector2f max, Vector4f baseColor) {
|
||||
Vector4f fillCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, 0.08f);
|
||||
bb.begin(GL11.GL_QUADS, 4);
|
||||
bb.setColor(fillCol);
|
||||
bb.vertex(min.x, min.y, 0f, 0f);
|
||||
bb.vertex(max.x, min.y, 0f, 0f);
|
||||
bb.vertex(max.x, max.y, 0f, 0f);
|
||||
bb.vertex(min.x, max.y, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
|
||||
Vector4f edgeCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, 0.35f);
|
||||
GL11.glLineWidth(2.0f);
|
||||
bb.begin(GL11.GL_LINE_LOOP, 4);
|
||||
bb.setColor(edgeCol);
|
||||
bb.vertex(min.x, min.y, 0f, 0f);
|
||||
bb.vertex(max.x, min.y, 0f, 0f);
|
||||
bb.vertex(max.x, max.y, 0f, 0f);
|
||||
bb.vertex(min.x, max.y, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
GL11.glLineWidth(1.0f);
|
||||
}
|
||||
|
||||
private void drawVertexId(Mesh2D mesh2D, BufferBuilder bb, int id, float x, float y, float size) {
|
||||
String idText = String.valueOf(id);
|
||||
TextRenderer textRenderer = ModelRender.getTextRenderer();
|
||||
if (textRenderer != null) {
|
||||
float textWidth = textRenderer.getTextWidth(idText);
|
||||
float textX = x + size + 6.0f;
|
||||
float textY = y - size * 0.2f;
|
||||
|
||||
float padX = 6f;
|
||||
float padY = 4f;
|
||||
float left = textX - padX;
|
||||
float right = textX + textWidth + padX;
|
||||
float top = textY - 12f - padY;
|
||||
float bottom = textY + 4f + padY;
|
||||
|
||||
bb.begin(GL11.GL_TRIANGLES, 6);
|
||||
bb.setColor(new Vector4f(0f, 0f, 0f, 0.25f));
|
||||
float sx = 2f, sy = -2f;
|
||||
bb.vertex(left + sx, top + sy, 0f, 0f);
|
||||
bb.vertex(right + sx, top + sy, 0f, 0f);
|
||||
bb.vertex(right + sx, bottom + sy, 0f, 0f);
|
||||
bb.vertex(right + sx, bottom + sy, 0f, 0f);
|
||||
bb.vertex(left + sx, bottom + sy, 0f, 0f);
|
||||
bb.vertex(left + sx, top + sy, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
|
||||
bb.begin(GL11.GL_TRIANGLES, 6);
|
||||
bb.setColor(new Vector4f(0.06f, 0.06f, 0.06f, 0.88f));
|
||||
bb.vertex(left, top, 0f, 0f);
|
||||
bb.vertex(right, top, 0f, 0f);
|
||||
bb.vertex(right, bottom, 0f, 0f);
|
||||
bb.vertex(right, bottom, 0f, 0f);
|
||||
bb.vertex(left, bottom, 0f, 0f);
|
||||
bb.vertex(left, top, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
|
||||
bb.begin(GL11.GL_LINE_LOOP, 4);
|
||||
bb.setColor(new Vector4f(1f, 1f, 1f, 0.1f));
|
||||
bb.vertex(left, top, 0f, 0f);
|
||||
bb.vertex(right, top, 0f, 0f);
|
||||
bb.vertex(right, bottom, 0f, 0f);
|
||||
bb.vertex(left, bottom, 0f, 0f);
|
||||
bb.endImmediate();
|
||||
|
||||
ModelRender.renderText(idText, textX, textY, new Vector4f(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.LightSource;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.test;
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.test;
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.test;
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||
import org.lwjgl.glfw.GLFWVidMode;
|
||||
import org.lwjgl.opengl.GL;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.chuangzhou.vivid2D.test;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.transform.WaveDeformer;
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.test;
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.chuangzhou.vivid2D.test;
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.PhysicsSystem;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
@@ -8,10 +8,8 @@ import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.VertexDeformationTool;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex;
|
||||
import com.chuangzhou.vivid2D.render.model.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.util.ManagementDataToJsonConverter;
|
||||
import com.chuangzhou.vivid2D.util.ModelDataJsonConverter;
|
||||
import jnafilechooser.api.JnaFileChooser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -38,8 +36,6 @@ public class MainWindow extends JFrame {
|
||||
private final ParametersPanel parametersPanel;
|
||||
private final ModelPartInfoPanel partInfoPanel;
|
||||
|
||||
private final SecondaryVertexPanel secondaryVertexPanel;
|
||||
|
||||
private final KeyBindingManager keyBindingManager;
|
||||
public String currentModelPath = null;
|
||||
private JLabel statusBarLabel;
|
||||
@@ -61,7 +57,6 @@ public class MainWindow extends JFrame {
|
||||
this.transformPanel = new TransformPanel(renderPanel);
|
||||
this.parametersPanel = new ParametersPanel(renderPanel);
|
||||
this.partInfoPanel = new ModelPartInfoPanel(renderPanel);
|
||||
this.secondaryVertexPanel = new SecondaryVertexPanel();
|
||||
|
||||
createMenuBar();
|
||||
createToolBar();
|
||||
@@ -188,14 +183,10 @@ public class MainWindow extends JFrame {
|
||||
paramScroll.setBorder(BorderFactory.createTitledBorder("参数管理"));
|
||||
paramScroll.setPreferredSize(new Dimension(300, 200));
|
||||
|
||||
// 【修改布局】将 partInfoPanel 和 secondaryVertexPanel 放在一个垂直分割面板中
|
||||
secondaryVertexPanel.setBorder(BorderFactory.createTitledBorder("顶点信息"));
|
||||
secondaryVertexPanel.setPreferredSize(new Dimension(300, 200));
|
||||
|
||||
JSplitPane infoSplit = new JSplitPane(
|
||||
JSplitPane.VERTICAL_SPLIT,
|
||||
partInfoPanel,
|
||||
secondaryVertexPanel // 加入二级顶点面板
|
||||
paramScroll
|
||||
);
|
||||
infoSplit.setResizeWeight(0.5);
|
||||
infoSplit.setOneTouchExpandable(true);
|
||||
@@ -348,31 +339,7 @@ public class MainWindow extends JFrame {
|
||||
});
|
||||
|
||||
VertexDeformationTool vertexDeformationTool = (VertexDeformationTool) renderPanel.getToolManagement().getTool("顶点变形工具");
|
||||
|
||||
vertexDeformationTool.addChangeListener((part, mesh, vertex, type) -> {
|
||||
// 确保在 AWT 事件分发线程 (EDT) 更新 UI
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (vertex != null) {
|
||||
secondaryVertexPanel.setSecondaryVertex(vertex);
|
||||
}
|
||||
if (type == VertexDeformationTool.ChangeType.DELETE) {
|
||||
SecondaryVertex newSelected = vertexDeformationTool.getSelectedVertex();
|
||||
secondaryVertexPanel.setSecondaryVertex(newSelected);
|
||||
}
|
||||
if (type == VertexDeformationTool.ChangeType.CREATE ||
|
||||
type == VertexDeformationTool.ChangeType.MOVE ||
|
||||
type == VertexDeformationTool.ChangeType.DELETE) {
|
||||
setModelModified(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
renderPanel.getToolManagement().addToolChangeListener(newTool -> SwingUtilities.invokeLater(() -> {
|
||||
if (newTool instanceof VertexDeformationTool) {
|
||||
SecondaryVertex selectedVertex = ((VertexDeformationTool) newTool).getSelectedVertex();
|
||||
secondaryVertexPanel.setSecondaryVertex(selectedVertex);
|
||||
} else {
|
||||
secondaryVertexPanel.setSecondaryVertex(null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -384,7 +351,6 @@ public class MainWindow extends JFrame {
|
||||
transformPanel.setEnabled(enabled);
|
||||
parametersPanel.setEnabled(enabled);
|
||||
partInfoPanel.setEnabled(enabled);
|
||||
secondaryVertexPanel.setEnabled(enabled);
|
||||
renderPanel.setEnabled(enabled);
|
||||
for (Component comp : menuBar.getComponents()) {
|
||||
if (comp instanceof JMenu menu) {
|
||||
|
||||
Reference in New Issue
Block a user