59 Commits

Author SHA1 Message Date
7e97da60ff 更新 README.md
更改了项目地址
增加了官网网址
2025-11-02 13:40:00 +08:00
tzdwindows 7
0ad6835fed 上传模型文件 2025-11-02 11:05:21 +08:00
tzdwindows 7
c5097f91be feat(render): 实现 liquify overlay 显示控制与顶点同步优化
- 在 Mesh2D 中新增 isShowLiquifyOverlay 方法,用于控制 liquify overlay 的显示状态
- 修改 drawLiquifyOverlay 方法,增加对 mesh2D.isShowLiquifyOverlay() 的判断
- 重构 ModelPart 的 setPosition 方法,优化多选和单选状态下的顶点同步逻辑- 新增 syncSecondaryVerticesForPart 方法,实现部件及其子部件的二级顶点同步移动
- 移除 SelectionTool 中冗余的 syncSecondaryVerticesForPart 方法- 优化 SelectionTool 的 resize 操作逻辑,提高代码可读性和性能
- 在 VertexDeformationRander 中增加对 showSecondaryVertices 状态的检查- 完善多选操作时的中心点计算逻辑,提升用户体验
2025-11-01 19:17:03 +08:00
tzdwindows 7
5c66838b3e feat(render): 实现图层管理和渲染优化功能- 新增 LayerCellRenderer 类,用于渲染模型图层列表,支持可见性切换和缩略图显示- 添加 LayerOperationManager 类,提供图层的增删改查和视觉顺序调整功能
- 实现 LayerReorderTransferHandler 类,支持通过拖拽方式重新排列图层顺序- 优化 Mesh2D 类,引入 renderVertices 渲染缓存机制,提升渲染性能
- 完善二级顶点系统,增强网格变形算法,修复顶点移动和平移相关问题
- 改进三角分配变形算法,增加 pinned 控制点支持和整体位移校正
- 更新 GLContextManager任务队列处理逻辑,增加超时和中断处理机制- 修正模型包装器文档注释格式,提高代码可读性
2025-11-01 18:33:59 +08:00
tzdwindows 7
e06c59c8d1 refactor(ai):重构分割模型包装类继承结构- 将 Anime2ModelWrapper、Anime2VividModelWrapper 和 AnimeModelWrapper 改为继承自 VividModelWrapper 基类
- 移除重复的 ResultFiles 内部类和相关工具方法实现
- Anime2Segmenter 和 AnimeSegmenter 继承自抽象基类 Segmenter
- Anime2SegmentationResult与 AnimeSegmentationResult 继承 SegmentationResult
- 重命名 LabelPalette 为 BiSeNetLabelPalette 并调整其引用
- 更新模型路径配置以匹配新的文件命名约定
- 删除冗余的 getLabels() 和 getPalette() 方法定义
- 简化 segmentAndSave 方法中的类型转换逻辑- 移除已被继承方法替代的手动资源管理代码
- 调整 import 语句以反映包结构调整- 清理不再需要的独立主测试函数入口点- 修改字段访问权限以符合继承设计模式
- 替换具体的返回类型为更通用的 SegmentationResult 接口- 整合公共功能至基类减少子类间重复代码
- 统一分割后处理流程提高模块复用性
- 引入泛型支持增强 Wrapper 类型安全性
- 更新注释文档保持与最新架构同步
- 优化异常处理策略统一关闭资源方式
- 规范文件命名规则便于未来维护扩展
- 提取共通逻辑到父类降低耦合度
- 完善类型检查避免运行时 ClassCastException 风险
2025-10-31 09:25:18 +08:00
tzdwindows 7
a725e7eb23 feat(ai): 集成动漫人物分割与面部解析AI模型- 添加 DJL 深度学习框架依赖项以支持 PyTorch 和 ONNX Runtime 引擎
- 实现 Anime2VividModelWrapper 封装类用于动漫人物前景背景分离
- 开发 AnimeModelWrapper用于精细的动漫面部特征(如头发、眼睛)分割
- 创建配套的标签调色板和结果处理工具类提升可视化效果
- 增加多个测试用例验证不同AI模型的推理及文件输出功能
- 支持通过 synset.txt 自定义模型标签并增强命令行可测试性
2025-10-27 18:39:13 +08:00
tzdwindows 7
f2cb74379e feat(render): 实现网格顶点预测与控制点优化功能
- 添加 previewPoint 字段支持预览点显示
- 实现 predictVerticesWithTemporarySecondary 方法用于顶点变形预测
- 引入 SNAP_THRESHOLD 和 pinnedController 支持控制点吸附逻辑- 优化 updateVerticesFromSecondaryVertices 方法的三角分配策略
- 添加 moveSecondaryVertex 方法支持控制点移动与锁定逻辑
- 集成 RegionOptimizer 优化控制点半径分配- 移除冗余的 liquify 和 puppet 渲染代码进网格
- 改变形算法稳定性与性能表现
2025-10-26 18:37:55 +08:00
tzdwindows 7
401263cd2b feat(render): 实现液化工具及键盘快捷键管理
- 完全重写ModelRenderPanel
- 添加液化工具类,支持网格液化变形操作- 实现顶点渲染优化,提升大网格绘制性能
- 添加键盘管理器,支持多种快捷键操作- 实现摄像机控制与缩放功能
- 添加工具切换与状态管理功能
- 支持液化模式下的顶点显示控制
- 实现撤销/重做等编辑操作快捷键
2025-10-26 18:22:12 +08:00
tzdwindows 7
71aa2b8699 feat(render): 实现独立的 OpenGL 上下文管理器
- 将 GL 上下文管理从 ModelRenderPanel 抽离到独立的 GLContextManager 类- 实现离屏渲染上下文的创建、初始化和资源管理
- 支持动态调整渲染缓冲区大小和缩放功能
- 提供线程安全的任务队列机制用于在 GL 线程执行操作
- 实现像素数据读取和转换为 BufferedImage 的完整流程- 添加摄像机拖拽状态和缩放控制的支持
-重构 ModelRenderPanel以使用新的 GLContextManager- 更新所有 GL 相关操作的调用方式指向新的上下文管理器
- 修改 dispose 流程以正确释放所有 OpenGL 资源
- 优化渲染循环和平滑缩放逻辑实现
2025-10-26 10:57:54 +08:00
tzdwindows 7
43aab9f0fd refactor(render):优化渲染系统代码结构与字体加载逻辑- 简化模型点击监听器为 lambda 表达式- 移除未使用的 Mesh2D 和 ModelClickListener 导入- 使用方法引用替换匿名渲染调用- 重命名 getProgrami 方法为 getProgram
- 改进字体加载逻辑,支持多平台路径查找
- 添加字体文件不存在时的日志警告- 更新着色器程序链接与验证状态检查调用新方法名
2025-10-26 07:09:58 +08:00
tzdwindows 7
5775bc5d7e refactor(model):优化网格序列化逻辑并修复测试文件路径
- 使用Set避免重复序列化网格数据
- 在模型加载时自动补充缺失的网格引用
- 更新测试文件路径至统一的testing.model
- 移除冗余的部件位置设置代码
2025-10-25 17:41:29 +08:00
tzdwindows 7
3add504321 refactor(animation):优化动画系统字段不可变性与getter方法格式- 将AnimationClip中的creationTime字段设为final
- 将AnimationLayer中的parameterOverrides字段设为final
- 将AnimationParameter中的id、defaultValue、minValue、maxValue字段设为final
- 将LightSource中的position、color、intensity字段设为final
- 统一所有getter方法的代码格式,增加换行与大括号
- 优化Mesh2D中部分条件判断逻辑与字段final声明- 调整部分JavaDoc注释格式与空行位置提升可读性
2025-10-25 17:12:21 +08:00
tzdwindows 7
a9c2d202d3 refactor(animation):优化动画系统字段不可变性与getter方法格式- 将AnimationClip中的creationTime字段设为final
- 将AnimationLayer中的parameterOverrides字段设为final
- 将AnimationParameter中的id、defaultValue、minValue、maxValue字段设为final
- 将LightSource中的position、color、intensity字段设为final
- 统一所有getter方法的代码格式,增加换行与大括号
- 优化Mesh2D中部分条件判断逻辑与字段final声明- 调整部分JavaDoc注释格式与空行位置提升可读性
2025-10-25 17:11:51 +08:00
tzdwindows 7
1f5752257e feat(render): 添加木偶工具和二级顶点支持- 添加木偶控制点相关字段和方法- 实现木偶控制点的添加、移除和选择功能- 实现基于木偶控制点的网格变形算法
- 添加二级顶点支持及相关操作方法
- 实现二级顶点的渲染和交互功能- 添加变形冲突检测和解决机制
- 实现双线性插值和反距离加权插值算法- 添加控制点影响范围可视化
- 添加二级顶点与网格同步移动功能- 添加变形状态保存和重置功能
2025-10-25 17:05:04 +08:00
tzdwindows 7
cdc0843174 feat(render): 实现网格液化变形功能
- 添加向量变换工具方法,支持旋转和缩放变换
- 实现网格顶点的动态增删改功能
- 添加液化状态可视化渲染,包括顶点显示和状态指示器
- 支持创建细分网格以提高液化精度- 实现液化模式的交互控制,包括双击进入和快捷键操作- 添加液化画笔效果,支持推动、膨胀等多种变形模式- 完善网格数据结构,支持顶点数量动态变化时的UV和索引自动调整-优化选中框绘制逻辑,避免与顶点渲染冲突
2025-10-25 14:20:36 +08:00
tzdwindows 7
331d836d62 feat(render): 实现中文文本渲染与悬停提示功能- 在 Mesh2D 中增加悬停状态支持,允许显示红色边框和名称标签
- 添加 splitLines 方法支持文本自动换行显示
- 重构 TextRenderer 以支持 ASCII 和中文字符混合渲染
- 增加 getTextWidth 方法用于计算文本实际渲染宽度
- 修复 RenderSystem 中字体加载方法命名一致性问题- 调整 ModelRenderPanel 中坐标转换逻辑,确保拾取准确性
- 移除冗余的 Matrix3fUtils 引用,优化包导入结构- 完善 Mesh2D 绘制流程中的程序状态管理和纹理绑定操作- 为 Mesh2D 和 ModelPart 建立双向关联,便于获取模型部件名称
- 修改摄像机偏移计算方式,提高渲染坐标一致性
2025-10-25 10:08:09 +08:00
tzdwindows 7
d2bb534d26 Merge remote-tracking branch 'origin/master' 2025-10-24 21:09:05 +08:00
tzdwindows 7
210ac72a38 feat(render): 实现摄像机系统和文字渲染功能
- 添加 Camera 类,支持位置、缩放、Z轴控制- 在 ModelRender 中集成摄像机投影矩阵计算
- 实现屏幕坐标到世界坐标的转换方法
- 添加默认文字渲染器和字体加载逻辑
- 在渲染面板中添加摄像机控制的鼠标手势支持
- 支持通过鼠标滚轮进行摄像机缩放操作
- 添加摄像机状态显示和调试信息渲染
- 实现多选框渲染逻辑的重构和优化
-修复坐标系变换相关的边界框计算问题
- 增加摄像机启用/禁用快捷键支持cyon 等- 添加对 Linux 和 macOS 的 LWJGL 原生库支持
- 将任务定义方式从 task 改为 tasks.register 以提高性能
- 更新部分 JavaFX 和其他图形库的版本
-优化依赖项排列顺序,增强可读性与逻辑分组
2025-10-24 21:07:51 +08:00
tzdwindows 7
7ac960be5e feat(render): 实现摄像机系统和文字渲染功能
- 添加 Camera 类,支持位置、缩放、Z轴控制- 在 ModelRender 中集成摄像机投影矩阵计算
- 实现屏幕坐标到世界坐标的转换方法
- 添加默认文字渲染器和字体加载逻辑
- 在渲染面板中添加摄像机控制的鼠标手势支持
- 支持通过鼠标滚轮进行摄像机缩放操作
- 添加摄像机状态显示和调试信息渲染
- 实现多选框渲染逻辑的重构和优化
-修复坐标系变换相关的边界框计算问题
- 增加摄像机启用/禁用快捷键支持
2025-10-24 20:05:40 +08:00
tzdwindows 7
2278c5d0c7 chore(build): 更新构建脚本并优化操作历史日志
- 修改 runClient任务组和描述信息
- 添加多个 2D 模型测试任务 (test2DModelLayerPanel, testModelRenderLightingTest 等)
- 替换 System.out.println 日志为 SLF4J Logger 实现
- 移除冗余的日志打印和注释代码
- 统一使用占位符方式记录日志信息
- 注册和注销操作类型时增加日志跟踪
- 完善操作监听器添加与移除的日志提示
-优化异常处理中的错误日志输出
2025-10-22 22:33:15 +08:00
tzdwindows 7
fec5de1276 feat(render): 实现PSD文件导入和多选支持功能
- 添加PSD文件解析和图层导入功能- 实现多选状态下网格选择和边界框绘制
- 增加虚线边框和多选操作手柄显示
- 支持多选状态下点精确检测算法
- 添加拖拽操作历史记录功能
- 实现模型部件唯一命名避免冲突- 增加纹理垂直翻转和像素数据转换- 支持可见PSD图层性和不透明度设置
- 添加模型状态调试打印功能
-优化网格包含点检测逻辑和性能

重要更新
- 支持多选图层
- 支持导入psd文件
- 支持撤回和重做操作
2025-10-19 18:48:12 +08:00
tzdwindows 7
6a3eb89aaf feat(render): 实现模型部件变换控制面板
- 新增 TransformPanel 类,提供图形界面控制模型部件的位移、旋转、缩放和中心点
- 在 ModelLayerPanelTest 中集成变换面板,支持自动更新选中部件
- 为 ModelPart 添加事件系统,支持变换属性变更通知
- 实现 Mesh2D 的 pivot 和 originalPivot 分离,支持更精确的变换控制- 添加 ModelEvent 接口,用于模型部件事件触发机制
- 优化 ModelRenderPanel 的选中部件获取逻辑
- 完善模型点击监听器,支持自动切换到变换控制选项卡
-修复拖拽移动中心点时的边界检查问题
- 增强各变换操作的边界验证和错误处理
- 改进中心点绘制逻辑,增加边界检查和回退机制

重要更新
- 修复上个版本的所有问题,并且增加新的面板观测图层的各种信息
2025-10-18 15:27:04 +08:00
tzdwindows 7
b3c50ca794 feat(render): 添加网格中心点和旋转功能支持
- 为 Mesh2D 类添加 pivot 属性及对应的 getter/setter 方法
- 实现中心点和旋转手柄的可视化绘制逻辑
- 在 ModelRenderPanel 中新增旋转和移动中心点的交互模式
- 支持通过拖拽调整网格的中心点位置- 支持围绕自定义中心点进行旋转操作
- 更新 Mesh2D 的 copy、equals 和 hashCode 方法以包含 pivot 信息-优化选中网格的显示效果,添加多层边框和辅助标记
-修复 ModelPart 中设置中心点时的顶点坐标计算问题

(注意是测试版)
2025-10-17 21:28:25 +08:00
tzdwindows 7
879069a9f4 feat(render): 实现模型图层管理与选中高亮功能
- 添加 ModelLayerPanel 图层管理面板,支持图层增删、重排、重命名- 实现 Mesh2D 选中状态管理与可视化高亮边框绘制
- 添加模型点击与悬停事件监听接口 ModelClickListener
- 引入完整着色器接口 CompleteShader 及默认片段着色器实现
- 改进 BufferUploader 支持颜色 uniform 传递- 完善 Mesh2D 复制逻辑与边界框计算方法
- 重构部分工具类包路径并增强矩阵工具功能
- 移除 LightSourceData 中冗余的构造逻辑

重要更新
- 更新了一个可视化界面可以控制图层顺序(ModelLayerPanel),并且给ModelRenderPanel增加了很多新功能,比如设置模型图层位置、大小
- 重写了逻辑着色器(Shader)、BufferUploader逻辑,让着色器能够规范的注册和使用
2025-10-17 18:16:24 +08:00
tzdwindows 7
27744d4b5c refactor(render):重构渲染系统架构
- 将 BufferBuilder 移至 systems.buffer 包并增强功能- 添加 BuiltBuffer 和 RenderState 内部类支持状态管理- 新增 BufferUploader 类处理缓冲区上传和状态应用
- 引入 RenderSystem 统一封装 OpenGL 调用
- Mesh2D 和 ModelRender 更新使用新的渲染系统接口- ModelGLPanel 适配新包结构并使用 RenderSystem 初始化
- 移除旧版 LightSource 构造函数- 整体提升渲染代码的模块化和可维护性

重要更新
- 重写渲染器
- 移除辉光,采用旧版着色器渲染,任何有关辉光的将在下一个版本彻底删除
2025-10-17 01:48:07 +08:00
tzdwindows 7
1bc2634afb feat(render):重构 ModelGLPanel与 ModelRender 并增强渲染功能
- 重构 ModelGLPanel 支持动态尺寸调整和离屏渲染上下文重建
- 添加 GL 上下文任务队列机制,支持线程安全的 OpenGL 操作- 引入 SLF4J 日志系统替换原有 System.out 输出
- 优化像素读取逻辑,支持 ARGB 格式与图像缓冲复用- 增强错误处理与资源清理逻辑,提升稳定性
- 完善 Model2D与 ModelRender 类的文档注释与结构定义
- 新增 TestModelGLPanel 动画示例,展示模型部件控制与物理系统应用
2025-10-13 22:12:30 +08:00
tzdwindows 7
082478cdb6 feat(render): 实现高性能OpenGL渲染面板
- 添加ModelGLPanel类支持离屏OpenGL渲染
- 集成LWJGL3.3.6版本并更新相关依赖
- 实现模型树节点转换功能
- 添加纹理读取与显示错误处理机制
- 引入CommonMark库支持Markdown解析
-优化物理系统注释信息
- 禁用部分调试日志输出
- 添加测试用例TestModelGLPanel
2025-10-13 10:56:56 +08:00
tzdwindows 7
b501da0254 feat(model): 添加模型姿态管理系统- 新增 ModelPose 类用于管理模型部件的姿态数据
- 在 Model2D 中实现姿态保存、应用和混合功能- 支持姿态的序列化和反序列化
- 添加日志记录替代原有的 System.out 和 System.err 输出-优化网格和模型部件的调试信息输出
- 引入 PartPoseData 和 PoseData用于姿态数据持久化- 实现姿态间的平滑过渡和插值计算
- 增加默认姿态初始化和管理机制
2025-10-12 08:41:34 +08:00
tzdwindows 7
fb1db942ed refactor(model):重构模型数据包结构并增强光源系统
- 将 AnimationLayerData 类从 util 包移动到 data 包
- 将 BufferBuilder 类从 util 包移动到 buffer 包并更新包引用
- 为 LightSource 类添加辉光(Glow)支持及相关字段
- 扩展 LightSourceData 序列化类以包含辉光相关字段
- 新增 MeshData 类用于网格数据的序列化- 更新 Model2D 和 ModelData 的包引用以适应新的类结构
- 移除 ModelData 中重复的内部类定义,统一使用 data 包中的类- 为多个类添加作者信息注解
2025-10-12 08:16:42 +08:00
tzdwindows 7
22c3661d6e feat(model): 添加液化笔划数据的序列化与反序列化支持
- 在 ModelData.PartData 中新增 liquifyStrokes 字段用于保存液化笔划- 实现通过反射读取 ModelPart 的液化笔划数据(兼容旧版本)- 支持多种数据结构形式的液化点读取(Vector2f、自定义类、Map)
- 反序列化时自动重放液化笔划到 ModelPart- 添加 LiquifyStrokeData 和 LiquifyPointData 用于序列化存储
- 提供深度拷贝支持以确保 liquifyStrokes 数据完整复制
- 增加 ModelLoadTest 测试类用于验证模型加载与结构检查
2025-10-12 08:01:25 +08:00
tzdwindows 7
16af846e48 feat(render): 使用Color类替换Vector3f表示光源颜色
- 在LightSource类中引入java.awt.Color类型
- 添加colorToVector3f和vector3fToColor静态转换方法- 修改构造函数以接受Color参数并自动转换
- 更新LightSourceData反序列化逻辑以使用新颜色格式
- 在测试类中使用标准Color常量设置光源
- 移除旧的直接Vector3f颜色构造方式
2025-10-11 20:39:25 +08:00
tzdwindows 7
9cde0192fd feat(render): 添加光源与物理系统支持
- 新增 BufferBuilder 工具类用于简化顶点数据提交
- 实现 LightSource 和 LightSourceData 类以支持光源管理- 在 Model2D 中集成光源系统,支持序列化与反序列化
- 扩展 ModelData 以支持物理系统数据的完整序列化
- 重构 ModelRender以支持物理系统应用及碰撞箱渲染
- 添加粒子、弹簧、约束与碰撞体的数据结构与序列化逻辑
- 实现变形器的序列化接口以支持参数驱动动画的持久化
2025-10-11 20:21:11 +08:00
tzdwindows 7
22af92cd84 feat(model): 实现动画层数据序列化与纹理管理增强
- 添加 AnimationLayerData 类用于动画层的序列化支持- 增强 Model2D 的 addTexture 方法,添加空值检查和重复纹理处理
- 在 ModelData 中添加动画层序列化与反序列化逻辑
- 扩展 TextureData 结构以支持完整纹理参数和元数据- 改进纹理反序列化过程,添加错误处理和后备纹理创建- 更新模型测试用例以验证新功能和修复的问题
- 优化网格序列化逻辑,避免重复序列化相同网格- 添加日志记录支持以提高调试能力

重要
- 完全实现了模型的保存和加载(贴图待测试)
2025-10-08 21:02:46 +08:00
tzdwindows 7
424c00ede9 feat(render): 实现模型旋转中心点支持- 为 ModelPart 添加 pivot 属性,支持设置旋转中心点
- 更新局部变换矩阵计算,考虑 pivot 对旋转和平移的影响
- 在 Mesh2D 中增强着色器 uniform 设置,兼容 uModelMatrix 和 uModel- 添加 setPivot 和 getPivot 方法,支持动态调整旋转中心- 创建测试用例 ModelRenderTest2,验证不同 pivot 点的旋转效果
-修复纹理绑定逻辑,确保渲染时正确应用纹理
- 添加调试纹理生成功能,便于视觉验证 pivot 效果
2025-10-08 18:45:17 +08:00
tzdwindows 7
becf789cb8 feat(render): 实现模型渲染层级变换与网格世界坐标烘焙
- 为 Mesh2D 添加 getX/Y 方法并优化顶点访问逻辑
-修复 FloatBuffer 剩余空间判断逻辑
- 添加 bakedToWorld 标志支持网格世界坐标烘焙- 重构 ModelPart 变换更新逻辑,增加递归重计算
- 实现 ModelPart.draw() 方法支持 shader 传参绘制
- 更新 ModelRender 渲染流程,支持 worldTransform 传递
-修正网格顶点坐标上传逻辑,兼容 baked 状态- 移除废弃的调试与上传方法
- 增强部件变换时的局部与世界矩阵同步
- 修复 printWorldPosition 使用 worldTransform 坐标
- 调整测试模型初始位置与层级结构

重点:
- 修复了XY轴无法设置的重大问题
2025-10-08 16:49:26 +08:00
tzdwindows 7
52ed33b5c8 refactor(render):重构Mesh2D渲染逻辑并优化着色器代码
- 将Mesh2D的渲染方法移至Mesh2D类中,简化ModelRender职责
- 移除冗余的纹理绑定逻辑,交由Mesh.draw()处理
- 更新顶点着色器和片段着色器以支持调试模式- 弃用旧的uploadMeshData方法,改用Mesh.draw()
- 添加getVaoId方法暴露VAO ID用于外部访问-修正uniform location获取方式为静态导入- 添加调试输出用于网格顶点坐标检查
- 移除无用的注释和冗余变量声明
2025-10-08 15:33:26 +08:00
tzdwindows 7
173c30f277 feat(render):优化模型渲染与局部变换矩阵计算
- 精简 updateLocalTransform 方法注释并调整代码格式
- 修正局部变换矩阵的构建方式,明确先缩放再旋转的顺序
- 添加 printWorldPosition 方法用于调试世界坐标
- 在 ModelRender 中引入 Vector2f 类(暂未使用)- 调整 renderPartRecursive 方法逻辑结构并增加世界坐标打印注释- 移除冗余空行,提升代码可读性
2025-10-08 12:30:37 +08:00
tzdwindows 7
3cf7f5883c feat(anim): 实现2D模型动画系统核心类
- 添加AnimationClip类用于管理动画剪辑和关键帧
- 添加AnimationLayer类支持动画层和混合模式
- 实现动画曲线采样和插值算法
- 支持事件标记和动画状态控制
- 添加参数覆盖和权重混合功能
- 实现动画轨道和关键帧管理- 添加多种插值类型支持(线性、步进、平滑、缓入缓出)
- 实现动画事件系统和监听器模式
- 支持动画剪辑的深拷贝和合并功能
- 添加AnimationParameter类用于动画参数管理
2025-10-08 11:08:57 +08:00
tzdwindows 7
1e0aa62ca8 chore(version): 更新版本号至0.2.2
- 将VERSIONS常量从0.1.2更新为0.2.2
2025-10-07 17:08:36 +08:00
tzdwindows 7
efc73c935d feat(browser): 实现主题和字体动态更新功能
- 移除重复的字体信息注入逻辑
- 添加 updateTheme 方法统一处理主题和字体更新
- 在 setVisible 方法中调用 updateTheme 确保显示时更新
-优化 JavaScript 中的主题应用逻辑,增强兼容性
- 增强 HTML 页面中的主题监听和字体应用功能
- 添加事件计数器和调试信息用于追踪主题变化
2025-10-07 17:07:15 +08:00
tzdwindows 7
9eede23a94 feat(database): 实现表设计器和数据编辑功能
- 添加表设计器模态框,支持创建和修改表结构
- 实现列、索引、约束的动态添加和编辑功能- 增加数据表行数据的增删改查操作界面
- 添加工具面板的折叠展开功能和快速创建表按钮- 实现表搜索功能,支持按名称过滤表列表
- 更新Java后端模拟数据以支持新的表结构操作- 添加MySQL连接配置的字符集和编码设置
- 增加表设计器的表单控件和响应式布局样式
- 实列属性的完整现表设计器中配置选项
- 添加保存表结构时的数据收集和验证逻辑
2025-10-07 15:35:33 +08:00
tzdwindows 7
8f40542ab0 feat(browser): 添加数据库管理工具和JS对话框处理- 实现了浏览器窗口中的JavaScript alert弹窗拦截与处理
- 添加了数据库连接管理器,支持多种数据库类型(MySQL、PostgreSQL、SQLite、Oracle、H2)
- 开发了数据库管理工具的前端界面,包含连接配置、查询编辑器和结果展示
- 支持本地数据库创建与示例数据初始化
- 提供了数据库表结构管理和基础SQL执行功能- 增加了暗色主题切换和响应式布局设计
- 集成了事件日志面板用于调试和状态跟踪
2025-10-07 12:38:53 +08:00
tzdwindows 7
167bf6405f feat(theme): 实现Windows主题变更监听与动态更新
- 添加WindowsTheme工具类用于监听系统主题变更
- 实现runMonitorTopics方法监控主题变化并自动更新- 新增TopicsUpdateEvents事件类用于主题更新通知
- 重构setTopic方法使用updateTheme统一处理主题设置
-优化MainWindow背景透明度更新逻辑- 添加isSettingsVisible方法判断设置界面可见状态
- 移除RegisterTray类中的静态库加载代码
- 调整设置面板显示逻辑,支持重新显示已打开的设置窗口
-修复库加载错误日志信息
- 添加异常堆栈打印到崩溃报告组织方法中
2025-10-06 11:00:31 +08:00
tzdwindows 7
adf659853d feat(browser): 实现Java字体和主题动态同步到HTML界面
- 添加javaFontsLoaded和javaThemeChanged事件监听机制
- 在BrowserWindow和BrowserWindowJDialog中实现字体信息获取和注入
- 前端HTML文件增加对应的字体应用逻辑和样式更新
- 创建WindowRegistry统一管理窗口主题更新
- 更新README文档说明HTML事件使用方法- 支持Monaco和CodeMirror编辑器的字体动态调整
-优化CEF浏览器与Java UI的字体和主题同步流程
2025-10-05 19:49:53 +08:00
tzdwindows 7
f24e78ab95 feat(window):优化窗口重绘逻辑与主题更新
- 重构窗口重绘逻辑,区分全窗口重载与局部刷新
- 添加窗口内容清除与UI重新初始化流程
- 改进背景图片存在时的选择按钮背景色处理
- 更新语言配置文件中的时间戳与主题颜色选择器文本-修复窗口重绘时的残留背景问题
2025-10-05 18:49:49 +08:00
Vinfya
000ab3488b 哈哈哈哈哈哈哈哈哈哈哈哈哈哈 2025-10-05 17:06:26 +08:00
tzdwindows 7
d254e57e1f feat(decryption):重构QQ音乐解密工具并增强播放功能
- 新增音频播放功能,支持mp3/ogg/flac格式
- 实现可视化频谱显示与粒子效果- 添加播放列表管理与文件拖放支持
- 改进UI设计,使用现代化布局与配色方案
- 增加设置对话框,支持自定义输出路径
- 实现播放控制(播放/暂停/停止)与进度条拖动- 添加文件信息查看与资源管理器定位功能
-优化日志显示与错误处理机制
- 支持快捷键操作(空格切换播放/暂停)- 增强文件列表渲染,支持长文件名换行显示
2025-10-05 16:08:48 +08:00
tzdwindows 7
3d3b626c73 feat(box): 增加插件目录参数处理
- 新增插件目录参数解析逻辑
- 实现插件目录的动态设置
- 优化参数预处理,提高代码可读性和可维护性
2025-08-25 12:53:11 +08:00
tzdwindows 7
86a9e9e81d feat(RegisterTray): 重构并添加新功能
- 重构了 RegisterTray.dll 的核心逻辑,使用更现代的 Windows API
- 添加了自定义弹出菜单功能,支持鼠标悬停和点击事件
- 优化了托盘图标的创建和销毁流程
-改进了错误处理和资源管理- 新增 registerEx 方法,支持描述信息
2025-08-21 16:21:36 +08:00
tzdwindows 7
75f765bb47 feat: 添加暗黑主题配置文件
- 新增 dark.xml 文件,定义暗黑主题的配色方案
- 设置背景色、前景色和其他关键元素的颜色
- 包括关键字、括号、大括号、数字、注释、方法调用和类名的颜色配置
2025-08-19 12:57:34 +08:00
tzdwindows 7
0ec498f6eb Merge remote-tracking branch 'origin/master' 2025-08-19 12:54:31 +08:00
tzdwindows 7
e6df4fe4b2 feat(system-tools): 添加任务栏外观设置工具
- 新增 LocalCall 类实现任务栏外观设置功能- 添加 TaskbarAppearanceWindow 界面类
- 在 MainWindow 中集成任务栏主题设置工具
-优化图标路径处理逻辑
2025-08-19 12:37:50 +08:00
Hydrogen
b23662b861 fix(CasdoorLoginWindow): 修复登录界面右键菜单问题 2025-08-18 20:38:20 +08:00
tzdwindows 7
5df14e353a feat(box): 添加调试环境下的 F12 开发者工具快捷键
- 在 BrowserWindow 和 BrowserWindowJDialog 中添加了键盘事件处理,检测 F12 键- 按下 F12 键时,会立即创建并显示开发者工具
- 仅在调试环境下启用此功能,以避免在生产环境中暴露开发者工具
2025-08-18 19:20:24 +08:00
tzdwindows 7
a30c306cf1 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	src/main/java/com/axis/innovators/box/AxisInnovatorsBox.java
2025-08-18 08:06:41 +08:00
Hydrogen
628389150c fix(token,AxisInnovatorsBox) 修复报错NullPointerException,添加token持久化加解密逻辑 2025-08-18 02:16:16 +08:00
Hydrogen
2904258983 fix(CasdoorLoginWindow,resources): 修复了内嵌浏览器创建失败无法登录问题,完善登录逻辑的错误处理,添加无内嵌浏览器时使用默认浏览器登录的逻辑,补提交上次缺失的资源文件,为内嵌浏览器添加返回登录界面功能 2025-08-18 01:33:03 +08:00
Hydrogen
ba5c07746a feat(gui,login): 完成登录逻辑 2025-08-18 00:15:31 +08:00
Hydrogen
37ef4029b4 feat(gui,login): 完成登录逻辑 2025-08-17 21:19:18 +08:00
213 changed files with 51313 additions and 1406 deletions

8
.gitignore vendored
View File

@@ -40,3 +40,11 @@ bin/
### Mac OS ###
.DS_Store
### logs ###
*.log
logs/
### JCEF Dlls ###
library/*/

2
.idea/gradle.xml generated
View File

@@ -5,7 +5,7 @@
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="" />
<option name="gradleJvm" value="corretto-20" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

662
README.md Normal file
View File

@@ -0,0 +1,662 @@
# AxisInnovatorsBoxWindowApi
[项目链接](https://gitea.nimblenexa.cn/lanxi/window-axis-innovators-box) |
[官网](https://box.nimblenexa.cn)
| 简体中文
---
## 项目概述
`AxisInnovatorsBoxWindowApi` 是一个为 **AxisInnovatorsBox** 平台设计的管理API接口库开发者可通过此API创建自定义插件实现窗口管理、事件交互等核心功能。该仓库提供了接口定义、类说明文档及插件开发示例代码帮助开发者快速接入AxisInnovatorsBox生态系统。
---
## 功能特性
- 🖥️ **窗口生命周期管理** - 创建/销毁窗口、调整窗口状态(最小化/最大化)
- 🎮 **事件驱动交互** - 支持窗口事件监听与自定义事件触发
- 📦 **跨语言插件支持** - 基于 Java 平台无缝加载 Python 插件提供标准插件基类Java/Python实现快速扩展
- 📄 **动态配置管理** - 通过 properties 配置文件灵活加载多语言插件(支持 Java/Python 插件声明)
- 📊 **统一日志追踪** - 集成 Java 平台日志系统,同步记录 Python 插件的运行状态与异常信息
---
## 插件加载系统说明
- 插件加载系统核心组件。
- 插件加载系统由 **程序内部** 完成
### 注册Jar插件
- Jar插件在/plug-in中添加
```java
@PluginMeta(id = "test", name = "测试插件",
supportedVersions = {"0.0.2"},
description = "测试插件",
icon = "",
registeredName = "test")
public class Template {
public static PluginDescriptor INSTANCE = null;
public Template() {
GlobalEventBus.EVENT_BUS.register(this);
}
@SubscribeEvent
public void onStartup(StartupEvent event) {
MainWindow.ToolCategory category = new MainWindow.ToolCategory("测试插件", "test", "测试插件");
event.main().getRegistrationTool().addToolCategory(
category,
INSTANCE,
"templatePlugin"
);
}
}
```
- 插件加载系统会自动填充 **INSTANCE** 内容
- 使用PluginMeta注册插件信息
### Python插件注册
- Python插件在/plug-in/python中添加
- Python可以直接调用Java类实现对插件系统的控制
- 插件还需要单独的放在一个子文件夹中,如/plug-in/python/Examples
- Python插件需要声明一个metadata.json文件
```json
{
"id": "testing",
"name": "测试",
"version": "0.0.1",
"description": "测试插件",
"author": "tzdwindows 7",
"dependencies": [],
"_comment": {
"warning": "本文件为插件元数据配置,修改后需重启应用生效",
"path": "插件资源应放置在plugins/{id}/目录下"
}
}
```
- Python插件需要声明一个main.py文件做为插件的主脚本
```python
"""
工具模块初始化脚本
功能向Axis Innovators Box注册自定义工具类别和工具项
作者tzdwindows 7
版本1.1
"""
from com.axis.innovators.box.python import PyLocalSide
from javax.swing import AbstractAction
class MyAction(AbstractAction):
def actionPerformed(self, event):
"""工具项点击事件处理"""
print("[DEBUG] Tool item clicked! Event source:", event.getSource())
def onStartup():
"""
系统启动时自动执行的初始化逻辑
功能:
1. 创建工具类别
2. 创建工具项并绑定动作
3. 注册到系统全局工具集
"""
print('[INFO] 正在初始化自定义工具...')
# --------------------------
# 创建工具类别(参数顺序:显示名称,图标资源名,描述)
# --------------------------
tool_category = PyLocalSide.getToolCategory(
u"数据分析工具", # 显示名称GUI可见
u"analytics_icon.png", # 图标文件名(需存在于资源目录)
u"高级数据分析功能集合" # 悬停提示描述
)
# --------------------------
# 创建工具项参数顺序显示名称图标描述ID动作对象
# --------------------------
tool_action = MyAction()
tool_item = PyLocalSide.getToolItem(
u"数据可视化", # 工具项显示名称
u"chart_icon.png", # 工具项图标
u"生成交互式数据图表", # 工具项描述
1001, # 工具项唯一ID需在配置中统一管理
tool_action # 点击触发的动作
)
tool_category.addTool(tool_item)
# --------------------------
# 注册工具类别到系统(参数:类别对象,全局唯一注册名称)
# --------------------------
PyLocalSide.addToolCategory(
tool_category,
u"custom_module::data_analysis_tools" # 推荐命名规则:模块名::功能名
)
print('[SUCCESS] 工具类别注册成功')
if __name__ == '__main__':
result = 0
errorResult = ""
# 确保Jython运行时可以访问onStartup函数
# 原理:将函数显式绑定到全局字典
globals()['onStartup'] = onStartup
```
### 声明CorePlugins
* CorePlugins核心组件可以修改部分模块的字节码。
* CorePlugins需要在jar的属性中添加 **CorePlugins: CorePlugins类位置**
* 自动化构建在build.gradle中添加如下代码
```groovy
jar {
manifest {
attributes 'CorePlugin': 'com.axis.core.template.TemplateLoadingCorePlugin'
}
}
```
* CorePlugins核心组件需要实现 **CorePlugins** 接口,如:
```java
package com.axis.core.template;
import com.axis.innovators.box.plugins.LoadingCorePlugin;
import com.axis.innovators.template.Template;
/**
* 注册core插件
*/
public class TemplateLoadingCorePlugin implements LoadingCorePlugin {
@Override
public String getMainClass() {
// 返回主类名
return Template.class.getName();
}
@Override
public String[] getASMTransformerClass() {
// 返回字节码转换器类名
return new String[]{TemplateTransformer.class.getName()};
}
}
```
* IClassTransformer的实现
```java
package com.axis.core.template;
import com.axis.innovators.box.plugins.IClassTransformer;
import org.objectweb.asm.*;
/**
* core plugin transformer
* @author tzdwindows 7
*/
public class TemplateTransformer implements IClassTransformer {
@Override
public byte[] transform(String s, String s1, byte[] bytes) {
ClassReader classReader = new ClassReader(bytes);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
if ((access & Opcodes.ACC_PRIVATE) != 0) {
access = (access & ~Opcodes.ACC_PRIVATE) | Opcodes.ACC_PUBLIC;
System.out.println("Changing field access to public: " + name);
}
return super.visitField(access, name, descriptor, signature, value);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println(name + " , descriptor:" + descriptor);
if ((access & Opcodes.ACC_PRIVATE) != 0) {
access = (access & ~Opcodes.ACC_PRIVATE) | Opcodes.ACC_PUBLIC;
System.out.println("Changing method access to public: " + name);
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
};
classReader.accept(classVisitor, 0);
return classWriter.toByteArray();
}
}
```
## 事件系统说明
- 事件驱动架构核心组件
- 事件总线系统支持跨模块通信
- 事件总线由 **EventBus & GlobalEventBus** 实现
### 1.. EventBus & GlobalEventBus 说明
应用程序内的事件驱动架构核心组件
```java
package com.axis.innovators.box.events;
/**
* 事件总线系统(支持多总线实例隔离)
*/
public class EventBus {
// 核心方法
public void register(Object listener); // 注册监听器
public void unregister(Object target); // 注销监听器
public boolean post(Object event); // 发布事件
public void shutdown(); // 关闭总线
}
public class GlobalEventBus {
public static final EventBus EVENT_BUS = new EventBus(); // 全局单例总线
}
```
- **EventBus**:用于处理应用程序内各个模块之间的事件通信。
- **GlobalEventBus**:用于处理应用程序内各个模块之间的事件通信,支持多总线实例隔离。
### 2. EventBus & GlobalEventBus 使用示例
#### 示例1基础事件处理
```java
// 1. 定义事件类型
public class UserLoginEvent {
private final String username;
private boolean cancelled;
public UserLoginEvent(String username) {
this.username = username;
}
// Getter/Setter...
}
// 2. 创建监听器类
public class SecurityLogger {
@SubscribeEvent
public void logLoginAttempt(UserLoginEvent event) {
System.out.println("[安全审计] 登录尝试: " + event.getUsername());
}
}
// 3. 使用全局总线
public class Main {
public static void main(String[] args) {
// 注册监听器
GlobalEventBus.EVENT_BUS.register(new SecurityLogger());
// 模拟用户登录
UserLoginEvent loginEvent = new UserLoginEvent("admin");
GlobalEventBus.EVENT_BUS.post(loginEvent);
}
}
```
- 示例2事件取消机制
```java
// 1. 定义可取消事件
public class FileDeleteEvent {
private final Path filePath;
private boolean cancelled;
// 构造方法/getters/setters...
}
// 2. 创建权限校验监听器
public class PermissionValidator {
@SubscribeEvent
public void validateDeletePermission(FileDeleteEvent event) {
if (!checkAdminAccess()) {
event.setCancelled(true);
System.out.println("文件删除被拒绝:权限不足");
}
}
private boolean checkAdminAccess() {
// 权限校验逻辑
return false;
}
}
// 3. 主业务流程
public class FileManager {
public void deleteFile(Path path) {
FileDeleteEvent event = new FileDeleteEvent(path);
GlobalEventBus.EVENT_BUS.post(event);
if (!event.isCancelled()) {
// 执行删除操作
System.out.println("正在删除文件: " + path);
}
}
}
```
### 3.所有系统所支持的事件
- `com.axis.innovators.box.events.CategoryRenderingEvent`: 分类栏的渲染事件
- `com.axis.innovators.box.events.MainWindowEvents`: 主窗口事件
- `com.axis.innovators.box.events.OpenFileEvents`: 接收文件事件
- `com.axis.innovators.box.events.SettingsLoadEvents`: 程序初始化事件
- `com.axis.innovators.box.events.StartupEvent`: 程序启动事件
- `com.axis.innovators.box.events.TABUIEvents`: 选项卡Ui属性事件
## HTML窗口集成指南
我们实现了一套高性能的HTML渲染系统通过Java Chromium Embedded Framework (JCEF) 将HTML内容无缝集成到Java桌面应用中底层基于[jcefmaven](https://github.com/jcefmaven/jcefmaven)项目。
### 核心实现步骤
#### 1. 创建HTML窗口
```java
// 创建窗口引用
AtomicReference<BrowserWindowJDialog> htmlWindow = new AtomicReference<>();
SwingUtilities.invokeLater(() -> {
// 通过窗口注册表创建子窗口
WindowRegistry.getInstance().createNewChildWindow("main", builder -> {
htmlWindow.set(builder
.title("Axis Innovators Box AI 工具箱") // 窗口标题
.parentFrame(parentFrame) // 父级窗口
.icon(getApplicationIcon()) // 应用图标
.size(1280, 720) // 初始尺寸
.htmlPath(getHtmlResourcePath()) // HTML文件路径
.operationHandler(createOperationHandler()) // 自定义操作处理器
.build());
});
// 配置消息路由
configureMessageRouter(htmlWindow.get());
});
```
#### 2. 辅助方法
```java
// 获取应用图标
private Image getApplicationIcon() {
return new ImageIcon(Objects.requireNonNull(
MainApplication.class.getClassLoader()
.getResource("icons/logo.png")
)).getImage();
}
// 获取HTML资源路径
private String getHtmlResourcePath() {
return FolderCreator.getJavaScriptFolder() + "/AIaToolbox_dark.html";
}
```
#### 3. 配置消息路由器
```java
private void configureMessageRouter(BrowserWindowJDialog window) {
CefMessageRouter msgRouter = window.getMsgRouter();
if (msgRouter == null) return;
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
@Override
public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId,
String request, boolean persistent, CefQueryCallback callback) {
// 处理来自HTML的请求
handleBrowserRequest(request, callback);
return true; // 表示已处理该请求
}
@Override
public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) {
// 处理请求取消逻辑
System.out.println("请求被取消: " + queryId);
}
}, true); // true表示优先处理
}
```
## HTML事件
HTML窗口内可以捕捉到一些Java的事件
| 事件名 | 介绍 | 触发时机 |
|--------|------------|---------------------|
| `javaFontsLoaded` | Java字体加载完成 | Java字体信息传输到HTML时或在在更新主题时 |
| `javaThemeChanged` | 在主题发生变化时触发 | 在更新主题时 |
#### 具体示例
```javascript
// 监听Java字体加载事件
document.addEventListener('javaFontsLoaded', function(event) {
const fontInfo = event.detail;
console.log('接收到Java字体信息:', fontInfo);
// 应用Java字体到界面
applyJavaFonts(fontInfo);
});
// 监听Java主题变化事件
document.addEventListener('javaThemeChanged', function(event) {
const themeInfo = event.detail;
console.log('接收到Java主题信息:', themeInfo);
applyJavaTheme(themeInfo);
});
```
### 窗口管理系统说明
通过`WindowRegistry`统一管理应用窗口:
| 方法 | 说明 |
|------|------|
| `createNewWindow(String id, Consumer<Builder> config)` | 创建主窗口 |
| `createNewChildWindow(String id, Consumer<Builder> config)` | 创建模态子窗口 |
| `getWindow(String id)` | 获取已注册窗口 |
| `unregisterWindow(String id)` | 关闭指定窗口 |
### CefMessageRouter 使用指南
实现Java与JavaScript双向通信的核心组件
1. **消息处理流程**
- JavaScript → Java: 通过`window.cefQuery()`发送请求
- Java → JavaScript: 使用`CefFrame.executeJavaScript()`执行脚本
2. **核心方法**
```java
// JavaScript调用示例
function callJavaMethod(data) {
window.cefQuery({
request: JSON.stringify(data),
onSuccess: response => console.log("Success:", response),
onFailure: (err, msg) => console.error("Error:", msg)
});
}
```
3. **最佳实践**
- 使用JSON格式进行数据交换
- 为不同功能模块使用独立的路由处理器
- 在窗口关闭前移除所有路由处理器
### 生命周期管理
```java
// 关闭窗口时清理资源
htmlWindow.get().addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
window.getMsgRouter().dispose();
CefApp.getInstance().dispose();
}
});
```
## 核心类说明
### 1. AxisInnovatorsBox
窗口实例的核心操作类,提供以下功能:
- `getMain()`: 获取当前AxisInnovatorsBox实例
- `getMainWindow()`: 获取主窗口实例
- `quit()`: 退出程序
- `organizingCrashReports(Exception)`: 组织崩溃报告,用于在应用程序发生异常时生成崩溃报告。
- `popupWindow(WindowsJDialog)`: 弹出新的窗口,并将其添加到窗口列表中。
- `isWindowStartup(WindowsJDialog)`: 判断指定的窗口是否已经启动。
- `clearWindow(WindowsJDialog)`: 清除指定的窗口,并将其从窗口列表中移除。
- `reloadAllWindow()`: 重新加载窗口。
- `getRegistrationTool()`: 获取注册工具实例。
- `getArgs()`: 获取命令行参数。
- `isWindow()`: 判断窗口是否已经启动。
- `getVersion()`: 获取应用程序的版本号。
- `getRegistrationTopic()`: 获取注册主题实例。
- `getAuthor()`: 获取应用程序的作者信息。
- `getStateManager()`: 获取状态管理器实例。
### 2. RegistrationTool
负责在 **应用程序启动阶段** 注册和管理工具分类的核心组件,具备插件系统集成能力:
```java
package com.axis.innovators.box.register;
/**
* 工具分类注册中心(窗口启动前必须完成注册)
*/
public class RegistrationTool {
// 构造方法关联主程序实例
public RegistrationTool(AxisInnovatorsBox main) { ... }
// 核心功能方法
public boolean addToolCategory(ToolCategory category, String regName);
public void addToolCategory(ToolCategory category, PluginDescriptor descriptor, String regName);
public ToolCategory getToolCategory(UUID id);
public UUID getUUID(String registeredName);
}
```
- `addToolCategory(ToolCategory category, String regName)`: 向工具分类注册中心添加一个新的工具分类。
- `addToolCategory(ToolCategory category, PluginDescriptor descriptor, String regName)`: 向工具分类注册中心添加一个新的工具分类,同时关联插件描述符。
- `getToolCategory(UUID id)`: 通过UUID获取工具分类。
- `getUUID(String registeredName)`: 通过注册名称获取UUID。
#### 注册示例
```java
// 创建调试工具分类
MainWindow.ToolCategory debugCategory = new MainWindow.ToolCategory(
"逆向分析工具",
"icons/debugger.png",
"二进制逆向分析工具集"
);
// 添加工具项(带点击事件)
debugCategory.addTool(new MainWindow.ToolItem(
"内存分析器",
"icons/memory.png",
"实时查看进程内存映射",
1,
new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(null, "启动内存分析模块...");
}
}
));
// 注册到系统(必须在窗口初始化前完成)
try {
registrationTool.addToolCategory(debugCategory, "system:reverseEngineering");
} catch (RegistrationError ex) {
System.err.println("注册失败: " + ex.getMessage());
}
```
### 3. RegistrationTopic
负责在 **应用程序初始化阶段** 统一管理UI主题注册的核心组件支持类名/LookAndFeel双模式主题注入
```java
package com.axis.innovators.box.register;
/**
* 主题注册中心(窗口初始化前必须完成注册)
*/
public class RegistrationTopic {
// 核心注册方法
public void addTopic(String topicClass, String name, String tip, Icon icon, String regName);
public void addTopic(LookAndFeel laf, String name, String tip, Icon icon, String regName);
// 状态管理方法
public boolean isLoading(String themeName);
public void setLoading(String themeName);
}
```
- `addTopic(String topicClass, String name, String tip, Icon icon, String regName)`: 向主题注册中心添加一个新的主题。
- `addTopic(LookAndFeel laf, String name, String tip, Icon icon, String regName)`: 向主题注册中心添加一个新的主题同时关联LookAndFeel。
- `isLoading(String themeName)`: 判断指定主题是否正在加载。
- `setLoading(String themeName)`: 设置指定主题为正在加载状态。
#### 注册示例
```java
try {
// 重复注册相同名称
topicRegistry.addTopic("com.axis.light.MaterialTheme",
"质感浅色",
"Material Design风格",
materialIcon,
"theme:light"); // 已存在同名注册
} catch (RegistrationError ex) {
// 捕获异常并提示theme:light duplicate registered names
JOptionPane.showMessageDialog(null, ex.getMessage());
}
```
### 4. StateManager
应用程序状态管理工具类,提供跨会话的配置持久化能力:
```java
package com.axis.innovators.box.tools;
/**
* 状态持久化管理器(线程安全)
*/
public class StateManager {
// 构造方法
public StateManager(); // 默认使用toolbox.properties
public StateManager(String customFileName); // 自定义状态文件名
// 核心操作方法
public void saveState(String key, [int|long|boolean...] value);
public [String|int|boolean...] getStateAs[Type](String key);
}
```
- `saveState(String key, [int|long|boolean...] value)`: 保存状态到配置文件。
- `getStateAs[Type](String key)`: 从配置文件获取状态。
- `[String|int|boolean...]`: 支持多种数据类型保存到配置文件,并支持多种数据类型从配置文件获取。
### 5. RegistrationSettingsItem
负责管理系统 **设置中心** 的配置面板注册,支持插件化扩展设置项的核心组件:
```java
package com.axis.innovators.box.register;
/**
* 设置项注册中心(集成插件配置扩展能力)
*/
public class RegistrationSettingsItem extends WindowsJDialog {
// 核心注册方法
public void addSettings(JPanel panel, String title, Icon icon, String tip, String regName);
public void addSettings(JPanel panel, String title, Icon icon, String tip,
PluginDescriptor plugin, String regName);
// 查询方法
public static List<RegistrationSettingsItem> getRegistrationsByPlugin(PluginDescriptor plugin);
}
```
- `addSettings(JPanel panel, String title, Icon icon, String tip, String regName)`: 向设置项注册中心添加一个新的设置项。
- `addSettings(JPanel panel, String title, Icon icon, String tip, PluginDescriptor plugin, String regName)`: 向设置项注册中心添加一个新的设置项,同时关联插件描述符。
- `getRegistrationsByPlugin(PluginDescriptor plugin)`: 通过插件描述符获取关联的设置项列表。
### 6. LanguageManager
应用程序多语言管理核心组件,支持动态加载与合并多语言资源:
```java
package com.axis.innovators.box.register;
/**
* 国际化语言管理中心(支持插件扩展语言包)
*/
public class LanguageManager {
// 核心操作方法
public static void addLanguage(Language lang);
public static void loadLanguage(String regName);
public static Language getLoadedLanguages();
public static Language getLanguage(String identifier);
}
```
- `addLanguage(Language lang)`: 向语言管理中心添加一个新的语言包。
- `loadLanguage(String regName)`: 加载指定语言包。
- `getLoadedLanguages()`: 获取当前系统加载的语言包。
- `getLanguage(String identifier)`: 通过标识符获取语言包。

View File

@@ -1,3 +1,5 @@
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
plugins {
id 'java'
id 'application'
@@ -13,6 +15,7 @@ configurations {
proguardLib
}
// JDK 版本检查
def requiredJavaVersion = 20
def currentJavaVersion = JavaVersion.current().majorVersion.toInteger()
@@ -30,92 +33,169 @@ repositories {
mavenCentral()
}
//tasks.named("bootJar") {
// enabled = false
//}
dependencies {
// === 构建工具 ===
proguardLib files('libs/proguard.jar')
// === 测试框架 ===
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
implementation 'org.commonmark:commonmark:0.24.0'
implementation 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.1'
implementation 'com.google.code.gson:gson:2.8.9'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
// === 开发工具 ===
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// === 本地库文件 ===
implementation files('libs/JNC-1.0-jnc.jar')
implementation files('libs/dog api 1.3.jar')
implementation files('libs/DesktopWallpaperSdk-1.0-SNAPSHOT.jar')
// === DJL API ===
implementation platform('ai.djl:bom:0.35.0')
implementation 'ai.djl:api'
implementation 'ai.djl:model-zoo'
implementation 'ai.djl.pytorch:pytorch-model-zoo:0.35.0'
implementation 'ai.djl.pytorch:pytorch-engine'
implementation 'ai.djl:basicdataset'
implementation 'ai.djl.onnxruntime:onnxruntime-engine'
runtimeOnly 'ai.djl.pytorch:pytorch-native-cpu:2.7.1'
runtimeOnly 'ai.djl.onnxruntime:onnxruntime-native-cpu:1.3.0'
// === 核心工具库 ===
implementation 'com.google.code.gson:gson:2.10.1' // 统一版本
implementation 'org.apache.logging.log4j:log4j-api:2.20.0'
implementation 'org.apache.logging.log4j:log4j-core:2.20.0'
implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.20.0'
implementation 'commons-io:commons-io:2.18.0' // 统一版本
implementation 'com.google.guava:guava:31.1-jre'
implementation 'net.java.dev.jna:jna:5.13.0'
implementation 'net.java.dev.jna:jna-platform:5.13.0'
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'org.apache.commons:commons-compress:1.23.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
// === 字节码操作 ===
implementation 'org.ow2.asm:asm:9.7.1'
implementation 'org.ow2.asm:asm-commons:9.7.1'
implementation 'org.ow2.asm:asm-analysis:9.7.1'
implementation 'org.ow2.asm:asm-util:9.7.1'
implementation 'org.ow2.asm:asm-tree:9.7.1'
implementation 'net.bytebuddy:byte-buddy:1.17.6'
// === 反编译工具 ===
implementation 'org.bitbucket.mstrobel:procyon-core:0.6.0' // 统一版本
implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.6.0' // 统一版本
implementation 'org.benf:cfr:0.152'
// === Java 解析与分析 ===
implementation 'com.github.javaparser:javaparser-core:3.25.1'
implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.25.9'
// === 文本处理 ===
implementation 'org.commonmark:commonmark:0.24.0'
implementation 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.1'
implementation 'com.vladsch.flexmark:flexmark:0.64.8'
// === Web 和网络 ===
implementation 'org.jsoup:jsoup:1.17.2'
implementation 'commons-io:commons-io:2.14.0'
implementation 'org.json:json:20231013' // 统一版本
implementation 'org.openjfx:javafx-web:17'
// === UI 框架 ===
implementation 'com.formdev:flatlaf:3.2.1'
implementation 'com.formdev:flatlaf-extras:3.2.1'
implementation 'com.formdev:flatlaf-intellij-themes:3.2.1'
implementation 'io.github.vincenzopalazzo:material-ui-swing:1.1.2'
implementation 'org.python:jython-standalone:2.7.3'
implementation 'org.graalvm.python:python-embedding:24.2.1'
implementation files('libs/JNC-1.0-jnc.jar')
implementation files('libs/dog api 1.3.jar')
implementation files('libs/DesktopWallpaperSdk-1.0-SNAPSHOT.jar')
// JavaFX
implementation 'org.openjfx:javafx-controls:21'
implementation 'org.openjfx:javafx-graphics:21'
implementation 'org.fxmisc.richtext:richtextfx:0.11.0'
implementation 'org.bitbucket.mstrobel:procyon-core:0.5.36'
implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.5.36'
implementation 'org.controlsfx:controlsfx:11.1.2'
implementation 'com.dlsc.formsfx:formsfx-core:11.6.0'
implementation 'com.dustinredmond.fxtrayicon:FXTrayIcon:4.0.1'
// === 代码编辑器 ===
implementation 'com.fifesoft:rsyntaxtextarea:3.5.4'
implementation 'com.fifesoft:rstaui:3.3.1'
implementation 'com.fifesoft:languagesupport:3.3.0'
implementation 'com.fifesoft:autocomplete:3.3.2'
implementation 'org.apache.commons:commons-compress:1.23.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
implementation 'org.controlsfx:controlsfx:11.1.2'
implementation 'com.dlsc.formsfx:formsfx-core:11.6.0'
// === 图形和游戏引擎 ===
// LWJGL
implementation 'org.lwjgl:lwjgl:3.3.6'
implementation 'org.lwjgl:lwjgl-stb:3.3.6'
implementation 'org.lwjgl:lwjgl-glfw:3.3.6'
implementation 'org.lwjgl:lwjgl-opengl:3.3.6'
implementation 'org.lwjgl:lwjgl-jawt:3.3.5'
// Lwjgl natives
if (DefaultNativePlatform.currentOperatingSystem.isWindows()) {
runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-windows'
runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-windows'
runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-windows'
runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-windows'
} else if (DefaultNativePlatform.currentOperatingSystem.isLinux()) {
runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-linux'
runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-linux'
runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-linux'
runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-linux'
} else if (DefaultNativePlatform.currentOperatingSystem.isMacOsX()) {
runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-macos'
runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-macos'
runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-macos'
runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-macos'
}
// 其他图形库
implementation 'com.badlogicgames.gdx:gdx:1.12.1'
implementation 'org.joml:joml:1.10.7'
implementation 'com.kitfox.svg:svg-salamander:1.0'
implementation 'net.sourceforge.plantuml:plantuml:8059'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'org.openjfx:javafx-controls:21'
implementation 'org.benf:cfr:0.152'
implementation 'com.github.javaparser:javaparser-core:3.25.1'
implementation 'com.1stleg:jnativehook:2.1.0'
implementation 'org.json:json:20230618'
implementation 'org.lwjgl:lwjgl:3.3.1'
implementation 'org.lwjgl:lwjgl-glfw:3.3.1'
implementation 'org.lwjgl:lwjgl-opengl:3.3.1'
implementation 'org.lwjgl:lwjgl:3.3.1:natives-windows'
implementation 'org.lwjgl:lwjgl-glfw:3.3.1:natives-windows'
implementation 'org.lwjgl:lwjgl-opengl:3.3.1:natives-windows'
implementation 'com.twelvemonkeys.imageio:imageio-psd:3.12.0'
// === 图像处理 ===
implementation 'com.madgag:animated-gif-lib:1.4'
implementation 'org.bytedeco:javacv-platform:1.5.7'
implementation 'org.bytedeco:javacpp-platform:1.5.7'
implementation 'com.madgag:animated-gif-lib:1.4'
implementation 'org.openjfx:javafx-web:17'
runtimeOnly 'com.mysql:mysql-connector-j'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'com.kitfox.svg:svg-salamander:1.0'
implementation 'com.vladsch.flexmark:flexmark:0.64.8'
// === 编程语言支持 ===
implementation 'org.python:jython-standalone:2.7.3'
implementation 'org.graalvm.python:python-embedding:24.2.1'
// === 系统交互 ===
implementation 'com.github.kwhat:jnativehook:2.2.2'
implementation 'com.dustinredmond.fxtrayicon:FXTrayIcon:4.0.1'
implementation 'org.openjfx:javafx-graphics:21'
implementation 'me.friwi:jcefmaven:122.1.10'
implementation 'com.alphacephei:vosk:0.3.45'
implementation 'net.java.dev.jna:jna:5.13.0'
implementation 'net.java.dev.jna:jna-platform:5.13.0'
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'com.google.guava:guava:31.1-jre'
implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.25.9'
implementation 'org.bitbucket.mstrobel:procyon-core:0.6.0'
implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.6.0'
implementation 'com.belerweb:pinyin4j:2.5.1'
implementation 'commons-io:commons-io:2.18.0'
implementation 'com.1stleg:jnativehook:2.1.0'
// === 数据库 ===
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'mysql:mysql-connector-java:8.0.33'
implementation 'com.h2database:h2:2.2.220'
implementation 'org.xerial:sqlite-jdbc:3.41.2.1'
implementation 'org.postgresql:postgresql:42.6.0'
// === 音频处理 ===
implementation 'jflac:jflac:1.3'
implementation 'com.github.axet:TarsosDSP:2.4'
implementation 'org.json:json:20231013'
implementation 'com.googlecode.soundlibs:mp3spi:1.9.5-1'
implementation 'com.googlecode.soundlibs:vorbisspi:1.0.3-2'
implementation 'com.googlecode.soundlibs:jorbis:0.0.17-2'
// === 语音识别 ===
implementation 'com.alphacephei:vosk:0.3.45'
// === 浏览器引擎 ===
implementation 'me.friwi:jcefmaven:122.1.10'
// === 中文处理 ===
implementation 'com.belerweb:pinyin4j:2.5.1'
// === 安全认证 ===
implementation 'cn.dev33:sa-token-spring-boot-starter:1.44.0'
implementation 'org.casbin:casdoor-java-sdk:1.37.0'
}
configurations.all {
configurations.configureEach {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
@@ -167,9 +247,9 @@ application {
mainClass = 'com.axis.innovators.box.Main'
}
task runClient(type: JavaExec) {
group = "application"
description = "运行 com.axis.innovators.box.Main"
tasks.register('runClient', JavaExec) {
group = "run-toolboxProgram"
description = "执行工具箱程序"
classpath = sourceSets.main.runtimeClasspath
mainClass = "com.axis.innovators.box.Main"
jvmArgs = [
@@ -177,3 +257,43 @@ task runClient(type: JavaExec) {
"-Djava.system.class.loader=com.axis.innovators.box.plugins.BoxClassLoader"
]
}
tasks.register('test2DModelLayerPanel', JavaExec) {
group = "test-model"
description = "运行 2D Model Layer Panel 测试"
classpath = sourceSets.main.runtimeClasspath
mainClass = "com.chuangzhou.vivid2D.test.ModelLayerPanelTest"
jvmArgs = [
"-Dfile.encoding=UTF-8"
]
}
tasks.register('testModelRenderLightingTest', JavaExec) {
group = "test-model"
description = "运行 2D Model 高亮灯光测试"
classpath = sourceSets.main.runtimeClasspath
mainClass = "com.chuangzhou.vivid2D.test.ModelRenderLightingTest"
jvmArgs = [
"-Dfile.encoding=UTF-8"
]
}
tasks.register('testModelTest', JavaExec) {
group = "test-model"
description = "运行 2D Model 保存和完整性测试"
classpath = sourceSets.main.runtimeClasspath
mainClass = "com.chuangzhou.vivid2D.test.ModelTest"
jvmArgs = [
"-Dfile.encoding=UTF-8"
]
}
tasks.register('testModelTest2', JavaExec) {
group = "test-model"
description = "运行 2D Model 物理基准测试"
classpath = sourceSets.main.runtimeClasspath
mainClass = "com.chuangzhou.vivid2D.test.ModelTest2"
jvmArgs = [
"-Dfile.encoding=UTF-8"
]
}

0
gradle.properties Normal file
View File

View File

@@ -1,6 +1,6 @@
#Tue Feb 04 17:20:23 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https://mirrors.aliyun.com/macports/distfiles/gradle/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -584,6 +584,59 @@
ignoreUnescapedHTML: true
});
// 监听Java字体加载事件
document.addEventListener('javaFontsLoaded', function(event) {
const fontInfo = event.detail;
console.log('接收到Java字体信息:', fontInfo);
// 应用Java字体到界面
applyJavaFonts(fontInfo);
});
// 应用Java字体的函数
function applyJavaFonts(fontInfo) {
const uiFonts = fontInfo.uiFonts || {};
const defaultFont = fontInfo.defaultFont || uiFonts['Label.font'] || {};
if (defaultFont && defaultFont.family) {
const fontFamily = defaultFont.family;
const fontSize = defaultFont.size || 14;
const fontWeight = defaultFont.bold ? 'bold' : 'normal';
const fontStyle = defaultFont.italic ? 'italic' : 'normal';
// 创建字体样式
const style = document.createElement('style');
style.textContent = `
body, html {
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
}
.message.user .bubble {
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
}
.message.ai .bubble {
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
font-size: ${fontSize}px !important;
}
input, button {
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
}
code, pre {
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', 'Courier New', monospace !important;
}
`;
// 添加到文档头
document.head.appendChild(style);
console.log('Java字体已应用到DeepSeek界面:', fontFamily, fontSize + 'px');
}
}
// 如果字体信息已经存在,立即应用
if (typeof window.javaFontInfo !== 'undefined') {
applyJavaFonts(window.javaFontInfo);
}
// CEF通信桥接
window.javaQuery = window.cefQuery ? (request, success, error) => {
window.cefQuery({

View File

@@ -624,6 +624,59 @@
);
}
// 监听Java字体加载事件
document.addEventListener('javaFontsLoaded', function(event) {
const fontInfo = event.detail;
console.log('接收到Java字体信息:', fontInfo);
// 应用Java字体到界面
applyJavaFonts(fontInfo);
});
// 应用Java字体的函数
function applyJavaFonts(fontInfo) {
const uiFonts = fontInfo.uiFonts || {};
const defaultFont = fontInfo.defaultFont || uiFonts['Label.font'] || {};
if (defaultFont && defaultFont.family) {
const fontFamily = defaultFont.family;
const fontSize = defaultFont.size || 14;
const fontWeight = defaultFont.bold ? 'bold' : 'normal';
const fontStyle = defaultFont.italic ? 'italic' : 'normal';
// 创建字体样式
const style = document.createElement('style');
style.textContent = `
body, html {
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
}
.message.user .bubble {
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
}
.message.ai .bubble {
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
font-size: ${fontSize}px !important;
}
input, button {
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
}
code, pre {
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', 'Courier New', monospace !important;
}
`;
// 添加到文档头
document.head.appendChild(style);
console.log('Java字体已应用到DeepSeek界面:', fontFamily, fontSize + 'px');
}
}
// 如果字体信息已经存在,立即应用
if (typeof window.javaFontInfo !== 'undefined') {
applyJavaFonts(window.javaFontInfo);
}
function showError(requestId, message) {
const stream = streams.get(requestId);
if (stream) {

View File

@@ -122,6 +122,61 @@
});
});
// 监听Java字体加载事件
document.addEventListener('javaFontsLoaded', function(event) {
const fontInfo = event.detail;
console.log('接收到Java字体信息:', fontInfo);
// 应用Java字体到编辑器
applyJavaFonts(fontInfo);
});
// 应用Java字体的函数
function applyJavaFonts(fontInfo) {
const uiFonts = fontInfo.uiFonts || {};
const defaultFont = fontInfo.defaultFont || uiFonts['Label.font'] || {};
if (defaultFont && defaultFont.family) {
const fontFamily = defaultFont.family;
const fontSize = defaultFont.size || 14;
const fontWeight = defaultFont.bold ? 'bold' : 'normal';
const fontStyle = defaultFont.italic ? 'italic' : 'normal';
// 创建字体样式
const style = document.createElement('style');
style.textContent = `
body, html {
font-family: '${fontFamily}', 'JetBrains Mono', 'Consolas', monospace !important;
}
#output {
font-family: '${fontFamily}', 'JetBrains Mono', monospace !important;
font-size: ${fontSize}px !important;
}
#executeBtn {
font-family: '${fontFamily}', 'JetBrains Mono', monospace !important;
}
`;
// 添加到文档头
document.head.appendChild(style);
// 更新Monaco编辑器字体
if (window.editor) {
editor.updateOptions({
fontFamily: fontFamily,
fontSize: fontSize
});
}
console.log('Java字体已应用到编辑器:', fontFamily, fontSize + 'px');
}
}
// 如果字体信息已经存在,立即应用
if (typeof window.javaFontInfo !== 'undefined') {
applyJavaFonts(window.javaFontInfo);
}
monaco.languages.register({ id: 'c' });
// C语言关键字配置

View File

@@ -129,7 +129,60 @@
editor.setValue(getDefaultCode(lang));
}
// 监听Java字体加载事件
document.addEventListener('javaFontsLoaded', function(event) {
const fontInfo = event.detail;
console.log('接收到Java字体信息:', fontInfo);
// 应用Java字体到编辑器
applyJavaFonts(fontInfo);
});
// 应用Java字体的函数
function applyJavaFonts(fontInfo) {
const uiFonts = fontInfo.uiFonts || {};
const defaultFont = fontInfo.defaultFont || uiFonts['Label.font'] || {};
if (defaultFont && defaultFont.family) {
const fontFamily = defaultFont.family;
const fontSize = defaultFont.size || 14;
const fontWeight = defaultFont.bold ? 'bold' : 'normal';
const fontStyle = defaultFont.italic ? 'italic' : 'normal';
// 创建字体样式
const style = document.createElement('style');
style.textContent = `
body, html {
font-family: '${fontFamily}', 'Microsoft YaHei', sans-serif !important;
}
#output {
font-family: '${fontFamily}', 'Microsoft YaHei', monospace !important;
font-size: ${fontSize}px !important;
}
.btn, #language-select, #status {
font-family: '${fontFamily}', 'Microsoft YaHei', sans-serif !important;
}
`;
// 添加到文档头
document.head.appendChild(style);
// 更新Monaco编辑器字体
if (window.editor) {
editor.updateOptions({
fontFamily: fontFamily,
fontSize: fontSize
});
}
console.log('Java字体已应用到编辑器:', fontFamily, fontSize + 'px');
}
}
// 如果字体信息已经存在,立即应用
if (typeof window.javaFontInfo !== 'undefined') {
applyJavaFonts(window.javaFontInfo);
}
function runCode() {
const code = editor.getValue();

2015
javascript/DatabaseTool.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -446,6 +446,62 @@
}
})();
// 监听Java字体加载事件
document.addEventListener('javaFontsLoaded', function(event) {
const fontInfo = event.detail;
console.log('接收到Java字体信息:', fontInfo);
// 应用Java字体到编辑器
applyJavaFonts(fontInfo);
});
// 应用Java字体的函数
function applyJavaFonts(fontInfo) {
const uiFonts = fontInfo.uiFonts || {};
const defaultFont = fontInfo.defaultFont || uiFonts['Label.font'] || {};
if (defaultFont && defaultFont.family) {
const fontFamily = defaultFont.family;
const fontSize = defaultFont.size || 14;
const fontWeight = defaultFont.bold ? 'bold' : 'normal';
const fontStyle = defaultFont.italic ? 'italic' : 'normal';
// 创建字体样式
const style = document.createElement('style');
style.textContent = `
body, html {
font-family: '${fontFamily}', 'Fira Code', 'JetBrains Mono', monospace !important;
}
.toolbar, button, select {
font-family: '${fontFamily}', 'Fira Code', 'JetBrains Mono', monospace !important;
}
.log-item {
font-family: '${fontFamily}', 'Fira Code', 'JetBrains Mono', monospace !important;
font-size: ${fontSize}px !important;
}
.CodeMirror {
font-family: '${fontFamily}', 'Fira Code', 'JetBrains Mono', monospace !important;
font-size: ${fontSize}px !important;
}
`;
// 添加到文档头
document.head.appendChild(style);
// 更新CodeMirror编辑器字体
if (window.editor) {
editor.refresh(); // 刷新编辑器以应用新字体
}
console.log('Java字体已应用到HTML编辑器:', fontFamily, fontSize + 'px');
}
}
// 如果字体信息已经存在,立即应用
if (typeof window.javaFontInfo !== 'undefined') {
applyJavaFonts(window.javaFontInfo);
}
window.javaMessageHandler = {
loadContent: (content, fileName) => {
editor.setValue(content);

View File

@@ -1,3 +1,3 @@
#Current Loaded Language
#Sat Aug 16 18:11:03 CST 2025
#Sun Oct 05 18:45:33 CST 2025
loadedLanguage=system\:zh_CN

View File

@@ -41,6 +41,9 @@ material_oceanic_theme.default.tip=\u57FA\u4E8E MaterialLookAndFeel \u7684\u6D45
flatLightLaf_theme.system.topicName=flatLightLaf\u98CE\u683C
flatLightLaf_theme.default.tip=flatLightLaf\u98CE\u683C
blur.system.topicName=\u6A21\u7CCA\u98CE\u683C
blur.default.tip=\u6A21\u7CCA\u4E3B\u9898\uFF0C\u9002\u7528\u4E8E\u9AD8\u5BF9\u6BD4\u5EA6\u7684\u4E3B\u9898
mainWindow.title=\u8F74\u521B\u5DE5\u5177\u7BB1 v1.0
mainWindow.title.2=\u8F74\u521B\u5DE5\u5177\u7BB1
mainWindow.settings.title=\u7CFB\u7EDF\u8BBE\u7F6E
@@ -91,7 +94,7 @@ fridaWindow.settings.font=\u5B57\u4F53
fridaWindow.settings.size=\u5927\u5C0F
fridaWindow.settings.theme=\u4E3B\u9898
fridaWindow.menu.settingsMenu.settingsItem.1=\u8BBE\u7F6E
fridaWindow.menu.help.about=\u5173\u4E8E
fridaWindow.menu.help.about=\u5173\u4E8E\u6211\u4EEC
localWindow.newBtn=\u65B0\u5BF9\u8BDD
localWindow.saveBtn=\u4FDD\u5B58\u8BB0\u5F55
@@ -165,10 +168,10 @@ fridaWindow.font.preview=\u793A\u4F8B\u6587\u672C\uFF1Aconsole.log("Hello Frida"
fridaWindow.about.title=\u5173\u4E8E
fridaWindow.about.content=\u57FA\u4E8EFrida\u7684\u73B0\u4EE3\u6CE8\u5165\u5DE5\u5177\n\n\u7248\u6743\u6240\u6709 \u00A9 {0}
settings.1.title=\u63D2\u4EF6
settings.1.title=\u63D2\u4EF6\u7BA1\u7406
settings.2.title=\u57FA\u7840\u8BBE\u7F6E
settings.3.title=\u5173\u4E8E
settings.4.title=\u4E3B\u9898
settings.3.title=\u5173\u4E8E\u6211\u4EEC
settings.4.title=\u4E3B\u9898\u8BBE\u7F6E
settings.1.tip=\u63D2\u4EF6\u7BA1\u7406
settings.2.tip=\u5916\u89C2\u8BBE\u7F6E
settings.3.tip=\u7248\u672C\u4FE1\u606F
@@ -183,6 +186,8 @@ settings.1.scrollPane=\u5DF2\u52A0\u8F7D\u63D2\u4EF6\u5217\u8868
settings.2.color=\u754C\u9762\u4E3B\u9898\u989C\u8272\uFF1A
settings.2.colorBtn=\u9009\u62E9\u989C\u8272
settings.2.colorBtn.color=\u9009\u62E9\u4E3B\u9898\u989C\u8272
settings.2.colorBtn.cancel=\u53D6\u6D88
settings.2.colorBtn.apply=\u5E94\u7528
settings.2.font=\u754C\u9762\u5B57\u4F53\uFF1A
settings.2.fontBtn=\u9009\u62E9\u5B57\u4F53
settings.2.showConfirmDialog=\u9009\u62E9\u5B57\u4F53
@@ -194,6 +199,17 @@ settings.2.language=\u754C\u9762\u8BED\u8A00\uFF1A
settings.2.language.error=\u672A\u77E5\u8BED\u8A00
settings.3.infoArea.1=\u8F6F\u4EF6\u7248\u672C:
settings.3.infoArea.2=\u5F00\u53D1\u4F5C\u8005:
settings.3.infoArea.description=\u8FD9\u662F\u4E00\u4E2A\u529F\u80FD\u5F3A\u5927\u7684\u521B\u65B0\u5DE5\u5177\u7BB1\uFF0C\u96C6\u6210\u4E86\u591A\u79CD\u5B9E\u7528\u5DE5\u5177\u3002
settings.3.infoArea.features=\u4E3B\u8981\u529F\u80FD
settings.3.infoArea.feature1=\u6570\u636E\u5206\u6790\u548C\u53EF\u89C6\u5316
settings.3.infoArea.feature2=\u81EA\u52A8\u5316\u5904\u7406
settings.3.infoArea.feature3=\u81EA\u5B9A\u4E49\u63D2\u4EF6\u652F\u6301
settings.3.infoArea.email=tzdwindows 7
settings.3.infoArea.website=https://axisInnovatorsBox.com
settings.3.infoArea.copyright=\u7248\u6743\u6240\u6709 \u00A9 2025 Axis Innovators. \u4FDD\u7559\u6240\u6709\u6743\u5229\u3002
settings.3.infoArea.requirement1=Windows 10 \u6216\u66F4\u9AD8\u7248\u672C
settings.3.infoArea.requirement2=\u81F3\u5C11 4GB \u5185\u5B58
settings.3.infoArea.support=\u5982\u6709\u95EE\u9898\u8BF7\u8054\u7CFB\u6280\u672F\u652F\u6301\u56E2\u961F
settings.4.no_theme=\u6CA1\u6709\u53EF\u7528\u7684\u4E3B\u9898
settings.4.search=\u641C\u7D22
settings.4.search_empty=\u8BF7\u8F93\u5165\u641C\u7D22\u5185\u5BB9\uFF01

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
library/chrome_elf.dll Normal file

Binary file not shown.

View File

@@ -7,12 +7,21 @@
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_axis_innovators_box_tools_RegisterTray
* Method: register
* Signature: (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lcom/axis/innovators/box/tools/RegisterTray/Event;)J
* Signature: (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lcom/axis/innovators/box/tools/RegisterTray/Event;)J
*/
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_register
(JNIEnv*, jclass, jstring, jobject, jstring, jobject);
/*
* Class: com_axis_innovators_box_tools_RegisterTray
* Method: registerEx
* Signature: (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lcom/axis/innovators/box/tools/RegisterTray/Event;)J
*/
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_registerEx
(JNIEnv*, jclass, jstring, jobject, jstring, jstring, jobject);
/*

View File

@@ -4,288 +4,790 @@
#include <jni.h>
#include <vector>
#include <string>
#include <mutex>
#include <atomic>
#include <algorithm>
#include <uxtheme.h>
#include <windowsx.h>
#pragma comment(lib, "uxtheme.lib")
#include "com_axis_innovators_box_tools_RegisterTray.h"
// 调试输出
#define DEBUG_LOG(msg) OutputDebugStringW(L"[Tray] " msg L"\n")
// 调试输出
#define DEBUG_LOG(msg) { OutputDebugStringW(L"[Tray] " msg L"\n"); }
// 全局 JVM 缓存(懒取)
static std::atomic<JavaVM*> gJvm{ nullptr };
struct MenuItemData {
jint menuId;
jobject eventObjGlobal; // global ref to the Event object for this item
jmethodID onClickMethod;
jobject eventObj;
int menuId;
std::wstring title;
};
struct TrayData {
HMENU hMenu = NULL;
std::vector<MenuItemData> menuItems;
jobject eventObj = NULL;
jmethodID onClickMethod = NULL;
HWND hwnd = NULL;
UINT trayId = 0;
HICON hIcon = NULL;
std::vector<MenuItemData> menuItems;
jobject eventObjGlobal = NULL; // global ref for the primary event callback
jmethodID onClickMethod = NULL;
std::wstring name;
std::wstring iconPath;
std::wstring description;
// 使用 Win32 线程句柄替代 std::thread
HANDLE threadHandle = NULL;
DWORD threadId = 0;
std::mutex mutex;
std::atomic<bool> running{ false };
};
std::vector<TrayData*> trayDataList;
static std::vector<TrayData*> gTrayList;
static std::mutex gTrayListMutex;
HICON LoadTrayIcon(const wchar_t* path) {
HICON hIcon = (HICON)LoadImageW(
NULL, path, IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON),
LR_LOADFROMFILE | LR_SHARED
);
return hIcon ? hIcon : LoadIconW(NULL, IDI_APPLICATION);
// ---------- 主题/字体 ----------
struct ThemeColors {
COLORREF bg;
COLORREF hover;
COLORREF text;
COLORREF border;
};
static bool IsLightTheme()
{
DWORD val = 1; // 默认为浅色
DWORD cb = sizeof(val);
LONG r = RegGetValueW(
HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
L"AppsUseLightTheme",
RRF_RT_REG_DWORD, nullptr, &val, &cb);
return (r != ERROR_SUCCESS) ? true : (val != 0);
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
TrayData* pData = (TrayData*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
static ThemeColors GetThemeColors()
{
if (IsLightTheme()) {
// 浅色主题
return ThemeColors{
RGB(245, 245, 245), // 背景
RGB(225, 225, 225), // 悬停
RGB(32, 32, 32), // 文字
RGB(210, 210, 210) // 边框
};
}
else {
// 深色主题
return ThemeColors{
RGB(30, 30, 30), // 背景
RGB(50, 50, 50), // 悬停
RGB(230, 230, 230), // 文字
RGB(60, 60, 60) // 边框
};
}
}
static HFONT CreatePopupUIFont()
{
LOGFONTW lf{};
// 使用系统图标标题字体作为基准
SystemParametersInfoW(SPI_GETICONTITLELOGFONT, sizeof(lf), &lf, 0);
// 替换为 Segoe UI更现代
wcscpy_s(lf.lfFaceName, L"Segoe UI");
lf.lfHeight = -12; // 约 9pt @96DPI
lf.lfWeight = FW_NORMAL;
lf.lfQuality = CLEARTYPE_NATURAL_QUALITY; // 开启更自然的 ClearType
return CreateFontIndirectW(&lf);
}
// ---------- 前向声明 ----------
LRESULT CALLBACK TrayWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK PopupWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
DWORD WINAPI TrayMessageThreadProc(LPVOID lpParam);
void TrayMessageThread(TrayData* td);
// 注册窗口类(线程安全)
void EnsureWindowClassesRegistered(HINSTANCE hInstance) {
static std::atomic_bool registered{ false };
bool expected = false;
if (!registered.compare_exchange_strong(expected, true)) return;
WNDCLASSEXW wc = { 0 };
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = TrayWndProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"ModernTray_TrayWindowClass";
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
WNDCLASSEXW popup = { 0 };
popup.cbSize = sizeof(popup);
popup.lpfnWndProc = PopupWndProc;
popup.hInstance = hInstance;
popup.lpszClassName = L"ModernTray_PopupWindowClass";
popup.hCursor = LoadCursor(NULL, IDC_ARROW);
popup.hbrBackground = NULL; // 自绘背景
RegisterClassExW(&wc);
RegisterClassExW(&popup);
}
// 加载图标(文件或默认)
HICON LoadTrayIconSafe(const std::wstring& path) {
if (!path.empty()) {
HICON h = (HICON)LoadImageW(NULL, path.c_str(), IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON),
LR_LOADFROMFILE | LR_SHARED);
if (h) return h;
}
return LoadIconW(NULL, IDI_APPLICATION);
}
// 查找 TrayData
TrayData* FindTrayById(UINT id) {
//std::lock_guard<std::mutex> lk(gTrayListMutex);
for (auto t : gTrayList) if (t->trayId == id) return t;
return nullptr;
}
// 计算窗口尺寸(基于菜单文本)
SIZE CalcPopupSize(HDC hdc, const std::vector<MenuItemData>& items, HFONT hFont, int paddingX = 14, int paddingY = 10, int itemHeight = 28) {
SIZE sz = { 0, 0 };
int maxw = 0;
HFONT old = (HFONT)SelectObject(hdc, hFont);
for (const auto& it : items) {
if (it.title.empty()) continue;
RECT rc = { 0,0,0,0 };
DrawTextW(hdc, it.title.c_str(), -1, &rc, DT_SINGLELINE | DT_LEFT | DT_CALCRECT);
int w = rc.right - rc.left;
if (w > maxw) maxw = w;
}
SelectObject(hdc, old);
sz.cx = maxw + paddingX * 2;
if (sz.cx < 140) sz.cx = 140; // 最小宽度
sz.cy = (int)items.size() * itemHeight + paddingY; // 顶部/底部各留 paddingY/2 的感觉
return sz;
}
// 绘制圆角背景与文本WM_PAINT 在 PopupWndProc 使用)
void PaintPopup(HWND hwnd, const std::vector<MenuItemData>& items, int hoverIndex) {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
RECT rc;
GetClientRect(hwnd, &rc);
ThemeColors c = GetThemeColors();
// 背景(圆角 + 边框)
HBRUSH bg = CreateSolidBrush(c.bg);
HBRUSH hover = CreateSolidBrush(c.hover);
HPEN border = CreatePen(PS_SOLID, 1, c.border);
HRGN rgn = CreateRoundRectRgn(rc.left, rc.top, rc.right, rc.bottom, 12, 12);
SelectClipRgn(hdc, rgn);
HGDIOBJ oldPen = SelectObject(hdc, border);
HGDIOBJ oldBrush = SelectObject(hdc, bg);
RoundRect(hdc, rc.left, rc.top, rc.right, rc.bottom, 12, 12);
// 文本 & 悬停绘制
SetBkMode(hdc, TRANSPARENT);
SetTextColor(hdc, c.text);
static HFONT sFont = nullptr;
if (!sFont) sFont = CreatePopupUIFont();
HFONT oldFont = (HFONT)SelectObject(hdc, sFont);
int y = 6;
const int itemH = 28;
for (size_t i = 0; i < items.size(); ++i) {
RECT itrc = { 8, y, rc.right - 8, y + itemH - 2 };
if ((int)i == hoverIndex) {
FillRect(hdc, &itrc, hover);
}
DrawTextW(hdc, items[i].title.c_str(), -1, &itrc, DT_VCENTER | DT_SINGLELINE | DT_LEFT | DT_NOPREFIX);
y += itemH;
}
SelectObject(hdc, oldFont);
SelectObject(hdc, oldBrush);
SelectObject(hdc, oldPen);
DeleteObject(bg);
DeleteObject(hover);
DeleteObject(border);
DeleteObject(rgn);
EndPaint(hwnd, &ps);
}
// ---------- Popup window helpers ----------
struct PopupContext {
TrayData* tray;
std::vector<MenuItemData> items;
int hoverIndex;
POINT origin;
};
// 存储 PopupContext 到窗口属性(使用 GWLP_USERDATA
static inline PopupContext* GetPopupContext(HWND hwnd) {
return (PopupContext*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
}
// PopupWndProc 实现:自绘菜单,鼠标移动、点击处理
LRESULT CALLBACK PopupWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
PopupContext* ctx = GetPopupContext(hwnd);
static const UINT kGuardTimerId = 1; // 初始防抖计时器
static const UINT kGuardMs = 120; // 防抖时长
switch (msg) {
case WM_USER + 1:
switch (lParam) {
case WM_RBUTTONUP:
if (pData && pData->hMenu) {
case WM_CREATE:
SetTimer(hwnd, kGuardTimerId, kGuardMs, nullptr);
return 0;
case WM_TIMER:
if (wParam == kGuardTimerId) {
KillTimer(hwnd, kGuardTimerId);
return 0;
}
break;
case WM_ACTIVATE:
if (LOWORD(wParam) == WA_INACTIVE) {
// 防抖期内忽略一次失焦
if (!KillTimer(hwnd, kGuardTimerId)) {
DestroyWindow(hwnd);
return 0;
}
else {
SetTimer(hwnd, kGuardTimerId, kGuardMs, nullptr);
}
}
return 0;
case WM_MOUSEMOVE: {
if (!ctx) break;
int my = GET_Y_LPARAM(lParam);
const int itemH = 28;
int count = (int)ctx->items.size();
if (count <= 0) break;
int idx = std::max(0, std::min(count - 1, (my - 6) / itemH));
if (idx != ctx->hoverIndex) {
ctx->hoverIndex = idx;
InvalidateRect(hwnd, NULL, TRUE);
}
return 0;
}
case WM_LBUTTONDOWN: {
if (!ctx) break;
int my = GET_Y_LPARAM(lParam);
const int itemH = 28;
int count = (int)ctx->items.size();
if (count > 0) {
int idx = std::max(0, std::min(count - 1, (my - 6) / itemH));
if (idx >= 0 && idx < count) {
MenuItemData sel = ctx->items[idx];
TrayData* td = ctx->tray;
JavaVM* jvm = gJvm.load();
if (!jvm) {
JNI_GetCreatedJavaVMs(&jvm, 1, NULL);
gJvm.store(jvm);
}
if (jvm && sel.eventObjGlobal && sel.onClickMethod) {
JNIEnv* env = nullptr;
if (jvm->AttachCurrentThread((void**)&env, NULL) == JNI_OK) {
env->CallVoidMethod(sel.eventObjGlobal, sel.onClickMethod, (jlong)td->trayId);
jvm->DetachCurrentThread();
}
}
}
}
DestroyWindow(hwnd);
return 0;
}
case WM_PAINT:
if (ctx) PaintPopup(hwnd, ctx->items, ctx->hoverIndex);
return 0;
case WM_NCDESTROY:
if (ctx) {
delete ctx;
SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
}
return DefWindowProcW(hwnd, msg, wParam, lParam);
default:
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
}
// 创建并显示自定义弹出菜单(在托盘窗口线程上下文调用)
void ShowCustomPopup(TrayData* td, int x, int y) {
if (!td) return;
HINSTANCE hInst = GetModuleHandleW(NULL);
HWND popup = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_TOPMOST,
L"ModernTray_PopupWindowClass", L"",
WS_POPUP,
x, y, 200, 200,
td->hwnd /* owner */, NULL, hInst, NULL
);
if (!popup) return;
PopupContext* ctx = new PopupContext();
ctx->tray = td;
ctx->items = td->menuItems; // 保证是完整列表
ctx->hoverIndex = -1;
SetWindowLongPtrW(popup, GWLP_USERDATA, (LONG_PTR)ctx);
// 计算尺寸并防止出屏
HDC hdc = GetDC(popup);
static HFONT sFont = nullptr;
if (!sFont) sFont = CreatePopupUIFont();
SIZE sz = CalcPopupSize(hdc, ctx->items, sFont);
ReleaseDC(popup, hdc);
RECT work{};
SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0);
LONG px = std::min(
std::max(static_cast<LONG>(x), work.left),
std::max(work.right - static_cast<LONG>(sz.cx), work.left)
);
LONG py = std::min(
std::max(static_cast<LONG>(y), work.top),
std::max(work.bottom - static_cast<LONG>(sz.cy), work.top)
);
SetWindowPos(popup, HWND_TOPMOST, px, py, sz.cx, sz.cy, SWP_SHOWWINDOW | SWP_NOACTIVATE);
// 圆角
HRGN r = CreateRoundRectRgn(0, 0, sz.cx + 1, sz.cy + 1, 12, 12);
SetWindowRgn(popup, r, TRUE);
// 抢前台以降低“瞬间失焦”概率
if (td && td->hwnd) SetForegroundWindow(td->hwnd);
ShowWindow(popup, SW_SHOWNORMAL);
SetForegroundWindow(popup);
SetFocus(popup);
// 仅用于悬停高亮(不再用离开即关闭)
TRACKMOUSEEVENT tme{ sizeof(TRACKMOUSEEVENT) };
tme.dwFlags = TME_LEAVE;
tme.hwndTrack = popup;
TrackMouseEvent(&tme);
UpdateWindow(popup);
}
// ---------- Tray 窗口处理函数 ----------
LRESULT CALLBACK TrayWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
TrayData* td = (TrayData*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
if (msg == (WM_USER + 1)) {
if (lParam == WM_RBUTTONUP) {
if (td && td->hwnd) {
SetForegroundWindow(td->hwnd);
}
else {
SetForegroundWindow(hwnd);
}
POINT pt;
GetCursorPos(&pt);
SetForegroundWindow(hwnd);
TrackPopupMenuEx(pData->hMenu,
TPM_RIGHTALIGN | TPM_BOTTOMALIGN,
pt.x, pt.y, hwnd, NULL);
PostMessageW(hwnd, WM_NULL, 0, 0);
}
if (td) ShowCustomPopup(td, pt.x, pt.y);
return 0;
case WM_LBUTTONDOWN:
if (pData && pData->onClickMethod) {
JavaVM* jvm;
JNIEnv* env;
if (JNI_GetCreatedJavaVMs(&jvm, 1, NULL) == JNI_OK &&
jvm->AttachCurrentThread((void**)&env, NULL) == JNI_OK) {
env->CallVoidMethod(pData->eventObj, pData->onClickMethod, (jlong)pData->trayId);
}
else if (lParam == WM_LBUTTONDOWN) {
if (td && td->eventObjGlobal && td->onClickMethod) {
JavaVM* jvm = gJvm.load();
if (!jvm) {
JNI_GetCreatedJavaVMs(&jvm, 1, NULL);
gJvm.store(jvm);
}
if (jvm) {
JNIEnv* env = nullptr;
if (jvm->AttachCurrentThread((void**)&env, NULL) == JNI_OK) {
env->CallVoidMethod(td->eventObjGlobal, td->onClickMethod, (jlong)td->trayId);
jvm->DetachCurrentThread();
}
}
return 0;
}
break;
case WM_COMMAND: {
int menuId = LOWORD(wParam);
if (pData) {
for (auto& item : pData->menuItems) {
if (item.menuId == menuId) {
JavaVM* jvm;
JNIEnv* env;
if (JNI_GetCreatedJavaVMs(&jvm, 1, NULL) == JNI_OK &&
jvm->AttachCurrentThread((void**)&env, NULL) == JNI_OK) {
env->CallVoidMethod(item.eventObj, item.onClickMethod, (jlong)pData->trayId);
jvm->DetachCurrentThread();
}
break;
}
}
}
return 0;
}
}
switch (msg) {
case WM_CREATE:
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
default:
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
}
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_register
(JNIEnv* env, jclass, jstring name, jobject menuItems, jstring icon, jstring, jobject event) {
// 注册窗口类
WNDCLASSEXW wc = { sizeof(WNDCLASSEXW) };
wc.lpfnWndProc = WndProc;
wc.hInstance = GetModuleHandleW(NULL);
wc.lpszClassName = L"TrayWindowClass";
if (!RegisterClassExW(&wc)) return -1;
// 线程入口Win32 线程入口)
DWORD WINAPI TrayMessageThreadProc(LPVOID lpParam) {
TrayData* td = (TrayData*)lpParam;
if (!td) return 0;
TrayMessageThread(td);
return 0;
}
// 创建消息窗口
HWND hwnd = CreateWindowExW(0, L"TrayWindowClass", L"", 0, 0, 0, 0, 0,
HWND_MESSAGE, NULL, NULL, NULL);
if (!hwnd) return -1;
// 线程主体:为单个托盘创建窗口、图标并运行消息循环
void TrayMessageThread(TrayData* td) {
if (!td) return;
td->running.store(true);
TrayData* pData = new TrayData();
pData->hwnd = hwnd;
pData->trayId = GetTickCount();
pData->hMenu = CreatePopupMenu();
HINSTANCE hInst = GetModuleHandleW(NULL);
EnsureWindowClassesRegistered(hInst);
// 解析菜单项
jclass listClass = env->GetObjectClass(menuItems);
jint size = env->CallIntMethod(menuItems, env->GetMethodID(listClass, "size", "()I"));
for (int i = 0; i < size; ++i) {
jobject item = env->CallObjectMethod(menuItems,
env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;"), i);
jstring name = (jstring)env->GetObjectField(item,
env->GetFieldID(env->GetObjectClass(item), "name", "Ljava/lang/String;"));
jobject eventObj = env->GetObjectField(item,
env->GetFieldID(env->GetObjectClass(item), "event",
"Lcom/axis/innovators/box/tools/RegisterTray$Event;"));
const jchar* nameChars = env->GetStringChars(name, NULL);
std::wstring menuName(nameChars, nameChars + env->GetStringLength(name));
env->ReleaseStringChars(name, nameChars);
MenuItemData menuItem;
menuItem.menuId = 1000 + i;
menuItem.eventObj = env->NewGlobalRef(eventObj);
jclass eventClass = env->GetObjectClass(eventObj);
menuItem.onClickMethod = env->GetMethodID(eventClass, "onClick", "(J)V");
AppendMenuW(pData->hMenu, MF_STRING, menuItem.menuId, menuName.c_str());
pData->menuItems.push_back(menuItem);
// 创建消息窗口(消息窗口必须在本线程创建)
HWND hwnd = CreateWindowExW(0, L"ModernTray_TrayWindowClass", L"", 0,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
HWND_MESSAGE, NULL, hInst, NULL);
if (!hwnd) {
//DEBUG_LOG(L"CreateWindowExW 失败");
td->running.store(false);
return;
}
// 事件回调
jclass eventClass = env->GetObjectClass(event);
pData->onClickMethod = env->GetMethodID(eventClass, "onClick", "(J)V");
pData->eventObj = env->NewGlobalRef(event);
// 配置托盘
NOTIFYICONDATAW nid = { sizeof(NOTIFYICONDATAW) };
nid.hWnd = hwnd;
nid.uID = pData->trayId;
nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
nid.uCallbackMessage = WM_USER + 1;
td->hwnd = hwnd;
SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)td);
// 加载图标
const wchar_t* iconPath = (const wchar_t*)env->GetStringChars(icon, NULL);
pData->hIcon = LoadTrayIcon(iconPath);
nid.hIcon = pData->hIcon;
env->ReleaseStringChars(icon, (const jchar*)iconPath);
td->hIcon = LoadTrayIconSafe(td->iconPath);
// 设置提示
const wchar_t* tip = (const wchar_t*)env->GetStringChars(name, NULL);
wcsncpy_s(nid.szTip, _countof(nid.szTip), tip, _TRUNCATE);
env->ReleaseStringChars(name, (const jchar*)tip);
if (!Shell_NotifyIconW(NIM_ADD, &nid)) {
delete pData;
return -1;
// 添加到系统托盘
NOTIFYICONDATAW nid = { 0 };
nid.cbSize = sizeof(nid);
nid.hWnd = hwnd;
nid.uID = td->trayId;
nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
nid.uCallbackMessage = WM_USER + 1;
nid.hIcon = td->hIcon;
// szTip
{
std::wstring tip = td->name.empty() ? L"Tray" : td->name;
wcsncpy_s(nid.szTip, tip.c_str(), _TRUNCATE);
}
SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)pData);
trayDataList.push_back(pData);
if (!Shell_NotifyIconW(NIM_ADD, &nid)) {
//DEBUG_LOG(L"Shell_NotifyIconW(NIM_ADD) 失败");
}
// 启动消息循环
// 消息循环
MSG msg;
while (GetMessageW(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
return (jlong)pData->trayId;
}
// 在退出前删除托盘图标(再一次保险)
Shell_NotifyIconW(NIM_DELETE, &nid);
TrayData* FindTrayData(UINT trayId) {
for (auto data : trayDataList) {
if (data->trayId == trayId) return data;
// 清理窗口句柄(如果尚未)
if (td->hwnd) {
DestroyWindow(td->hwnd);
td->hwnd = NULL;
}
return nullptr;
td->running.store(false);
}
// ---------- JNI 接口实现 ----------
// 辅助:将 jstring 转 wchar_t string
static std::wstring JStringToWString(JNIEnv* env, jstring js) {
if (!js) return L"";
const jchar* chars = env->GetStringChars(js, NULL);
jsize len = env->GetStringLength(js);
std::wstring ws(chars, chars + len);
env->ReleaseStringChars(js, chars);
return ws;
}
/*
* public static native long register(String name, List<Item> value, String icon, Event event);
*/
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_register
(JNIEnv* env, jclass, jstring jname, jobject jlist, jstring jicon, jobject jevent) {
// 转换输入
std::wstring name = JStringToWString(env, jname);
std::wstring iconPath = JStringToWString(env, jicon);
// 创建 TrayData
TrayData* td = new TrayData();
td->trayId = (UINT)GetTickCount();
td->name = name;
td->iconPath = iconPath;
// 缓存 JVM
JavaVM* jvm = nullptr;
if (JNI_GetCreatedJavaVMs(&jvm, 1, NULL) == JNI_OK && jvm) gJvm.store(jvm);
// event 全局引用与方法
if (jevent) {
td->eventObjGlobal = env->NewGlobalRef(jevent);
jclass evc = env->GetObjectClass(jevent);
td->onClickMethod = env->GetMethodID(evc, "onClick", "(J)V");
}
// 解析列表 items更鲁棒的遍历
if (jlist) {
jclass listClass = env->GetObjectClass(jlist);
jmethodID sizeMid = env->GetMethodID(listClass, "size", "()I");
jmethodID getMid = env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;");
jint size = env->CallIntMethod(jlist, sizeMid);
jint parsed = 0;
for (jint i = 0; i < size; ++i) {
jobject item = env->CallObjectMethod(jlist, getMid, i);
if (!item) continue;
jclass itemClass = env->GetObjectClass(item);
jfieldID nameF = env->GetFieldID(itemClass, "name", "Ljava/lang/String;");
jstring jtitle = (jstring)env->GetObjectField(item, nameF);
std::wstring title = JStringToWString(env, jtitle);
jfieldID eventF = env->GetFieldID(itemClass, "event", "Lcom/axis/innovators/box/tools/RegisterTray$Event;");
jobject ievent = env->GetObjectField(item, eventF);
jobject globalEvent = nullptr;
jmethodID onClickMid = nullptr;
if (ievent) {
globalEvent = env->NewGlobalRef(ievent);
jclass evc = env->GetObjectClass(ievent);
onClickMid = env->GetMethodID(evc, "onClick", "(J)V");
}
MenuItemData mid;
mid.menuId = 1000 + i;
mid.eventObjGlobal = globalEvent;
mid.onClickMethod = onClickMid;
mid.title = title;
td->menuItems.push_back(mid);
++parsed;
}
// 打印数量,便于确认 Java 侧是否传入了多项
wchar_t buf[128];
swprintf_s(buf, L"[register] parsed menu items = %d", (int)parsed);
//DEBUG_LOG(buf);
}
// 放入全局列表
{
//std::lock_guard<std::mutex> lk(gTrayListMutex);
gTrayList.push_back(td);
}
// 启动窗口线程
td->threadHandle = CreateThread(NULL, 0, TrayMessageThreadProc, td, 0, &td->threadId);
if (!td->threadHandle) {
DEBUG_LOG(L"CreateThread failed");
// 清理
if (td->eventObjGlobal) {
env->DeleteGlobalRef(td->eventObjGlobal);
td->eventObjGlobal = NULL;
}
for (auto& mi : td->menuItems) {
if (mi.eventObjGlobal) {
env->DeleteGlobalRef(mi.eventObjGlobal);
mi.eventObjGlobal = NULL;
}
}
td->menuItems.clear();
delete td;
return (jlong)0;
}
return (jlong)td->trayId;
}
/*
* public static native long registerEx(String name, List<Item> value, String icon, String description, Event event);
*/
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_registerEx
(JNIEnv* env, jclass, jstring jname, jobject jlist, jstring jicon, jstring jdesc, jobject jevent) {
// 功能与 register 相同,只是多了 description
std::wstring name = JStringToWString(env, jname);
std::wstring iconPath = JStringToWString(env, jicon);
std::wstring desc = JStringToWString(env, jdesc);
TrayData* td = new TrayData();
td->trayId = (UINT)GetTickCount();
td->name = name;
td->iconPath = iconPath;
td->description = desc;
// 缓存 JVM
JavaVM* jvm = nullptr;
if (JNI_GetCreatedJavaVMs(&jvm, 1, NULL) == JNI_OK && jvm) gJvm.store(jvm);
// event
if (jevent) {
td->eventObjGlobal = env->NewGlobalRef(jevent);
jclass evc = env->GetObjectClass(jevent);
td->onClickMethod = env->GetMethodID(evc, "onClick", "(J)V");
}
// 解析 list
if (jlist) {
jclass listClass = env->GetObjectClass(jlist);
jmethodID sizeMid = env->GetMethodID(listClass, "size", "()I");
jmethodID getMid = env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;");
jint size = env->CallIntMethod(jlist, sizeMid);
jint parsed = 0;
for (jint i = 0; i < size; ++i) {
jobject item = env->CallObjectMethod(jlist, getMid, i);
if (!item) continue;
jclass itemClass = env->GetObjectClass(item);
jfieldID nameF = env->GetFieldID(itemClass, "name", "Ljava/lang/String;");
jstring jtitle = (jstring)env->GetObjectField(item, nameF);
std::wstring title = JStringToWString(env, jtitle);
jfieldID eventF = env->GetFieldID(itemClass, "event", "Lcom/axis/innovators/box/tools/RegisterTray$Event;");
jobject ievent = env->GetObjectField(item, eventF);
jobject globalEvent = nullptr;
jmethodID onClickMid = nullptr;
if (ievent) {
globalEvent = env->NewGlobalRef(ievent);
jclass evc = env->GetObjectClass(ievent);
onClickMid = env->GetMethodID(evc, "onClick", "(J)V");
}
MenuItemData mid;
mid.menuId = 2000 + i;
mid.eventObjGlobal = globalEvent;
mid.onClickMethod = onClickMid;
mid.title = title;
td->menuItems.push_back(mid);
++parsed;
}
wchar_t buf[128];
swprintf_s(buf, L"[registerEx] parsed menu items = %d", (int)parsed);
//DEBUG_LOG(buf);
}
{
//std::lock_guard<std::mutex> lk(gTrayListMutex);
gTrayList.push_back(td);
}
td->threadHandle = CreateThread(NULL, 0, TrayMessageThreadProc, td, 0, &td->threadId);
if (!td->threadHandle) {
//DEBUG_LOG(L"CreateThread failed (registerEx)");
// 清理
if (td->eventObjGlobal) {
env->DeleteGlobalRef(td->eventObjGlobal);
td->eventObjGlobal = NULL;
}
for (auto& mi : td->menuItems) {
if (mi.eventObjGlobal) {
env->DeleteGlobalRef(mi.eventObjGlobal);
mi.eventObjGlobal = NULL;
}
}
td->menuItems.clear();
delete td;
return (jlong)0;
}
return (jlong)td->trayId;
}
/*
* public static native void unregister(long id);
*/
JNIEXPORT void JNICALL Java_com_axis_innovators_box_tools_RegisterTray_unregister
(JNIEnv* env, jclass clazz, jlong id) {
const UINT trayId = static_cast<UINT>(id);
TrayData* pData = FindTrayData(trayId);
if (!pData) {
OutputDebugStringW(L"[unregister] 找不到对应的托盘数据");
(JNIEnv* env, jclass, jlong id) {
UINT trayId = (UINT)id;
TrayData* td = nullptr;
{
//std::lock_guard<std::mutex> lk(gTrayListMutex);
auto it = std::find_if(gTrayList.begin(), gTrayList.end(), [trayId](TrayData* t) { return t->trayId == trayId; });
if (it != gTrayList.end()) {
td = *it;
gTrayList.erase(it);
}
}
if (!td) {
DEBUG_LOG(L"[unregister] 找不到对应的托盘数据");
return;
}
// 1. 删除系统托盘图标
NOTIFYICONDATAW nid = { sizeof(NOTIFYICONDATAW) };
nid.hWnd = pData->hwnd;
nid.uID = trayId;
if (!Shell_NotifyIconW(NIM_DELETE, &nid)) {
DWORD err = GetLastError();
wchar_t errMsg[256];
swprintf_s(errMsg, _countof(errMsg), L"[unregister] 销毁窗口失败 (错误码: 0x%08X)", err);
OutputDebugStringW(errMsg);
// 发布消息让线程结束(销毁窗口会退出消息循环)
if (td->hwnd) {
PostMessageW(td->hwnd, WM_CLOSE, 0, 0);
}
// 2. 释放图标资源
if (pData->hIcon) {
if (!DestroyIcon(pData->hIcon)) {
OutputDebugStringW(L"[unregister] 销毁图标失败");
// 等待线程退出
if (td->threadHandle) {
if (GetCurrentThreadId() != td->threadId) {
DWORD waitRes = WaitForSingleObject(td->threadHandle, 5000);
if (waitRes == WAIT_TIMEOUT) {
DEBUG_LOG(L"[unregister] WaitForSingleObject 超时");
}
}
else {
OutputDebugStringW(L"[unregister] 图标资源已释放");
for (int i = 0; i < 100 && td->running.load(); ++i) Sleep(10);
}
pData->hIcon = NULL;
}
// 3. 销毁菜单
if (pData->hMenu) {
if (!DestroyMenu(pData->hMenu)) {
OutputDebugStringW(L"[unregister] 销毁菜单失败");
CloseHandle(td->threadHandle);
td->threadHandle = NULL;
td->threadId = 0;
}
else {
OutputDebugStringW(L"[unregister] 菜单已销毁");
}
pData->hMenu = NULL;
for (int i = 0; i < 50 && td->running.load(); ++i) Sleep(20);
}
// 4. 销毁窗口
if (pData->hwnd) {
if (!DestroyWindow(pData->hwnd)) {
DWORD err = GetLastError();
wchar_t errMsg[256];
swprintf_s(errMsg, _countof(errMsg), // 修改点
L"[unregister] 销毁窗口失败 (错误码: 0x%08X)",
err);
OutputDebugStringW(errMsg);
}
else {
OutputDebugStringW(L"[unregister] 窗口已销毁");
}
pData->hwnd = NULL;
// 删除通知区图标(保险)
NOTIFYICONDATAW nid = { 0 };
nid.cbSize = sizeof(nid);
nid.hWnd = td->hwnd;
nid.uID = td->trayId;
Shell_NotifyIconW(NIM_DELETE, &nid);
// 释放 icon
if (td->hIcon) {
DestroyIcon(td->hIcon);
td->hIcon = NULL;
}
// 5. 释放JNI全局引用
if (pData->eventObj) {
env->DeleteGlobalRef(pData->eventObj);
pData->eventObj = NULL;
OutputDebugStringW(L"[unregister] 事件全局引用已释放");
// 删除全局引用
if (td->eventObjGlobal) {
env->DeleteGlobalRef(td->eventObjGlobal);
td->eventObjGlobal = NULL;
}
// 6. 释放菜单项全局引用
for (auto& item : pData->menuItems) {
if (item.eventObj) {
env->DeleteGlobalRef(item.eventObj);
item.eventObj = NULL;
for (auto& mi : td->menuItems) {
if (mi.eventObjGlobal) {
env->DeleteGlobalRef(mi.eventObjGlobal);
mi.eventObjGlobal = NULL;
}
}
pData->menuItems.clear();
OutputDebugStringW(L"[unregister] 菜单项资源已清理");
td->menuItems.clear();
// 7. 从全局列表移除
auto it = std::remove_if(trayDataList.begin(), trayDataList.end(),
[pData](TrayData* data) { return data == pData; });
if (it != trayDataList.end()) {
trayDataList.erase(it, trayDataList.end());
OutputDebugStringW(L"[unregister] 已从全局列表移除");
if (td->hwnd) {
DestroyWindow(td->hwnd);
td->hwnd = NULL;
}
// 8. 释放内存
delete pData;
OutputDebugStringW(L"[unregister] 内存已释放");
// 9. 强制重绘任务栏
HWND taskbar = FindWindowW(L"Shell_TrayWnd", NULL);
if (taskbar) {
RedrawWindow(taskbar, NULL, NULL,
RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN);
}
delete td;
DEBUG_LOG(L"[unregister] 已完成清理");
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
// DllMain
BOOL APIENTRY DllMain(HMODULE, DWORD, LPVOID) {
return TRUE;
}

View File

@@ -1,8 +1,11 @@
package com.axis.innovators.box;
import com.axis.innovators.box.browser.WindowRegistry;
import com.axis.innovators.box.events.GlobalEventBus;
import com.axis.innovators.box.events.OpenFileEvents;
import com.axis.innovators.box.events.StartupEvent;
import com.axis.innovators.box.events.TopicsUpdateEvents;
import com.axis.innovators.box.util.WindowsTheme;
import com.axis.innovators.box.window.*;
import com.axis.innovators.box.plugins.PluginDescriptor;
import com.axis.innovators.box.plugins.PluginLoader;
@@ -12,12 +15,17 @@ import com.axis.innovators.box.register.RegistrationSettingsItem;
import com.axis.innovators.box.register.RegistrationTool;
import com.axis.innovators.box.register.RegistrationTopic;
import com.axis.innovators.box.tools.*;
import com.axis.innovators.box.tools.Crypto.AESCryptoUtil;
import com.axis.innovators.box.tools.Crypto.Base64CryptoUtil;
import com.axis.innovators.box.util.PythonResult;
import com.axis.innovators.box.util.Tray;
import com.axis.innovators.box.util.UserLocalInformation;
import com.axis.innovators.box.verification.UserTags;
import com.axis.innovators.box.verification.LoginResult;
import com.axis.innovators.box.verification.CasdoorServer;
import com.axis.innovators.box.verification.LoginData;
import com.formdev.flatlaf.themes.FlatMacDarkLaf;
import com.formdev.flatlaf.themes.FlatMacLightLaf;
import com.jogamp.nativewindow.swt.SWTAccessor;
import com.sun.javafx.stage.WindowHelper;
import com.sun.management.HotSpotDiagnosticMXBean;
import mdlaf.MaterialLookAndFeel;
import mdlaf.themes.JMarsDarkTheme;
@@ -31,6 +39,7 @@ import org.apache.logging.log4j.core.appender.FileAppender;
import org.apache.logging.log4j.core.appender.RollingFileAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.api.dog.agent.VirtualMachine;
import org.casbin.casdoor.entity.User;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
@@ -39,9 +48,12 @@ import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.*;
import java.lang.instrument.Instrumentation;
import java.lang.management.*;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.List;
@@ -56,9 +68,12 @@ import java.util.zip.ZipOutputStream;
*/
public class AxisInnovatorsBox {
private static final Logger logger = LogManager.getLogger(AxisInnovatorsBox.class);
private static final String VERSIONS = "0.1.2";
private static final String VERSIONS = "0.2.2";
private static final String[] AUTHOR = new String[]{
"tzdwindows 7"
"tzdwindows 7",
"lyxyz5223",
"\uD83D\uDC3EMr. Liu\uD83D\uDC3E",
"泽钰"
};
/** 我是总任务数 **/
@@ -69,6 +84,7 @@ public class AxisInnovatorsBox {
LanguageManager.getLoadedLanguages().getText("progressBarManager.title"),
totalTasks);
private static AxisInnovatorsBox main;
private final boolean quickStart;
private MainWindow ex;
private Thread thread;
private final String[] args;
@@ -77,17 +93,90 @@ public class AxisInnovatorsBox {
private final RegistrationTopic registrationTopic = new RegistrationTopic(this);
private final List<WindowsJDialog> windowsJDialogList = new ArrayList<>();
private final StateManager stateManager = new StateManager();
private UserTags userTags;
private final boolean isDebug;
private static DebugWindow debugWindow;
public AxisInnovatorsBox(String[] args, boolean isDebug) {
private static LoginData loginData;
public AxisInnovatorsBox(String[] args, boolean isDebug, boolean quickStart) {
this.args = args;
this.isDebug = isDebug;
this.quickStart = quickStart;
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
organizingCrashReports(throwable instanceof Exception ?
(Exception) throwable : new Exception(throwable));
});
if (quickStart){
initLog4j2();
setTopic();
main = this;
}
}
/**
* 判断当前环境是否为快速启动环境
*/
public boolean getQuickStart() {
return quickStart;
}
/**
* 弹出登录窗口
*/
private void popupLogin() {
// 加载登录信息,如果没有,弹出登录弹窗,后续可以删掉默认弹出
// TODO: login window should not be show when AxisInnovatorsBox initialize,
// it should be show when user click login button.
try {
String excryptedKey = "loginToken";
try {
excryptedKey = Base64CryptoUtil.base64Encode(excryptedKey);
} catch (Exception e) {
logger.error("Failed to encrypt key", e);
}
String encryptedToken = stateManager.getState(excryptedKey);
String token = null;
if (encryptedToken != null && !encryptedToken.isEmpty()) {
try {
token = AESCryptoUtil.decrypt(encryptedToken);
} catch (Exception ex) {
logger.error("Token 解密失败", ex);
token = null;
}
}
if (token == null || token.isEmpty()) {
LoginResult loginResult = CasdoorLoginWindow.showLoginDialogAndGetLoginResult();
if (loginResult == null) {
// 用户取消登录
JOptionPane.showMessageDialog(null, "取消登录", "登录",
JOptionPane.INFORMATION_MESSAGE);
} else if (loginResult.success()) {
loginData = loginResult.loginData();
String encrypted = AESCryptoUtil.encrypt(loginResult.token());
stateManager.saveState(excryptedKey, encrypted);
logger.info(
"Login result: token: " + loginResult.token() + ", user: " + loginResult.user());
JOptionPane.showMessageDialog(null, "登录成功", "登录",
JOptionPane.INFORMATION_MESSAGE);
} else {
// 登录失败,弹出错误提醒,这里只是输出登录错误信息
logger.error("Login error: " + loginResult.message());
JOptionPane.showMessageDialog(null, "登录失败: \n" + loginResult.message(), "登录失败",
JOptionPane.ERROR_MESSAGE);
}
} else {
CasdoorServer casdoorServer = new CasdoorServer();
User user = casdoorServer.parseJwtToken(token);
loginData = new LoginData(token, user);
}
} catch (InterruptedException e) {
logger.error("InterruptedException: Failed to load login information", e);
} catch (InvocationTargetException e) {
logger.error("InvocationTargetException: Failed to load login information", e);
} catch (Exception e) {
logger.error("Exception: Failed to load login information", e);
} // 添加了更详细的异常处理
}
/**
@@ -104,8 +193,9 @@ public class AxisInnovatorsBox {
LibraryLoad.loadLibrary("FridaNative");
LibraryLoad.loadLibrary("ThrowSafely");
LibraryLoad.loadLibrary("DogAgent");
LibraryLoad.loadLibrary("RegisterTray");
} catch (Exception e) {
logger.error("Failed to load the 'FridaNative' library", e);
logger.error("Failed to load library", e);
}
}
@@ -138,6 +228,7 @@ public class AxisInnovatorsBox {
* 组织崩溃报告
*/
public void organizingCrashReports(Exception e) {
e.printStackTrace();
SwingUtilities.invokeLater(() -> {
String systemOut = Log4j2OutputStream.systemOutContent.toString();
String systemErr = Log4j2OutputStream.systemErrContent.toString();
@@ -629,6 +720,28 @@ public class AxisInnovatorsBox {
return tempFile;
}
/**
* 运行监控主题
*/
public void runMonitorTopics(){
WindowsTheme.setThemeChangeListener((themeName, darkTheme) -> {
logger.info("主题变更: {}, 暗主题: {}", themeName, darkTheme);
if (registrationTopic.isDarkMode() == darkTheme){
return;
}
try {
updateTheme(themeName,darkTheme);
} catch (UnsupportedLookAndFeelException e) {
throw new RuntimeException(e);
}
if (SwingUtilities.isEventDispatchThread()) {
reloadAllWindow();
} else {
SwingUtilities.invokeLater(this::reloadAllWindow);
}
});
}
private void addFileToZip(ZipOutputStream zos, File file, String entryPath) throws IOException {
if (!file.exists()) {
return;
@@ -720,7 +833,7 @@ public class AxisInnovatorsBox {
private void setTopic() {
try {
boolean isDarkMode = SystemInfoUtil.isSystemDarkMode();
boolean isDarkMode = WindowsTheme.isDarkTheme();
// 1. 默认系统主题
main.registrationTopic.addTopic(
@@ -840,14 +953,32 @@ public class AxisInnovatorsBox {
true
);
//main.registrationTopic.addTopic(
// new BlurTopic(),
// LanguageManager.getLoadedLanguages().getText("blur.system.topicName"),
// LanguageManager.getLoadedLanguages().getText("blur.default.tip"),
// LoadIcon.loadIcon(MainWindow.class, "logo.png", 64),
// "system:blur",true
//);
updateTheme("",isDarkMode);
} catch (Exception e) {
logger.warn("Failed to load the system facade class", e);
}
}
/**
* 更新主题
* @param themeName 主题名称
* @param isDarkMode 是否是暗主题
*/
public void updateTheme(String themeName,boolean isDarkMode) throws UnsupportedLookAndFeelException {
LookAndFeel defaultLaf = isDarkMode ? new FlatMacDarkLaf() : new FlatMacLightLaf();
UIManager.setLookAndFeel(defaultLaf);
main.registrationTopic.setLoading(
isDarkMode ? "system:flatMacDark_theme" : "system:flatMacLight_theme"
);
} catch (Exception e) {
logger.warn("Failed to load the system facade class", e);
}
GlobalEventBus.EVENT_BUS.post(new TopicsUpdateEvents(themeName,isDarkMode));
}
/**
@@ -893,6 +1024,7 @@ public class AxisInnovatorsBox {
windowsJDialog.revalidate();
windowsJDialog.repaint();
}
WindowRegistry.getInstance().update();
ex.initUI();
ex.updateTheme();
ex.revalidate();
@@ -926,25 +1058,48 @@ public class AxisInnovatorsBox {
debugWindow.setVisible(true);
}
public static void run(String[] args, boolean isDebug) {
public static void run(String[] args, boolean isDebug, boolean quickStart) {
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
if (main != null) {
main.organizingCrashReports(throwable instanceof Exception ?
(Exception) throwable : new Exception(throwable));
} else {
new AxisInnovatorsBox(args, isDebug)
new AxisInnovatorsBox(args, isDebug,true)
.organizingCrashReports(throwable instanceof Exception ?
(Exception) throwable : new Exception(throwable));
}
});
// 设置EDT(事件调度线程)的异常处理器
// Set the exception handler for EDT(event dispatcher thread)
System.setProperty("sun.awt.exception.handler", EDTCrashHandler.class.getName());
main = new AxisInnovatorsBox(args,isDebug);
// Check if AxisInnovatorsBox is started
// If it's started, and it's not a quick start, don't allow it
// because it's already started
// Stop loading if the current running context is the quickStart context
if (AxisInnovatorsBox.getMain() != null
&& !AxisInnovatorsBox.getMain().getQuickStart() || quickStart) {
// Manually created if it is a quickStart context and the AxisInnovatorsBox instance in the context is empty
if (AxisInnovatorsBox.getMain() == null && quickStart) {
new AxisInnovatorsBox(args,isDebug,true);
}
return;
}
main = new AxisInnovatorsBox(args,isDebug,false);
try {
main.initLog4j2();
main.setTopic();
main.runMonitorTopics();
//main.popupLogin();
main.thread = new Thread(() -> {
try {
// 主任务1加载插件
logger.info("Loaded plugins Started");
main.progressBarManager.updateMainProgress(++main.completedTasks);
PluginLoader.loadPlugins();
PluginPyLoader.loadAllPlugins();
logger.info("Loaded plugins End");
List<Map<String, String>> validFiles = ArgsParser.parseArgs(args);
for (Map<String, String> fileInfo : validFiles) {
@@ -957,23 +1112,6 @@ public class AxisInnovatorsBox {
}
}
main.thread = new Thread(() -> {
try {
UserLocalInformation userLocalInformation = new UserLocalInformation(main);
main.userTags = userLocalInformation.getUserTags();
if (main.userTags == null) {
// 登录窗口
main.userTags = LoginWindow.createAndShow();
userLocalInformation.setUserTags(main.userTags);
}
// 主任务1加载插件
logger.info("Loaded plugins Started");
main.progressBarManager.updateMainProgress(++main.completedTasks);
PluginLoader.loadPlugins();
PluginPyLoader.loadAllPlugins();
logger.info("Loaded plugins End");
main.progressBarManager.close();
SwingUtilities.invokeLater(() -> {
@@ -1052,9 +1190,16 @@ public class AxisInnovatorsBox {
}
ex.initUI();
RegistrationSettingsItem.applyAllSettings();
isWindow = true;
ex.setVisible(true);
Toolkit.getDefaultToolkit().addPropertyChangeListener("win.xpstyle.themeName",
evt -> {
logger.info("系统主题发生变化: {}", evt.getNewValue());
ex.updateTheme();
});
if (isDebug) {
SwingUtilities.invokeLater(this::createDebugWindow);
}
@@ -1092,13 +1237,6 @@ public class AxisInnovatorsBox {
return AUTHOR;
}
/**
* 获取用户标签
* @return 用户标签
*/
public UserTags getUserTags() {
return userTags;
}
/**
* 获取状态管理器

View File

@@ -6,33 +6,121 @@ import org.apache.logging.log4j.Logger;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
/**
* 将输出传递给 Log4j2 的日志记录器
* 将输出传递给 Log4j2 的日志记录器,同时保持控制台输出
* 修复问题控制台输出被Log4j2覆盖
* @author tzdwindows 7
*/
public class Log4j2OutputStream extends OutputStream {
private static final Logger logger = LogManager.getLogger();
// 恢复静态变量
public static final ByteArrayOutputStream systemOutContent = new ByteArrayOutputStream();
public static final ByteArrayOutputStream systemErrContent = new ByteArrayOutputStream();
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
private final boolean isErrorStream;
private final PrintStream originalStream; // 保存原始的控制台流
public Log4j2OutputStream(boolean isErrorStream, PrintStream originalStream) {
this.isErrorStream = isErrorStream;
this.originalStream = originalStream;
}
@Override
public void write(int b) {
// 写入原始控制台
originalStream.write(b);
buffer.write(b);
// 将内容同时写入对应的静态变量
if (isErrorStream) {
systemErrContent.write(b);
} else {
systemOutContent.write(b);
logger.info(String.valueOf((char) b));
}
// 遇到换行符时刷新缓冲区到日志
if (b == '\n') {
flush();
}
}
@Override
public void write(byte[] b, int off, int len) {
// 写入原始控制台
originalStream.write(b, off, len);
buffer.write(b, off, len);
// 将内容同时写入对应的静态变量
if (isErrorStream) {
systemErrContent.write(b, off, len);
} else {
systemOutContent.write(b, off, len);
String message = new String(b, off, len).trim();
}
// 检查是否包含换行符
for (int i = off; i < off + len; i++) {
if (b[i] == '\n') {
flush();
break;
}
}
}
@Override
public void flush() {
originalStream.flush();
String message = buffer.toString(StandardCharsets.UTF_8).trim();
if (!message.isEmpty()) {
if (isErrorStream) {
logger.error(message);
} else {
logger.info(message);
}
}
buffer.reset(); // 清空缓冲区
}
@Override
public void close() {
flush();
originalStream.close();
}
/**
* 重定向 System.out 和 System.err 到 Log4j2
* 重定向 System.out 和 System.err 到 Log4j2,同时保持控制台输出
*/
public static void redirectSystemStreams() {
System.setOut(new PrintStream(new Log4j2OutputStream(), true));
System.setErr(new PrintStream(new Log4j2OutputStream(), true));
// 保存原始流
PrintStream originalOut = System.out;
PrintStream originalErr = System.err;
// System.out 使用 INFO 级别,同时输出到原始控制台
System.setOut(new PrintStream(new Log4j2OutputStream(false, originalOut), true, StandardCharsets.UTF_8));
// System.err 使用 ERROR 级别,同时输出到原始控制台
System.setErr(new PrintStream(new Log4j2OutputStream(true, originalErr), true, StandardCharsets.UTF_8));
}
/**
* 清空静态缓冲区内容
*/
public static void clearBuffers() {
systemOutContent.reset();
systemErrContent.reset();
}
/**
* 获取输出内容
*/
public static String getSystemOutContent() {
return systemOutContent.toString(StandardCharsets.UTF_8);
}
public static String getSystemErrContent() {
return systemErrContent.toString(StandardCharsets.UTF_8);
}
}

View File

@@ -6,6 +6,7 @@ import com.axis.innovators.box.register.LanguageManager;
import com.axis.innovators.box.tools.ArgsParser;
import com.axis.innovators.box.tools.FolderCleaner;
import com.axis.innovators.box.tools.FolderCreator;
import org.QQdecryption.ui.DecryptionUI;
import javax.swing.*;
import java.io.File;
@@ -14,8 +15,7 @@ import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.util.List;
import java.util.Map;
import java.util.*;
/**
* @author tzdwindows 7
@@ -26,33 +26,49 @@ public class Main {
private static FileLock lock = null;
private static RandomAccessFile lockFile = null;
private static FileChannel lockChannel = null;
private final static boolean releaseEnvironments = false;
public static void main(String[] args) {
// 清理日志文件最大日志为10
FolderCleaner.cleanFolder(FolderCreator.getLogsFolder(), 10);
// 加载保存的语言
LanguageManager.loadSavedLanguage();
// 如果没有加载的语言,则加载默认的语言
if (LanguageManager.getLoadedLanguages() == null) {
LanguageManager.loadLanguage("system:zh_CN");
}
// 检查是否包含调试控制台参数
boolean debugWindowEnabled = false;
for (int i = 0; i < args.length; i++) {
if ("-debugControlWindow-on".equals(args[i])) {
String pluginsDirectory = null;
List<String> remainingArgs = new ArrayList<>();
for (String arg : args) {
if (!releaseEnvironments && "-debugControlWindow-on".equals(arg)) {
debugWindowEnabled = true;
// 移除此参数避免干扰后续处理
String[] newArgs = new String[args.length - 1];
System.arraycopy(args, 0, newArgs, 0, i);
System.arraycopy(args, i + 1, newArgs, i, args.length - i - 1);
args = newArgs;
break;
}
continue;
}
List<Map<String, String>> validFiles = ArgsParser.parseArgs(args);
if (arg.startsWith("pluginsDirectory=")) {
pluginsDirectory = arg.substring("pluginsDirectory=".length());
if (pluginsDirectory.startsWith("\"") && pluginsDirectory.endsWith("\"")) {
pluginsDirectory = pluginsDirectory.substring(1, pluginsDirectory.length() - 1);
}
continue;
}
remainingArgs.add(arg);
}
if (pluginsDirectory != null && !pluginsDirectory.isEmpty()) {
System.out.println("Setting up the plugin directory: " + pluginsDirectory);
FolderCreator.setPluginPath(pluginsDirectory);
}
boolean quickStart = false;
String[] processedArgs = remainingArgs.toArray(new String[0]);
final Set<String> mUSICEXTS = new HashSet<>(Arrays.asList(
".mflac", ".mgg", ".qmc0", ".qmc3", ".qmcflac", ".qmcogg",
".tkm", ".qmc2", ".bkcmp3", ".bkcflac", ".ogg"
));
List<Map<String, String>> validFiles = ArgsParser.parseArgs(processedArgs);
for (Map<String, String> fileInfo : validFiles) {
String extension = fileInfo.get("extension");
String path = fileInfo.get("path");
@@ -60,26 +76,35 @@ public class Main {
SwingUtilities.invokeLater(() -> {
try {
UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarculaLaf());
} catch (Exception ex) {
ex.printStackTrace();
}
} catch (Exception ignored) {}
ModernJarViewer viewer = new ModernJarViewer(null, path);
viewer.setVisible(true);
});
releaseLock(); // 释放锁(窗口模式)
return;
quickStart = true;
}
if (".html".equals(extension)) {
MainApplication.popupHTMLWindow(path);
releaseLock();
return;
quickStart = true;
}
if (extension != null && mUSICEXTS.contains(extension.toLowerCase(Locale.ROOT))) {
final String p = path;
SwingUtilities.invokeLater(() -> {
DecryptionUI ui = new DecryptionUI(p);
ui.setVisible(true);
});
releaseLock();
quickStart = true;
}
}
if (!acquireLock()) {
return;
}
AxisInnovatorsBox.run(args, debugWindowEnabled);
AxisInnovatorsBox.run(processedArgs, debugWindowEnabled,quickStart);
}
/**

View File

@@ -1,6 +1,9 @@
package com.axis.innovators.box.browser;
import com.axis.innovators.box.AxisInnovatorsBox;
import com.axis.innovators.box.events.BrowserCreationCallback;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.CefSettings;
@@ -8,9 +11,11 @@ import org.cef.browser.CefBrowser;
import org.cef.browser.CefFrame;
import org.cef.browser.CefMessageRouter;
import org.cef.callback.CefContextMenuParams;
import org.cef.callback.CefJSDialogCallback;
import org.cef.callback.CefMenuModel;
import org.cef.callback.CefQueryCallback;
import org.cef.handler.*;
import org.cef.misc.BoolRef;
import org.cef.network.CefRequest;
import javax.swing.*;
@@ -218,7 +223,6 @@ public class BrowserWindow extends JFrame {
}
}
private Component initializeCef(Builder builder) throws MalformedURLException {
if (!isInitialized) {
isInitialized = true;
@@ -289,6 +293,20 @@ public class BrowserWindow extends JFrame {
}
});
if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) {
client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
@Override
public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
// 检测 F12
if (event.windows_key_code == 123) {
browser.getDevTools().createImmediately();
return true;
}
return false;
}
});
}
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
@Override
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
@@ -408,6 +426,24 @@ public class BrowserWindow extends JFrame {
}
});
client.addJSDialogHandler(new CefJSDialogHandlerAdapter() {
@Override
public boolean onJSDialog(CefBrowser browser, String origin_url, CefJSDialogHandler.JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) {
if (dialog_type == CefJSDialogHandler.JSDialogType.JSDIALOGTYPE_ALERT) {
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(
BrowserWindow.this,
message_text,
"警告",
JOptionPane.INFORMATION_MESSAGE
);
});
callback.Continue(true, "");
return true;
}
return false;
}
});
// 3. 拦截所有新窗口(关键修复点!)
@@ -468,6 +504,7 @@ public class BrowserWindow extends JFrame {
config.jsQueryFunction = "javaQuery";// 定义方法
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
updateTheme();
// 6. 配置窗口布局(确保只添加一次)
SwingUtilities.invokeLater(() -> {
@@ -539,6 +576,153 @@ public class BrowserWindow extends JFrame {
return null;
}
/**
* 更新主题
*/
public void updateTheme() {
// 1. 获取Java字体信息
String fontInfo = getSystemFontsInfo();
boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
injectFontInfoToPage(browser, fontInfo, isDarkTheme);
// 2. 注入主题信息
//injectThemeInfoToPage(browser, isDarkTheme);
//// 3. 刷新浏览器
//SwingUtilities.invokeLater(() -> {
// browser.reload();
//});
}
/**
* 获取Java字体信息从UIManager获取
*/
private String getSystemFontsInfo() {
try {
Gson gson = new Gson();
JsonObject fontInfo = new JsonObject();
JsonObject uiFonts = new JsonObject();
String[] fontKeys = {
"Label.font", "Button.font", "ToggleButton.font", "RadioButton.font",
"CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font",
"TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font",
"FormattedTextField.font", "Table.font", "TableHeader.font", "List.font",
"Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font",
"PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font",
"Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont",
"OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font"
};
for (String key : fontKeys) {
Font font = UIManager.getFont(key);
if (font != null) {
JsonObject fontObj = new JsonObject();
fontObj.addProperty("name", font.getFontName());
fontObj.addProperty("family", font.getFamily());
fontObj.addProperty("size", font.getSize());
fontObj.addProperty("style", font.getStyle());
fontObj.addProperty("bold", font.isBold());
fontObj.addProperty("italic", font.isItalic());
fontObj.addProperty("plain", font.isPlain());
uiFonts.add(key, fontObj);
}
}
fontInfo.add("uiFonts", uiFonts);
fontInfo.addProperty("timestamp", System.currentTimeMillis());
fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName());
return gson.toJson(fontInfo);
} catch (Exception e) {
return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}";
}
}
/**
* 注入主题信息到页面
*/
private void injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) {
if (client == null) {
return;
}
String themeInfo = String.format(
"{\"isDarkTheme\": %s, \"timestamp\": %d}",
isDarkTheme,
System.currentTimeMillis()
);
// 最简单的脚本 - 直接设置和分发事件
String script = String.format(
"window.javaThemeInfo = %s;" +
"console.log('主题信息已设置:', window.javaThemeInfo);" +
"" +
"var event = new CustomEvent('javaThemeChanged', {" +
" detail: window.javaThemeInfo" +
"});" +
"document.dispatchEvent(event);" +
"console.log('javaThemeChanged事件已分发');",
themeInfo);
browser.executeJavaScript(script, browser.getURL(), 0);
}
/**
* 注入字体信息到页面并设置字体
*/
private void injectFontInfoToPage(CefBrowser browser, String fontInfo,boolean isDarkTheme) {
if (client == null) {
return;
}
client.addLoadHandler(new CefLoadHandlerAdapter() {
@Override
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
// 使用更简单的脚本来注入字体信息
String script =
"if (typeof window.javaFontInfo === 'undefined') {" +
" window.javaFontInfo = " + fontInfo + ";" +
" console.log('Java font information has been loaded:', window.javaFontInfo);" +
" " +
" var event = new CustomEvent('javaFontsLoaded', {" +
" detail: window.javaFontInfo" +
" });" +
" document.dispatchEvent(event);" +
" console.log('The javaFontsLoaded event is dispatched');" +
"}";
browser.executeJavaScript(script, browser.getURL(), 0);
// 添加调试信息
browser.executeJavaScript(
"console.log('Font information injection is completewindow.javaFontInfo:', typeof window.javaFontInfo);" +
"console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');",
browser.getURL(), 0
);
String themeInfo = String.format(
"{\"isDarkTheme\": %s, \"timestamp\": %d}",
isDarkTheme,
System.currentTimeMillis()
);
script = String.format(
"window.javaThemeInfo = %s;" +
"console.log('主题信息已设置:', window.javaThemeInfo);" +
"" +
"var event = new CustomEvent('javaThemeChanged', {" +
" detail: window.javaThemeInfo" +
"});" +
"document.dispatchEvent(event);" +
"console.log('javaThemeChanged事件已分发');",
themeInfo);
browser.executeJavaScript(script, browser.getURL(), 0);
}
});
}
public static void printStackTrace() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (int i = 2; i < stackTrace.length; i++) {
@@ -551,6 +735,11 @@ public class BrowserWindow extends JFrame {
@Override
public void setVisible(boolean b) {
if (b) {
if (browser != null) {
updateTheme();
}
}
super.setVisible(b);
}

View File

@@ -1,6 +1,9 @@
package com.axis.innovators.box.browser;
import com.axis.innovators.box.AxisInnovatorsBox;
import com.axis.innovators.box.events.BrowserCreationCallback;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.CefSettings;
@@ -8,10 +11,13 @@ import org.cef.browser.CefBrowser;
import org.cef.browser.CefFrame;
import org.cef.browser.CefMessageRouter;
import org.cef.callback.CefContextMenuParams;
import org.cef.callback.CefJSDialogCallback;
import org.cef.callback.CefMenuModel;
import org.cef.callback.CefQueryCallback;
import org.cef.handler.*;
import org.cef.misc.BoolRef;
import org.cef.network.CefRequest;
import org.json.JSONObject;
import javax.swing.*;
import java.awt.*;
@@ -85,6 +91,7 @@ public class BrowserWindowJDialog extends JDialog {
this.title = title;
return this;
}
/**
* 设置链接打开方式
*
@@ -294,6 +301,20 @@ public class BrowserWindowJDialog extends JDialog {
}
});
if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) {
client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
@Override
public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
// 检测 F12
if (event.windows_key_code == 123) {
browser.getDevTools().createImmediately();
return true;
}
return false;
}
});
}
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
@Override
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
@@ -413,6 +434,26 @@ public class BrowserWindowJDialog extends JDialog {
}
});
// 添加 alert 弹窗监控处理
client.addJSDialogHandler(new CefJSDialogHandlerAdapter() {
@Override
public boolean onJSDialog(CefBrowser browser, String origin_url, CefJSDialogHandler.JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) {
if (dialog_type == CefJSDialogHandler.JSDialogType.JSDIALOGTYPE_ALERT) {
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(
BrowserWindowJDialog.this,
message_text,
"警告",
JOptionPane.INFORMATION_MESSAGE
);
});
callback.Continue(true, "");
return true;
}
return false;
}
});
// 3. 拦截所有新窗口(关键修复点!)
@@ -427,6 +468,7 @@ public class BrowserWindowJDialog extends JDialog {
Thread.currentThread().setName("BrowserRenderThread");
// 4. 加载HTML
if (htmlUrl.isEmpty()) {
String fileUrl = new File(htmlPath).toURI().toURL().toString();
@@ -471,6 +513,8 @@ public class BrowserWindowJDialog extends JDialog {
}
}
updateTheme();
CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
config.jsQueryFunction = "javaQuery";// 定义方法
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
@@ -510,6 +554,8 @@ public class BrowserWindowJDialog extends JDialog {
}
});
dragPanel.add(titleBar, BorderLayout.NORTH);
getContentPane().add(dragPanel, BorderLayout.CENTER);
getContentPane().add(browserComponent, BorderLayout.CENTER);
@@ -546,6 +592,148 @@ public class BrowserWindowJDialog extends JDialog {
return null;
}
/**
* 更新主题
*/
public void updateTheme() {
// 1. 获取Java字体信息
String fontInfo = getSystemFontsInfo();
injectFontInfoToPage(browser, fontInfo);
// 2. 注入主题信息
boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
injectThemeInfoToPage(browser, isDarkTheme);
//// 3. 刷新浏览器
//SwingUtilities.invokeLater(() -> {
// browser.reload();
//});
}
private void injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) {
if (client == null) {
return;
}
client.addLoadHandler(new CefLoadHandlerAdapter() {
@Override
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
String themeInfo = String.format(
"{\"isDarkTheme\": %s, \"timestamp\": %d}",
isDarkTheme,
System.currentTimeMillis()
);
String script =
"window.javaThemeInfo = " + themeInfo + ";\n" +
"console.log('Java theme information has been loaded:', window.javaThemeInfo);\n" +
"\n" +
"if (typeof applyJavaTheme === 'function') {\n" +
" applyJavaTheme(window.javaThemeInfo);\n" +
"}\n" +
"\n" +
"var event = new CustomEvent('javaThemeChanged', {\n" +
" detail: window.javaThemeInfo\n" +
"});\n" +
"document.dispatchEvent(event);\n" +
"console.log('The javaThemeChanged event is dispatched');";
browser.executeJavaScript(script, browser.getURL(), 0);
browser.executeJavaScript(
"console.log('Theme information injection is completewindow.javaThemeInfo:', typeof window.javaThemeInfo);" +
"console.log('Number of theme event listeners:', document.eventListeners ? document.eventListeners('javaThemeChanged') : '无法获取');",
browser.getURL(), 0
);
}
});
}
/**
* 获取Java字体信息从UIManager获取
*/
private String getSystemFontsInfo() {
try {
Gson gson = new Gson();
JsonObject fontInfo = new JsonObject();
JsonObject uiFonts = new JsonObject();
String[] fontKeys = {
"Label.font", "Button.font", "ToggleButton.font", "RadioButton.font",
"CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font",
"TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font",
"FormattedTextField.font", "Table.font", "TableHeader.font", "List.font",
"Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font",
"PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font",
"Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont",
"OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font"
};
for (String key : fontKeys) {
Font font = UIManager.getFont(key);
if (font != null) {
JsonObject fontObj = new JsonObject();
fontObj.addProperty("name", font.getFontName());
fontObj.addProperty("family", font.getFamily());
fontObj.addProperty("size", font.getSize());
fontObj.addProperty("style", font.getStyle());
fontObj.addProperty("bold", font.isBold());
fontObj.addProperty("italic", font.isItalic());
fontObj.addProperty("plain", font.isPlain());
uiFonts.add(key, fontObj);
}
}
fontInfo.add("uiFonts", uiFonts);
fontInfo.addProperty("timestamp", System.currentTimeMillis());
fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName());
return gson.toJson(fontInfo);
} catch (Exception e) {
return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}";
}
}
/**
* 注入字体信息到页面并设置字体
*/
private void injectFontInfoToPage(CefBrowser browser, String fontInfo) {
if (client == null) {
return;
}
client.addLoadHandler(new CefLoadHandlerAdapter() {
@Override
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
// 使用更简单的脚本来注入字体信息
String script =
"if (typeof window.javaFontInfo === 'undefined') {" +
" window.javaFontInfo = " + fontInfo + ";" +
" console.log('Java font information has been loaded:', window.javaFontInfo);" +
" " +
" var event = new CustomEvent('javaFontsLoaded', {" +
" detail: window.javaFontInfo" +
" });" +
" document.dispatchEvent(event);" +
" console.log('The javaFontsLoaded event is dispatched');" +
"}";
System.out.println("正在注入字体信息到页面...");
browser.executeJavaScript(script, browser.getURL(), 0);
// 添加调试信息
browser.executeJavaScript(
"console.log('Font information injection is completewindow.javaFontInfo:', typeof window.javaFontInfo);" +
"console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');",
browser.getURL(), 0
);
}
});
}
public static void printStackTrace() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (int i = 2; i < stackTrace.length; i++) {
@@ -558,6 +746,11 @@ public class BrowserWindowJDialog extends JDialog {
@Override
public void setVisible(boolean b) {
if (b) {
if (browser != null) {
updateTheme();
}
}
super.setVisible(b);
}

View File

@@ -56,6 +56,9 @@ public class CefAppManager {
}
}
/**
* 初始化Cef
*/
private static void initializeDefaultSettings() {
initLock.lock();
try {
@@ -81,6 +84,9 @@ public class CefAppManager {
String processType,
CefCommandLine commandLine
) {
//commandLine.appendSwitch("disable-dev-tools");
//commandLine.appendSwitch("disable-view-source");
LanguageManager.loadSavedLanguage();
LanguageManager.Language currentLang = LanguageManager.getLoadedLanguages();
if (currentLang != null){
@@ -109,6 +115,10 @@ public class CefAppManager {
}
}
/**
* 判断地区主题是否为黑色主题
* @return 是否
*/
private static boolean isDarkTheme() {
if (AxisInnovatorsBox.getMain() == null){
return false;

View File

@@ -57,6 +57,20 @@ public class WindowRegistry {
return windows.get(windowId);
}
public void update() {
for (BrowserWindow window : windows.values()) {
if (window != null) {
window.updateTheme();
}
}
for (BrowserWindowJDialog window : childWindows.values()) {
if (window != null) {
window.updateTheme();
}
}
}
/**
* 创建一个新的窗口
* @param windowId 窗口ID

View File

@@ -21,6 +21,13 @@ public class CodeExecutor {
void onOutput(String newOutput);
}
/**
* 执行代码
* @param code 代码字符串
* @param language 代码类型
* @param listener 回调,代码的输出回调
* @return 返回i执行结果
*/
public static String executeCode(String code, String language, OutputListener listener) {
switch (language.toLowerCase()) {
case "python":
@@ -35,6 +42,10 @@ public class CodeExecutor {
}
}
/**
* 执行Java代码
* @return 返回执行结果
*/
public static String executeJavaCode(String code, OutputListener listener) {
Path tempDir = null;
try {
@@ -176,6 +187,9 @@ public class CodeExecutor {
}
}
/**
* 执行C代码
*/
private static String executeC(String code, OutputListener listener) {
Path tempDir = null;
Path cFile = null;
@@ -316,63 +330,6 @@ public class CodeExecutor {
}
}
private static String captureProcessOutput(Process process, OutputListener listener)
throws IOException {
StringBuilder totalOutput = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
totalOutput.append(line).append("\n");
if (listener != null) {
listener.onOutput(line + "\n");
}
}
}
return totalOutput.toString();
}
// 输出监控线程
private static class OutputMonitor implements Runnable {
private final BufferedReader reader;
private final OutputListener listener;
private volatile boolean running = true;
private final StringBuilder totalOutput = new StringBuilder();
public OutputMonitor(BufferedReader reader, OutputListener listener) {
this.reader = reader;
this.listener = listener;
}
public void run() {
try {
while (running) {
if (reader.ready()) {
String line = reader.readLine();
if (line != null) {
totalOutput.append(line).append("\n");
if (listener != null) {
listener.onOutput(line + "\n");
}
}
} else {
Thread.sleep(50);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void stop() {
running = false;
}
public String getTotalOutput() {
return totalOutput.toString();
}
}
// 使用方法
public static void main(String[] args) {

View File

@@ -0,0 +1,400 @@
package com.axis.innovators.box.browser.util;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 数据库连接管理器
* @author tzdwindows 7
*/
public class DatabaseConnectionManager {
private static final java.util.Map<String, Connection> connections = new java.util.concurrent.ConcurrentHashMap<>();
private static final java.util.Map<String, DatabaseInfo> connectionInfo = new java.util.concurrent.ConcurrentHashMap<>();
public static class DatabaseInfo {
public String driver;
public String url;
public String host;
public String port;
public String database;
public String username;
public DatabaseInfo(String driver, String url, String host, String port, String database, String username) {
this.driver = driver;
this.url = url;
this.host = host;
this.port = port;
this.database = database;
this.username = username;
}
}
public static String connect(String driver, String host, String port,
String database, String username, String password) throws SQLException {
String connectionId = "conn_" + System.currentTimeMillis();
String drv = driver == null ? "" : driver.toLowerCase();
// 规范化 database 路径(特别是 Windows 反斜杠问题)
if (database != null) {
database = database.replace("\\", "/");
} else {
database = "";
}
// 先显式加载驱动,避免因为 classloader 问题找不到驱动
try {
switch (drv) {
case "mysql":
Class.forName("com.mysql.cj.jdbc.Driver");
break;
case "postgresql":
Class.forName("org.postgresql.Driver");
break;
case "sqlite":
Class.forName("org.sqlite.JDBC");
break;
case "oracle":
Class.forName("oracle.jdbc.OracleDriver");
break;
case "h2":
Class.forName("org.h2.Driver");
break;
default:
// 不抛出,使后续 URL 构造仍可检查类型
}
} catch (ClassNotFoundException e) {
throw new SQLException("JDBC 驱动未找到,请确认对应驱动已加入 classpath: " + e.getMessage(), e);
}
String url = buildConnectionUrl(driver, host, port, database);
Connection connection;
Properties props = new Properties();
if (username != null && !username.isEmpty()) props.setProperty("user", username);
if (password != null && !password.isEmpty()) props.setProperty("password", password);
switch (drv) {
case "mysql":
props.setProperty("useSSL", "false");
props.setProperty("serverTimezone", "UTC");
props.setProperty("allowPublicKeyRetrieval", "true");
props.setProperty("useUnicode", "true");
props.setProperty("characterEncoding", "UTF-8");
connection = DriverManager.getConnection(url, props);
break;
case "postgresql":
connection = DriverManager.getConnection(url, props);
break;
case "sqlite":
// sqlite 不需要 propsURL 已经是文件路径(已做过替换)
connection = DriverManager.getConnection(url);
break;
case "oracle":
connection = DriverManager.getConnection(url, props);
break;
case "h2":
// H2 使用默认用户 sa / 空密码(如果需要可调整)
connection = DriverManager.getConnection(url, "sa", "");
break;
default:
throw new SQLException("不支持的数据库类型: " + driver);
}
connections.put(connectionId, connection);
connectionInfo.put(connectionId, new DatabaseInfo(driver, url, host, port, database, username));
return connectionId;
}
public static void disconnect(String connectionId) throws SQLException {
Connection connection = connections.get(connectionId);
if (connection != null && !connection.isClosed()) {
connection.close();
}
connections.remove(connectionId);
connectionInfo.remove(connectionId);
}
public static Connection getConnection(String connectionId) {
return connections.get(connectionId);
}
public static DatabaseInfo getConnectionInfo(String connectionId) {
return connectionInfo.get(connectionId);
}
private static String buildConnectionUrl(String driver, String host, String port, String database) {
String drv = driver == null ? "" : driver.toLowerCase();
switch (drv) {
case "mysql":
return "jdbc:mysql://" + host + ":" + port + "/" + database;
case "postgresql":
return "jdbc:postgresql://" + host + ":" + port + "/" + database;
case "sqlite":
// 对于 SQLitedatabase 可能是绝对路径或相对文件名,先把反斜杠替成正斜杠
if (database == null || database.isEmpty()) {
return "jdbc:sqlite::memory:";
}
String normalized = database.replace("\\", "/");
// 如果看起来像相对文件名(不含冒号也不以 / 开头),则当作相对于用户目录的路径
if (!normalized.contains(":") && !normalized.startsWith("/")) {
String userHome = System.getProperty("user.home").replace("\\", "/");
normalized = userHome + "/" + normalized;
}
return "jdbc:sqlite:" + normalized;
case "oracle":
return "jdbc:oracle:thin:@" + host + ":" + port + ":" + database;
case "h2":
// H2 文件路径同样做反斜杠处理
if (database == null || database.isEmpty()) {
String userHome = System.getProperty("user.home").replace("\\", "/");
return "jdbc:h2:file:" + userHome + "/.axis_innovators_box/databases/h2db";
} else {
String norm = database.replace("\\", "/");
// 如果传入仅是名字(无斜杠或冒号),则存到用户目录下
if (!norm.contains("/") && !norm.contains(":")) {
String userHome = System.getProperty("user.home").replace("\\", "/");
norm = userHome + "/.axis_innovators_box/databases/" + norm;
}
return "jdbc:h2:file:" + norm;
}
default:
throw new IllegalArgumentException("不支持的数据库类型: " + driver);
}
}
/**
* 在服务器上创建数据库MySQL / PostgreSQL / Oracle示例
* @param driver mysql | postgresql | oracle
* @param host 数据库主机
* @param port 端口
* @param dbName 要创建的数据库名(或 schema 名)
* @param adminUser 管理员用户名(用于创建数据库)
* @param adminPassword 管理员密码
* @return 如果创建成功返回一个简短消息,否则抛出 SQLException
* @throws SQLException
*/
public static String createDatabaseOnServer(String driver, String host, String port,
String dbName, String adminUser, String adminPassword) throws SQLException {
if (driver == null) throw new SQLException("driver 不能为空");
String drv = driver.toLowerCase().trim();
// 简单校验 dbName避免注入——只允许字母数字下划线
if (dbName == null || !dbName.matches("[A-Za-z0-9_]+")) {
throw new SQLException("不合法的数据库名: " + dbName);
}
try {
switch (drv) {
case "mysql":
// 加载驱动(如果尚未加载)
try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) {
throw new SQLException("MySQL 驱动未找到,请加入 mysql-connector-java 到 classpath", e);
}
// 连接到服务器的默认库(不指定数据库)以执行 CREATE DATABASE
String mysqlUrl = "jdbc:mysql://" + host + ":" + port + "/?useSSL=false&serverTimezone=UTC";
try (Connection conn = DriverManager.getConnection(mysqlUrl, adminUser, adminPassword);
Statement st = conn.createStatement()) {
String sql = "CREATE DATABASE `" + dbName + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci";
st.executeUpdate(sql);
}
return "MySQL 数据库创建成功: " + dbName;
case "postgresql":
case "postgres":
try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e) {
throw new SQLException("Postgres 驱动未找到,请加入 postgresql 到 classpath", e);
}
// 连接到默认 postgres 数据库以创建新数据库
String pgUrl = "jdbc:postgresql://" + host + ":" + port + "/postgres";
try (Connection conn = DriverManager.getConnection(pgUrl, adminUser, adminPassword);
Statement st = conn.createStatement()) {
String sql = "CREATE DATABASE " + dbName + " WITH ENCODING 'UTF8'";
st.executeUpdate(sql);
}
return "PostgreSQL 数据库创建成功: " + dbName;
case "oracle":
// Oracle 数据库“创建数据库”通常由 DBA 完成(复杂),这里示例创建用户/模式(更常见)
try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException e) {
throw new SQLException("Oracle 驱动未找到,请把 ojdbc.jar 加入 classpath", e);
}
// 需使用具有足够权限的账户(通常为 sys or system并且 URL 需要正确SID / ServiceName
// 下面示例假设通过 SID 连接: jdbc:oracle:thin:@host:port:SID
String oracleUrl = "jdbc:oracle:thin:@" + host + ":" + port + ":" + "ORCL"; // 把 ORCL 换成实际 SID
try (Connection conn = DriverManager.getConnection(oracleUrl, adminUser, adminPassword);
Statement st = conn.createStatement()) {
// 创建 userschema示例
String pwd = adminPassword; // 实际应使用独立密码,不推荐用 adminPassword
String createUser = "CREATE USER " + dbName + " IDENTIFIED BY \"" + pwd + "\"";
String grant = "GRANT CONNECT, RESOURCE TO " + dbName;
st.executeUpdate(createUser);
st.executeUpdate(grant);
} catch (SQLException ex) {
// Oracle 操作更容易失败,给出提示
throw new SQLException("Oracle: 无法创建用户/模式,请检查权限和 URL通常需由 DBA 操作): " + ex.getMessage(), ex);
}
return "Oracle 用户/模式创建成功(注意:真正的 DB 实例通常由 DBA 管理): " + dbName;
default:
throw new SQLException("不支持的数据库类型: " + driver);
}
} catch (SQLException se) {
// 透传 SQLException调用方会拿到 message 并反馈给前端
throw se;
} catch (Exception e) {
throw new SQLException("创建数据库时发生异常: " + e.getMessage(), e);
}
}
public static String createLocalDatabase(String driver, String dbName) throws SQLException {
switch (driver.toLowerCase()) {
case "sqlite":
// 创建目录并构造规范化路径(确保路径使用正斜杠)
String dbFileName = dbName.endsWith(".db") ? dbName : (dbName + ".db");
java.nio.file.Path dbDir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases");
try {
java.nio.file.Files.createDirectories(dbDir);
} catch (Exception e) {
throw new SQLException("无法创建数据库目录: " + e.getMessage(), e);
}
String dbPath = dbDir.resolve(dbFileName).toAbsolutePath().toString().replace("\\", "/");
// 显式加载 sqlite 驱动(避免 No suitable driver
try {
Class.forName("org.sqlite.JDBC");
} catch (ClassNotFoundException e) {
throw new SQLException("未找到 sqlite 驱动,请确认 sqlite-jdbc 已加入 classpath", e);
}
// 直接使用 connect 构建连接connect 中会通过 buildConnectionUrl 处理 path
String connectionId = connect("sqlite", "", "", dbPath, "", "");
// 创建示例表
createSampleTables(connectionId);
return connectionId;
case "h2":
java.nio.file.Path h2Dir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases");
try {
java.nio.file.Files.createDirectories(h2Dir);
} catch (Exception e) {
throw new SQLException("无法创建数据库目录: " + e.getMessage(), e);
}
String h2Path = h2Dir.resolve(dbName).toAbsolutePath().toString().replace("\\", "/");
String h2Url = "jdbc:h2:file:" + h2Path;
try {
Class.forName("org.h2.Driver");
} catch (ClassNotFoundException e) {
throw new SQLException("未找到 H2 驱动,请确认 h2.jar 已加入 classpath", e);
}
Connection h2Conn = DriverManager.getConnection(h2Url, "sa", "");
String h2ConnectionId = "conn_" + System.currentTimeMillis();
connections.put(h2ConnectionId, h2Conn);
connectionInfo.put(h2ConnectionId, new DatabaseInfo("h2", h2Url, "localhost", "", dbName, "sa"));
createSampleTables(h2ConnectionId);
return h2ConnectionId;
default:
throw new SQLException("不支持创建本地数据库类型: " + driver);
}
}
private static void createSampleTables(String connectionId) throws SQLException {
Connection conn = getConnection(connectionId);
DatabaseInfo info = getConnectionInfo(connectionId);
if ("sqlite".equals(info.driver) || "h2".equals(info.driver)) {
try (Statement stmt = conn.createStatement()) {
// 创建用户表
stmt.execute("CREATE TABLE IF NOT EXISTS users (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"username VARCHAR(50) NOT NULL UNIQUE, " +
"email VARCHAR(100) NOT NULL, " +
"password VARCHAR(100) NOT NULL, " +
"status VARCHAR(20) DEFAULT 'active', " +
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
")");
// 创建产品表
stmt.execute("CREATE TABLE IF NOT EXISTS products (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"name VARCHAR(100) NOT NULL, " +
"description TEXT, " +
"price DECIMAL(10,2) NOT NULL, " +
"stock INTEGER DEFAULT 0, " +
"category VARCHAR(50), " +
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
")");
// 创建订单表
stmt.execute("CREATE TABLE IF NOT EXISTS orders (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"user_id INTEGER, " +
"product_id INTEGER, " +
"quantity INTEGER NOT NULL, " +
"total_price DECIMAL(10,2) NOT NULL, " +
"status VARCHAR(20) DEFAULT 'pending', " +
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
"FOREIGN KEY (user_id) REFERENCES users(id), " +
"FOREIGN KEY (product_id) REFERENCES products(id)" +
")");
// 插入示例数据
insertSampleData(conn);
}
}
}
private static void insertSampleData(Connection conn) throws SQLException {
// 检查是否已有数据
try (Statement checkStmt = conn.createStatement();
ResultSet rs = checkStmt.executeQuery("SELECT COUNT(*) FROM users")) {
if (rs.next() && rs.getInt(1) == 0) {
// 插入用户数据
try (PreparedStatement pstmt = conn.prepareStatement(
"INSERT INTO users (username, email, password) VALUES (?, ?, ?)")) {
String[][] users = {
{"admin", "admin@example.com", "password123"},
{"user1", "user1@example.com", "password123"},
{"user2", "user2@example.com", "password123"}
};
for (String[] user : users) {
pstmt.setString(1, user[0]);
pstmt.setString(2, user[1]);
pstmt.setString(3, user[2]);
pstmt.executeUpdate();
}
}
// 插入产品数据
try (PreparedStatement pstmt = conn.prepareStatement(
"INSERT INTO products (name, description, price, stock, category) VALUES (?, ?, ?, ?, ?)")) {
Object[][] products = {
{"笔记本电脑", "高性能笔记本电脑", 5999.99, 50, "电子"},
{"智能手机", "最新款智能手机", 3999.99, 100, "电子"},
{"办公椅", "舒适办公椅", 299.99, 30, "家居"},
{"咖啡机", "全自动咖啡机", 899.99, 20, "家电"}
};
for (Object[] product : products) {
pstmt.setString(1, (String) product[0]);
pstmt.setString(2, (String) product[1]);
pstmt.setDouble(3, (Double) product[2]);
pstmt.setInt(4, (Integer) product[3]);
pstmt.setString(5, (String) product[4]);
pstmt.executeUpdate();
}
}
}
}
}
}

View File

@@ -0,0 +1,28 @@
package com.axis.innovators.box.events;
/**
* 当主题变更时被调用
* @author tzdwindows 7
*/
public class TopicsUpdateEvents {
private final String themeName;
private final boolean darkTheme;
/**
* 构造函数
* @param themeName 主题名称
* @param darkTheme 是否是暗主题
*/
public TopicsUpdateEvents(String themeName, boolean darkTheme) {
this.themeName = themeName;
this.darkTheme = darkTheme;
}
public String getThemeName() {
return themeName;
}
public boolean isDarkTheme() {
return darkTheme;
}
}

View File

@@ -0,0 +1,17 @@
package com.axis.innovators.box.login;
import cn.dev33.satoken.stp.StpUtil;
/**
* @author tzdwindows 7
*/
public class LoginStpUtil {
public static void login(Object id){
StpUtil.login(id);
}
public static void main(String[] args) {
LoginStpUtil.login(1);
}
}

View File

@@ -6,6 +6,7 @@ import com.axis.innovators.box.window.FridaWindow;
import com.axis.innovators.box.window.JarApiProfilingWindow;
import com.axis.innovators.box.window.MainWindow;
import com.axis.innovators.box.plugins.PluginDescriptor;
import com.axis.innovators.box.window.TaskbarAppearanceWindow;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.tzd.lm.LM;
@@ -47,7 +48,6 @@ public class RegistrationTool {
}
}));
MainWindow.ToolCategory programmingToolsCategory = new MainWindow.ToolCategory("编程工具",
"programming/programming.png",
"编程工具");
@@ -82,6 +82,16 @@ public class RegistrationTool {
}
}));
programmingToolsCategory.addTool(new MainWindow.ToolItem("数据库管理工具", "programming/programming_dark.png",
"用于管理数据库" +
"\n作者tzdwindows 7", ++id, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
// Window owner = SwingUtilities.windowForComponent((Component) e.getSource());
MainApplication.popupDataBaseWindow();
}
}));
MainWindow.ToolCategory aICategory = new MainWindow.ToolCategory("AI工具",
"ai/ai.png",
"人工智能/大语言模型");
@@ -108,9 +118,44 @@ public class RegistrationTool {
}
}));
MainWindow.ToolCategory hahahah = new MainWindow.ToolCategory(
"good工具",
"haha/ok.png",
"good "
);
hahahah.addTool(new MainWindow.ToolItem("123", "ai/local/local_main.png",
"456789" +
"\n作者Vinfya", ++id, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
// 在这里写
// 这个就是弹窗Ok
JOptionPane.showMessageDialog(null, "你好...");
}
}));
MainWindow.ToolCategory systemCategory = new MainWindow.ToolCategory("系统工具",
"windows/windows.png",
"系统工具");
systemCategory.addTool(new MainWindow.ToolItem("任务栏主题设置", "windows/windowsOptimization/windowsOptimization.png",
"可以设置Windows任务栏的颜色等各种信息",++id, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
Window owner = SwingUtilities.windowForComponent((Component) e.getSource());
TaskbarAppearanceWindow taskbarAppearanceWindow = new TaskbarAppearanceWindow(owner);
main.popupWindow(taskbarAppearanceWindow);
}
}));
addToolCategory(debugCategory, "system:debugTools");
addToolCategory(aICategory,"system:fridaTools");
addToolCategory(programmingToolsCategory, "system:programmingTools");
addToolCategory(systemCategory, "system:systemTools");
addToolCategory(hahahah, "system:mc");
}
/**

View File

@@ -0,0 +1,51 @@
package com.axis.innovators.box.tools.Crypto;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.SecureRandom;
import java.util.Base64;
public class AESCryptoUtil {
private static final String KEY_FILE = System.getProperty("user.home") + "/.lingqi/.axis_box_key";
// 获取密钥Base64字符串长度16字节
public static byte[] getKeyBytes() throws Exception {
Path path = Paths.get(KEY_FILE);
if (Files.exists(path)) {
byte[] encrypted = Base64.getDecoder().decode(Files.readAllBytes(path));
return WindowsDPAPIUtil.unprotect(encrypted);
} else {
// 首次生成密钥
byte[] keyBytes = new byte[16];
new SecureRandom().nextBytes(keyBytes);
byte[] encrypted = WindowsDPAPIUtil.protect(keyBytes);
Files.createDirectories(path.getParent());
Files.write(path, Base64.getEncoder().encode(encrypted));
return keyBytes;
}
}
// 加密
public static String encrypt(String data) throws Exception {
byte[] keyBytes = getKeyBytes();
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
}
// 解密
public static String decrypt(String encrypted) throws Exception {
byte[] keyBytes = getKeyBytes();
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decoded = Base64.getDecoder().decode(encrypted);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,15 @@
package com.axis.innovators.box.tools.Crypto;
import java.util.Base64;
public class Base64CryptoUtil {
public static String base64Encode(String input) {
return Base64.getEncoder().encodeToString(input.getBytes());
}
public static String base64Decode(String input) {
byte[] decodedBytes = Base64.getDecoder().decode(input);
return new String(decodedBytes);
}
}

View File

@@ -0,0 +1,12 @@
package com.axis.innovators.box.tools.Crypto;
import java.security.MessageDigest;
import java.util.Base64;
public class HashUtil {
public static String sha256(String input) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(hash); // 输出为Base64字符串
}
}

View File

@@ -0,0 +1,15 @@
package com.axis.innovators.box.tools.Crypto;
import com.sun.jna.platform.win32.Crypt32Util;
public class WindowsDPAPIUtil {
// 加密
public static byte[] protect(byte[] data) {
return Crypt32Util.cryptProtectData(data);
}
// 解密
public static byte[] unprotect(byte[] encrypted) {
return Crypt32Util.cryptUnprotectData(encrypted);
}
}

View File

@@ -20,6 +20,13 @@ public class FolderCreator {
public static final String LANGUAGE_PATH = "language";
public static final String CONFIGURATION_PATH = "state";
public static final String JAVA_SCRIPT_PATH = "javascript";
public static String PLUGIN_PATH_ = "";
public static void setPluginPath(String pluginPath) {
PLUGIN_PATH_ = pluginPath;
}
public static String getConfigurationFolder() {
String folder = createFolder(CONFIGURATION_PATH);
if (folder == null) {
@@ -46,6 +53,9 @@ public class FolderCreator {
return folder;
}
public static String getPluginFolder() {
if (!PLUGIN_PATH_.isEmpty()){
return PLUGIN_PATH_;
}
String folder = createFolder(PLUGIN_PATH);
if (folder == null) {
logger.error("Plugin folder creation failed, please use administrator privileges to execute this procedure");

View File

@@ -0,0 +1,42 @@
package com.axis.innovators.box.tools;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.apache.logging.log4j.LogManager;
public class LoadResource {
public static String readString(String srcPath) {
try {
java.net.URL url = LoadResource.class.getResource(srcPath);
if (url == null) {
LogManager.getLogger(LoadResource.class).error("资源文件未找到: " + srcPath);
return null;
}
java.nio.file.Path path = java.nio.file.Paths.get(url.toURI());
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
} catch (Exception e) {
LogManager.getLogger(LoadResource.class).error("读取资源文件失败", e);
return null;
}
}
public static String loadString(Class<?> clazz, String srcPath) {
try {
java.net.URL url = clazz.getResource(srcPath);
if (url == null) {
LogManager.getLogger(LoadResource.class).error("资源文件未找到: " + srcPath);
return null;
}
java.nio.file.Path path = java.nio.file.Paths.get(url.toURI());
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
} catch (Exception e) {
LogManager.getLogger(LoadResource.class).error("读取资源文件失败", e);
return null;
}
}
}

View File

@@ -12,10 +12,6 @@ import java.util.List;
*/
public class RegisterTray {
static {
LibraryLoad.loadLibrary("RegisterTray");
}
/**
* 托盘菜单项构建器(流畅接口)
*/
@@ -26,39 +22,28 @@ public class RegisterTray {
* 添加菜单项
* @param id 菜单项唯一标识符需大于0
* @param label 菜单显示文本
* @param onClick 点击事件处理器
* @param onClick 点击事件处理器(接收 itemId
* @return 当前构建器实例
*/
public MenuBuilder addItem(int id, String label, MenuItemClickListener onClick) {
items.add(new Item(
id,
label,
"", "", "",
(Event) combinedId -> {
int itemId = (int)(combinedId >> 32);
onClick.onClick(itemId);
// 为每一项创建一个 Event 回调对象native 端会在点击时回调此 Event.onClick(long)
// 这里我们忽略 native 传回的 trayId只把 itemId 传给上层的 MenuItemClickListener
Event ev = new Event() {
@Override
public void onClick(long trayId) {
try {
onClick.onClick(id);
} catch (Throwable t) {
t.printStackTrace();
}
));
return this;
}
};
/**
* 添加菜单项
* @param id 菜单项唯一标识符需大于0
* @param label 菜单显示文本
* @param onClick 点击事件处理器
* @return 当前构建器实例
*/
public MenuBuilder addItem(MenuBuilder builder,int id, String label, MenuItemClickListener onClick) {
this.items = builder.items;
items.add(new Item(
id,
label,
"", "", "",
(Event) combinedId -> {
int itemId = (int)(combinedId >> 32);
onClick.onClick(itemId);
}
ev
));
return this;
}
@@ -72,6 +57,7 @@ public class RegisterTray {
}
}
/**
* 托盘配置器(流畅接口)
*/
@@ -143,7 +129,6 @@ public class RegisterTray {
title,
menuItems,
iconPath,
tooltip,
clickListener::onClick
);
} catch (Exception e) {
@@ -185,8 +170,13 @@ public class RegisterTray {
}
}
public static native long register(String name, List<Item> value,
String icon, String description, Event event);
public static native long register(String name, List<Item> items, String icon, Event event);
/**
* 更强的变体:允许提供 description可以用于 tooltip 或弹出顶部信息)。
* 推荐使用 registerEx 来获取现代化圆角弹出菜单。
*/
public static native long registerEx(String name, List<Item> items, String icon, String description, Event event);
public static native void unregister(long id);
public interface Event {

View File

@@ -60,22 +60,17 @@ public class Tray {
* @throws RegisterTray.TrayException 抛出错误
*/
public static void load(TrayLabels trayLabels) throws RegisterTray.TrayException {
if (trayLabels == null || trayLabelsList.contains(trayLabels)){
if (trayLabels == null || trayLabelsList.contains(trayLabels)) {
System.err.println("trayLabels is null or trayLabelsList contains trayLabels");
return;
}
trayLabelsList.add(trayLabels);
if (menuBuilders == null) {
RegisterTray.MenuBuilder menuBuilder = new RegisterTray.MenuBuilder()
.addItem(trayLabels.id, trayLabels.name, itemId -> trayLabels.action.run());
menuBuilders = menuBuilder;
menuItems = menuBuilder.build();
} else {
menuBuilders = new RegisterTray.MenuBuilder()
.addItem(menuBuilders, trayLabels.id, trayLabels.name, itemId -> trayLabels.action.run());
menuItems = menuBuilders.build();
menuBuilders = new RegisterTray.MenuBuilder();
}
menuBuilders.addItem(trayLabels.id, trayLabels.name, itemId -> trayLabels.action.run());
menuItems = menuBuilders.build();
}
public static void addAction(Runnable action){

View File

@@ -1,47 +0,0 @@
package com.axis.innovators.box.util;
import com.axis.innovators.box.AxisInnovatorsBox;
import com.axis.innovators.box.verification.OnlineVerification;
import com.axis.innovators.box.verification.UserTags;
import com.axis.innovators.box.verification.VerificationService;
/**
* 用于存储用户信息
* @author tzdwindows 7
*/
public class UserLocalInformation {
private final AxisInnovatorsBox main;
public UserLocalInformation(AxisInnovatorsBox main){
this.main = main;
}
/**
* 设置用户信息
* @param userTags 用户信息
*/
public void setUserTags(UserTags userTags){
OnlineVerification onlineVerification = userTags.getUser();
main.getStateManager().saveState("password", onlineVerification.password);
main.getStateManager().saveState("verification", onlineVerification.onlineVerification);
}
/**
* 获取用户信息
* @return 用户信息
*/
public UserTags getUserTags(){
String verification = main.getStateManager().getState("verification");
String password = main.getStateManager().getState("password");
if (verification == null || password == null){
return null;
}
OnlineVerification onlineVerification = OnlineVerification.validateLogin(
verification,
password);
if (onlineVerification == null){
return null;
}
return VerificationService.determineUserType(onlineVerification);
}
}

View File

@@ -0,0 +1,64 @@
package com.axis.innovators.box.util;
/**
* Windows主题变更监听器
* @author tzdwindows 7
*/
public class WindowsTheme {
/**
* 设置主题改变监听器
* @param listener 监听器
*/
public static native void setThemeChangeListener(ThemeChangeListener listener);
/**
* 主题改变监听器
*/
public interface ThemeChangeListener {
/**
* 当主题改变时调用
* @param themeName 主题名称
* @param darkTheme 是否是暗主题
*/
void themeChanged(String themeName,boolean darkTheme);
}
/**
* 获取当前是否为暗色主题
* @return true-暗色主题, false-亮色主题
*/
public static native boolean isDarkTheme();
/**
* 获取当前主题名称
* @return 主题名称
*/
public static native String getThemeName();
//public static void main(String[] args) {
// try {
// System.load("C:\\Users\\Administrator\\source\\repos\\RegisterTray\\x64\\Release\\RegisterTray.dll");
//
// // 先测试直接获取主题信息的方法
// System.out.println("Current topic Name: " + WindowsTheme.getThemeName());
// System.out.println("Is it a dark theme: " + WindowsTheme.isDarkTheme());
//
// WindowsTheme.setThemeChangeListener(new WindowsTheme.ThemeChangeListener() {
// @Override
// public void themeChanged(String themeName, boolean darkTheme) {
// System.out.println("The theme has changed: " + themeName + " is dark theme: " + darkTheme);
// }
// });
//
// System.out.println("Start listening for theme changes... Press Enter to exit");
//
// System.in.read();
//
// } catch (Exception e) {
// e.printStackTrace();
// } finally {
// WindowsTheme.setThemeChangeListener(null);
// }
//}
}

View File

@@ -0,0 +1,102 @@
package com.axis.innovators.box.verification;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.casbin.casdoor.config.Config;
import org.casbin.casdoor.entity.User;
import org.casbin.casdoor.service.AuthService;
import com.axis.innovators.box.tools.LoadResource;
import config.CasdoorConfig;
public class CasdoorServer {
private static final Logger logger = LogManager.getLogger(CasdoorServer.class);
private final AuthService authService;
private final Config config;
private final String certificate;
public CasdoorServer() {
this.certificate = LoadResource.readString("/cert/casdoor_cert.pem");
this.config = new Config(
CasdoorConfig.CASDOOR_API_URL,
CasdoorConfig.CASDOOR_CLIENT_ID,
CasdoorConfig.CASDOOR_CLIENT_SECRET,
this.certificate,
CasdoorConfig.CASDOOR_ORGANIZATION_NAME,
CasdoorConfig.CASDOOR_APPLICATION_NAME);
this.authService = new AuthService(this.config);
}
public AuthService getAuthService() {
return authService;
}
public Config getConfig() {
return config;
}
public String getCertificate() {
return certificate;
}
/**
* 获取登录 URL
* @return 登录 URL
*/
public String getSigninUrl() {
return authService.getSigninUrl(CasdoorConfig.CASDOOR_LOGIN_REDIRECT_URI);
}
/**
* 获取注册 URL
* @return 注册 URL
*/
public String getSignupUrl() {
String redirectUrl = CasdoorConfig.CASDOOR_SIGNUP_REDIRECT_URI;
if (redirectUrl == null) {
redirectUrl = getSigninUrl(); // 如果没有设置注册回调地址,则使用登录回调地址
}
return authService.getSignupUrl(redirectUrl);
}
/**
* 获取 OAuth Token
* @param code 登录回调的 code 参数
* @param state 登录回调的 state 参数
* @return
*/
public String getOAuthToken(String code, String state) {
try {
return authService.getOAuthToken(code, state);
} catch (Exception e) {
logger.error("获取 OAuth Token 失败: " + e.getMessage());
return null;
}
}
/**
* 解析 JWT Token
* @param token 令牌
* @return User 用户信息
*/
public User parseJwtToken(String token) {
try {
return authService.parseJwtToken(token);
} catch (Exception e) {
logger.error("解析 JWT Token 失败: " + e.getMessage());
return null;
}
}
/**
* 获取用户信息,同 parseJwtToken
* @param token 令牌
* @return User 用户信息
*/
public User getUserInfo(String token) {
return parseJwtToken(token);
}
}

View File

@@ -0,0 +1,21 @@
package com.axis.innovators.box.verification;
import org.casbin.casdoor.entity.User;
public class LoginData {
private String token;
private User user;
public LoginData(String token, User user) {
this.token = token;
this.user = user;
}
public String getToken() {
return token;
}
public User getUser() {
return user;
}
}

View File

@@ -0,0 +1,28 @@
package com.axis.innovators.box.verification;
import org.casbin.casdoor.entity.User;
public class LoginResult extends Result {
public LoginResult(boolean success, String message, LoginData data) {
super(success, message, data);
}
public String token() {
LoginData loginData = (LoginData)this.m_data;
return loginData != null ? loginData.getToken() : null;
}
public User user() {
LoginData loginData = (LoginData) this.m_data;
return loginData != null ? loginData.getUser() : null;
}
public LoginData loginData() {
return (LoginData) this.m_data;
}
public LoginData data() {
return (LoginData) this.m_data;
}
}

View File

@@ -1,33 +0,0 @@
package com.axis.innovators.box.verification;
/**
* 在线验证用户身份
* 用于报错用户的验证信息
* @author tzdwindows 7
*/
public class OnlineVerification {
public String onlineVerification;
public String password;
/* 我是错误信息,要返回错误请修改我 */
public static String errorMessage = "用户不存在";
/**
* 验证登录
* @param identifier 账号
* @param password 密码
*/
OnlineVerification(String identifier, String password){
this.onlineVerification = identifier;
this.password = password;
}
/**
* 验证登录
* @param identifier 账号
* @param password 密码
* @return 验证结果,如果返回null则表示验证失败使用errorMessage获取验证失败的原因
*/
public static OnlineVerification validateLogin(String identifier, String password){
return new OnlineVerification(identifier, password);
}
}

View File

@@ -0,0 +1,25 @@
package com.axis.innovators.box.verification;
public class Result {
protected final boolean m_success;
protected final String m_message;
protected final Object m_data;
public Result(boolean success, String message, Object data) {
this.m_success = success;
this.m_message = message;
this.m_data = data;
}
public boolean success() {
return m_success;
}
public String message() {
return m_message;
}
public Object data() {
return m_data;
}
}

View File

@@ -1,46 +0,0 @@
package com.axis.innovators.box.verification;
/**
* 用户标签组
* @author tzdwindows 7
*/
public enum UserTags {
/**
* 没有登录的标签
*/
None,
/**
* 普通用户标签
*/
RegularUsers,
/**
* 管理员标签
*/
AdminUsers,
/**
* VIP用户标签
*/
VipUsers,
/**
* SVip用户标签
*/
SVipUsers,
/**
* 企业用户标签
*/
EnterpriseUsers;
private OnlineVerification onlineVerification;
/**
* 设置用户组信息
* @param onlineVerification 用户验证结果信息
*/
void setUser(OnlineVerification onlineVerification) {
this.onlineVerification = onlineVerification;
}
public OnlineVerification getUser() {
return onlineVerification;
}
}

View File

@@ -1,38 +0,0 @@
package com.axis.innovators.box.verification;
/**
* @author tzdwindows 7
*/
public class VerificationService {
/**
* 确定用户类型
* @param identifier 用户
* @return 用户类型
*/
public static UserTags determineUserType(OnlineVerification identifier) {
UserTags userTags = UserTags.RegularUsers;
userTags.setUser(identifier);
return userTags;
}
/**
* 发送密码重置链接给用户
* @param text
* @return
*/
public static boolean sendPasswordReset(String text) {
return true;
}
/**
* 注册用户
* @param text
* @param text1
* @param pwd
* @return
*/
public static boolean registerUser(String text, String text1, String pwd) {
return true;
}
}

View File

@@ -0,0 +1,89 @@
package com.axis.innovators.box.verification.api;
import java.util.concurrent.CompletableFuture;
/**
* 用户API接口
* @author lyxyz5223
*/
public interface UserApi {
/**
* 发送验证码
* @param email 用户邮箱
* @return 发送结果
*/
CompletableFuture<UserApiResult> sendVerificationCode(String email);
/**
* 验证验证码
* @param email 用户邮箱
* @param verificationCode 验证码
* @return 验证结果
*/
CompletableFuture<UserApiResult> verifyCode(String email, String verificationCode);
/**
* 检验登录token有效性用于检验登录状态
* @param token 登录token
* @return 检验结果
*/
CompletableFuture<UserApiResult> checkTokenValidity(String token);
/**
* 申请新的token可以定期申请新的token保证安全性
* @param oldToken 旧的登录token
* @return 申请结果
*/
CompletableFuture<UserApiResult> applyNewToken(String oldToken);
/**
* 登录接口
* @param username 用户名
* @param password 密码(加密)
* @return { token: String, expiresIn: Number } 登录结果含token和有效期临近到期需要手动申请新的token每次登陆服务器将更新token有效期
*/
CompletableFuture<UserApiResult> login(String username, String password);
/**
* 登出接口
* @param username 用户名
* @return 登出结果
*/
CompletableFuture<UserApiResult> logout(String username);
/**
* 注册接口
* @param username 用户名
* @param password 密码(加密)
* @param email 邮箱
* @return 注册结果
*/
CompletableFuture<UserApiResult> register(String username, String password, String email);
/**
* 申请重置密码接口
* @param email 用户邮箱
* @return { resetToken: String, expiresIn: Number } 重置结果包含有效期内的重置token和有效期24小时内有效
*/
CompletableFuture<UserApiResult> postResetPasswordRequest(String email);
/**
* 重置密码接口
* @param email 用户邮箱
* @param resetToken 重置token有效期24小时内
* @param newPassword 新密码(加密)
* @return 重置结果
*/
CompletableFuture<UserApiResult> resetPassword(String email, String resetToken, String newPassword);
/**
* 修改密码接口
* @param username 用户名
* @param oldPassword 旧密码(加密)
* @param newPassword 新密码(加密)
* @return 修改结果
*/
CompletableFuture<UserApiResult> changePassword(String username, String oldPassword, String newPassword);
}

View File

@@ -0,0 +1,65 @@
package com.axis.innovators.box.verification.api;
import java.util.concurrent.CompletableFuture;
public class UserApiImpl implements UserApi {
@Override
public CompletableFuture<UserApiResult> sendVerificationCode(String email) {
// Implementation here
return CompletableFuture.completedFuture(new UserApiResult(true, "Code sent", null));
}
@Override
public CompletableFuture<UserApiResult> verifyCode(String email, String verificationCode) {
// Implementation here
return CompletableFuture.completedFuture(new UserApiResult(true, "Code verified", null));
}
@Override
public CompletableFuture<UserApiResult> checkTokenValidity(String token) {
// Implementation here
return CompletableFuture.completedFuture(new UserApiResult(true, "Token is valid", null));
}
@Override
public CompletableFuture<UserApiResult> applyNewToken(String oldToken) {
// Implementation here
return CompletableFuture.completedFuture(new UserApiResult(true, "New token applied", null));
}
@Override
public CompletableFuture<UserApiResult> login(String username, String password) {
// Implementation here
return CompletableFuture.completedFuture(new UserApiResult(true, "Login successful", null));
}
@Override
public CompletableFuture<UserApiResult> logout(String username) {
// Implementation here
return CompletableFuture.completedFuture(new UserApiResult(true, "Logout successful", null));
}
@Override
public CompletableFuture<UserApiResult> register(String username, String password, String email) {
// Implementation here
return CompletableFuture.completedFuture(new UserApiResult(true, "Registration successful", null));
}
@Override
public CompletableFuture<UserApiResult> postResetPasswordRequest(String email) {
// Implementation here
return CompletableFuture.completedFuture(new UserApiResult(true, "Reset password request sent", null));
}
@Override
public CompletableFuture<UserApiResult> resetPassword(String email, String resetToken, String newPassword) {
// Implementation here
return CompletableFuture.completedFuture(new UserApiResult(true, "Password reset successful", null));
}
@Override
public CompletableFuture<UserApiResult> changePassword(String username, String oldPassword, String newPassword) {
// Implementation here
return CompletableFuture.completedFuture(new UserApiResult(true, "Password changed successfully", null));
}
}

View File

@@ -0,0 +1,25 @@
package com.axis.innovators.box.verification.api;
public class UserApiResult {
private final boolean m_success;
private final String m_message;
private final Object m_data;
public UserApiResult(boolean success, String message, Object data) {
this.m_success = success;
this.m_message = message;
this.m_data = data;
}
public boolean success() {
return m_success;
}
public String message() {
return m_message;
}
public Object data() {
return m_data;
}
}

View File

@@ -0,0 +1,363 @@
package com.axis.innovators.box.window;
import com.axis.innovators.box.AxisInnovatorsBox;
import com.axis.innovators.box.browser.CefAppManager;
import com.axis.innovators.box.tools.LoadResource;
import com.axis.innovators.box.verification.CasdoorServer;
import com.axis.innovators.box.verification.LoginData;
import com.axis.innovators.box.verification.LoginResult;
import com.axis.innovators.box.verification.Result;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.awt.Desktop;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.casbin.casdoor.entity.User;
import javax.swing.*;
import config.CasdoorConfig;
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.browser.CefBrowser;
import org.cef.browser.CefFrame;
import org.cef.handler.CefContextMenuHandlerAdapter;
import org.cef.handler.CefKeyboardHandlerAdapter;
import org.cef.handler.CefLifeSpanHandlerAdapter;
import org.cef.handler.CefLoadHandlerAdapter;
import org.cef.callback.CefMenuModel;
import org.cef.callback.CefMenuModel.MenuId;
import org.cef.callback.CefContextMenuParams;
public class CasdoorLoginWindow {
private final Logger logger = LogManager.getLogger(CasdoorLoginWindow.class);
private final CasdoorServer casdoorServer;
private CefBrowser browser;
private HttpServer server;
private JDialog dialog;
private LoginResult loginResult = null;
private boolean windowVisible = true;
private boolean isModal = true;
public CasdoorLoginWindow() {
casdoorServer = new CasdoorServer();
}
private void startLocalCallbackServer() {
try {
server = HttpServer.create(new java.net.InetSocketAddress(CasdoorConfig.CASDOOR_WEB_SERVER_PORT), 0);
server.createContext("/casdoor/callback", this::handleCallback);
server.setExecutor(java.util.concurrent.Executors.newSingleThreadExecutor());
server.start();
} catch (IOException e) {
System.err.println("本地回调服务启动失败: " + e.getMessage());
}
}
private void handleCallback(HttpExchange exchange) throws IOException {
String query = exchange.getRequestURI().getQuery();
AtomicReference<String> code = new AtomicReference<>("");
AtomicReference<String> state = new AtomicReference<>("");
if (query != null) {
for (String param : query.split("&")) {
String[] kv = param.split("=");
if (kv.length == 2) {
if (kv[0].equals("code"))
code.set(kv[1]);
if (kv[0].equals("state"))
state.set(kv[1]);
logger.info("Received callback with code: " + code.get() + ", state: " + state.get());
loginResult = parseUserInfo(code.get(), state.get());
String response = "Login success, please close this window.";
exchange.sendResponseHeaders(200, response.getBytes().length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
}
}
}
}
private void initUI() {
if (browser == null) {
try {
CefApp cefApp = CefAppManager.getInstance();
CefClient client = cefApp.createClient();
browser = client.createBrowser(casdoorServer.getSigninUrl(), false, false);
if (AxisInnovatorsBox.getMain().isDebugEnvironment()) {
client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
@Override
public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
// 检测 F12
if (event.windows_key_code == 123) {
browser.getDevTools().createImmediately();
return true;
}
return false;
}
});
}
// 🔹 页面加载完成后注入 JS / CSS
client.addLoadHandler(new org.cef.handler.CefLoadHandlerAdapter() {
@Override
public void onLoadEnd(CefBrowser cefBrowser, org.cef.browser.CefFrame frame, int httpStatusCode) {
if (frame != null && !frame.isMain()) return;
String url = cefBrowser.getURL();
try {
String js =
"(function(){"
+ "if(window._axis_toolbar_injected) return; window._axis_toolbar_injected = true;"
+ "var style = document.createElement('style'); style.innerHTML = '"
+ ".axis-toolbar{position:fixed;right:16px;display:flex;gap:8px;z-index:2147483647;transition:transform .36s cubic-bezier(.2,.9,.2,1),opacity .28s;transform:translateY(20px) scale(.98);opacity:0;} "
+ ".axis-toolbar.show{transform:translateY(0) scale(1);opacity:1;} "
+ ".axis-btn{width:40px;height:40px;border-radius:10px;border:none;padding:6px;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(6px);background:rgba(255,255,255,0.08);box-shadow:0 6px 18px rgba(0,0,0,0.18);cursor:pointer;font-family:Segoe UI,SegoeUI,Arial,sans-serif;} "
+ ".axis-btn:hover{transform:translateY(-2px) scale(1.03);} "
+ ".axis-btn svg{width:20px;height:20px;display:block;} "
+ "@keyframes axisPageOut{0%{transform:scale(1);opacity:1}100%{transform:scale(0.96);opacity:0}}"
+ "@keyframes axisPageIn{0%{transform:scale(1.04);opacity:0}100%{transform:scale(1);opacity:1}}"
+ ".axis-page-out{animation:axisPageOut .3s ease-out forwards;pointer-events:none;}"
+ ".axis-page-in{animation:axisPageIn .4s ease-out forwards;}"
+ "';"
+ "document.head.appendChild(style);"
+ "var toolbar = document.createElement('div'); toolbar.className = 'axis-toolbar';"
+ "function makeBtn(title, svg, onclick){ var b = document.createElement('button'); b.className='axis-btn'; b.title=title; b.innerHTML = svg; b.addEventListener('click', function(e){ e.preventDefault(); try{ onclick(); }catch(ex){} }); return b; }"
+ "var svgBack = '<svg viewBox=\\'0 0 24 24\\' fill=\\'none\\' xmlns=\\'http://www.w3.org/2000/svg\\'><path d=\\'M15 6L9 12l6 6\\' stroke=\\'currentColor\\' stroke-width=\\'2\\' stroke-linecap=\\'round\\' stroke-linejoin=\\'round\\'/></svg>';"
+ "var svgFwd = '<svg viewBox=\\'0 0 24 24\\' fill=\\'none\\' xmlns=\\'http://www.w3.org/2000/svg\\'><path d=\\'M9 18l6-6-6-6\\' stroke=\\'currentColor\\' stroke-width=\\'2\\' stroke-linecap=\\'round\\' stroke-linejoin=\\'round\\'/></svg>';"
+ "var svgReload = '<svg viewBox=\\'0 0 24 24\\' fill=\\'none\\' xmlns=\\'http://www.w3.org/2000/svg\\'><path d=\\'M21 12a9 9 0 10-3 6.7\\' stroke=\\'currentColor\\' stroke-width=\\'2\\' stroke-linecap=\\'round\\' stroke-linejoin=\\'round\\'/><path d=\\'M21 3v6h-6\\' stroke=\\'currentColor\\' stroke-width=\\'2\\' stroke-linecap=\\'round\\' stroke-linejoin=\\'round\\'/></svg>';"
+ "var bBack = makeBtn('后退', svgBack, function(){ try{ history.back(); }catch(e){} });"
+ "var bFwd = makeBtn('前进', svgFwd, function(){ try{ history.forward(); }catch(e){} });"
+ "var bReload = makeBtn('重新加载', svgReload, function(){ try{ location.reload(); }catch(e){} });"
+ "toolbar.appendChild(bBack); toolbar.appendChild(bFwd); toolbar.appendChild(bReload); document.body.appendChild(toolbar);"
+ "setTimeout(function(){ try{ var footer = document.querySelector('footer'); var bottom = 24; if(footer && footer.offsetHeight) bottom = footer.offsetHeight + 16; toolbar.style.bottom = bottom + 'px'; }catch(e){} toolbar.classList.add('show'); }, 60);"
+ "var isAnimating = false;"
+ "document.addEventListener('click', function(e){"
+ " try{"
+ " if(isAnimating) return;"
+ " var a = e.target.closest && e.target.closest('a');"
+ " if(!a) return;"
+ " if(a.target=='_blank') return;"
+ " var href = a.href;"
+ " if(!href) return;"
+ " var same = (new URL(href, location.href)).origin === location.origin;"
+ " if(!same) return;"
+ " e.preventDefault();"
+ " isAnimating = true;"
+ " document.documentElement.classList.add('axis-page-out');"
+ " setTimeout(function(){"
+ " location.href = href;"
+ " isAnimating = false;"
+ " }, 300);"
+ " }catch(ex){}"
+ "}, true);"
+ "function runPageInAnimation() {"
+ " document.documentElement.classList.remove('axis-page-out');"
+ " document.documentElement.classList.add('axis-page-in');"
+ " setTimeout(function(){"
+ " document.documentElement.classList.remove('axis-page-in');"
+ " }, 400);"
+ "}"
+ "window.addEventListener('pageshow', function(event){"
+ " if (event.persisted) runPageInAnimation();"
+ "});"
+ "window.addEventListener('DOMContentLoaded', runPageInAnimation);"
+ "})();";
cefBrowser.executeJavaScript(js, url, 0);
} catch (Throwable ex) {
ex.printStackTrace();
}
}
});
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
@Override
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
String targetUrl, String targetFrameName) {
try {
Desktop.getDesktop().browse(new URI(targetUrl));
} catch (Exception e) {
System.out.println("Failed to open external browser: " + e.getMessage());
}
return true;
}
});
// 🔹 自定义右键菜单
client.addContextMenuHandler(new org.cef.handler.CefContextMenuHandlerAdapter() {
@Override
public void onBeforeContextMenu(org.cef.browser.CefBrowser browser, org.cef.browser.CefFrame frame,
org.cef.callback.CefContextMenuParams params, org.cef.callback.CefMenuModel model) {
model.clear(); // 清空默认菜单
if (browser.canGoBack()) {
model.addItem(MenuId.MENU_ID_BACK, "后退");
}
if (browser.canGoForward()) {
model.addItem(MenuId.MENU_ID_FORWARD, "前进");
}
// 仅有选中内容时显示“复制”
String selectionText = params.getSelectionText();
if (selectionText != null && !selectionText.trim().isEmpty()) {
model.addItem(MenuId.MENU_ID_COPY, "复制");
}
// 仅在可编辑区域显示“粘贴”
if (params.isEditable()) {
model.addItem(MenuId.MENU_ID_PASTE, "粘贴");
}
model.addItem(MenuId.MENU_ID_RELOAD, "刷新");
}
@Override
public boolean onContextMenuCommand(org.cef.browser.CefBrowser browser, org.cef.browser.CefFrame frame,
org.cef.callback.CefContextMenuParams params, int commandId, int eventFlags) {
return false; // 使用默认处理
}
});
} catch (Throwable e) {
logger.error("Failed to initialize CefBrowser", e);
openCasdoorLoginPageInDefaultBrowser();
String message = "浏览器初始化失败,已在默认浏览器打开登录页面,请手动完成登录。\n或者手动复制下面链接在浏览器打开进行登录\n"
+ casdoorServer.getSigninUrl();
JTextArea textArea = new JTextArea(message);
textArea.setEditable(false);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
textArea.setBackground(null);
textArea.setSize(504,835);
JOptionPane.showMessageDialog(dialog, new JScrollPane(textArea), "内嵌浏览器初始化失败", JOptionPane.WARNING_MESSAGE);
}
}
dialog = new JDialog();
dialog.setTitle("AXIS 认证");
dialog.setSize(504,835);
dialog.setLocationRelativeTo(null);
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
if (browser != null) {
panel.add(browser.getUIComponent());
}
dialog.add(panel);
dialog.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosed(java.awt.event.WindowEvent e) {
if (server != null) {
server.stop(0);
server = null;
}
}
});
}
private void openLoginAndListen() {
if (server == null) {
startLocalCallbackServer();
}
if (browser != null) {
browser.loadURL(casdoorServer.getSigninUrl());
}
}
private void openCasdoorLoginPageInDefaultBrowser() {
String loginUrl = casdoorServer.getSigninUrl();
try {
Desktop.getDesktop().browse(new URI(loginUrl));
} catch (Exception ex) {
JOptionPane.showMessageDialog(dialog, "无法打开浏览器: " + ex.getMessage());
}
}
private LoginResult parseUserInfo(String code, String state) {
if (code.isEmpty() || state.isEmpty()) {
return new LoginResult(false, "Login failed with error: Invalid code or state.", null);
}
try {
String token = casdoorServer.getOAuthToken(code, state);
User user = casdoorServer.parseJwtToken(token);
return new LoginResult(true, "Login successful.", new LoginData(token, user));
} catch (Exception ex) {
logger.error("解析登录信息失败: " + ex.getMessage());
return new LoginResult(false, "Login failed with error: " + ex.getMessage(), null);
}
}
private void resetResult() {
loginResult = null;
}
public LoginResult exec() {
resetResult();
initUI();
openLoginAndListen();
dialog.setModal(true);
isModal = true;
dialog.setVisible(windowVisible);
return getLoginResult();
}
public CompletableFuture<LoginResult> show() {
resetResult();
initUI();
CompletableFuture<LoginResult> future = new CompletableFuture<>();
dialog.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosed(java.awt.event.WindowEvent e) {
future.complete(getLoginResult());
}
});
openLoginAndListen();
dialog.setModal(false);
isModal = false;
dialog.setVisible(true);
return future;
}
public boolean isModal() {
return isModal;
}
public void setVisible(boolean b) {
this.windowVisible = b;
dialog.setVisible(b);
}
public LoginResult getLoginResult() {
return loginResult;
}
public static LoginResult showLoginDialogAndGetLoginResult() throws InterruptedException, InvocationTargetException {
AtomicReference<LoginResult> result = new AtomicReference<>();
SwingUtilities.invokeAndWait(() -> {
CasdoorLoginWindow window = new CasdoorLoginWindow();
result.set(window.exec());
});
return result.get();
}
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
Result result = showLoginDialogAndGetLoginResult();
System.out.println("Login result: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -1,539 +0,0 @@
package com.axis.innovators.box.window;
import com.axis.innovators.box.verification.OnlineVerification;
import com.axis.innovators.box.verification.UserTags;
import com.axis.innovators.box.verification.VerificationService;
import com.formdev.flatlaf.FlatDarculaLaf;
import javax.swing.*;
import javax.swing.border.LineBorder;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.atomic.AtomicReference;
/**
* 重构后的现代化单窗口登录/注册/找回密码界面(带平滑切换动画)
* 保留原有验证逻辑接口调用OnlineVerification / VerificationService
*
* 说明:
* - 单窗口JDialog内使用滑动动画切换视图仿微软登录体验
* - 所有子界面(登录/注册/找回密码)都在同一容器中切换,不再弹新窗口。
* - 按钮与输入框固定宽度,避免被挤压变形。
*
* 注意:需要 flatlaf 依赖以呈现更现代的外观。
*/
public class LoginWindow {
private static final AtomicReference<UserTags> loginResult = new AtomicReference<>(UserTags.None);
private final JDialog dialog;
private final JLayeredPane layeredPane;
private final int DIALOG_WIDTH = 460;
private final int DIALOG_HEIGHT = 560;
// 登录面板中的控件需要在类域以便访问
private JTextField loginEmailField;
private JPasswordField loginPasswordField;
public static UserTags createAndShow() throws InterruptedException, InvocationTargetException {
AtomicReference<UserTags> result = new AtomicReference<>(UserTags.None);
SwingUtilities.invokeAndWait(() -> {
LoginWindow window = new LoginWindow();
window.dialog.setVisible(true);
result.set(loginResult.get());
if (result.get() == UserTags.None) {
System.exit(0);
}
});
return result.get();
}
public LoginWindow() {
setupLookAndFeel();
dialog = new JDialog((Frame) null, "AXIS 安全认证", true);
dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
dialog.setSize(DIALOG_WIDTH, DIALOG_HEIGHT);
dialog.setResizable(false);
dialog.setLocationRelativeTo(null);
// 根容器:深色背景并居中卡片
JPanel root = new JPanel(new GridBagLayout());
root.setBackground(new Color(0x202225));
root.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
dialog.setContentPane(root);
// 卡片容器(居中)
JPanel cardWrapper = new JPanel(null) {
@Override
public Dimension getPreferredSize() {
return new Dimension(DIALOG_WIDTH - 40, DIALOG_HEIGHT - 40);
}
};
cardWrapper.setOpaque(false);
cardWrapper.setPreferredSize(new Dimension(DIALOG_WIDTH - 40, DIALOG_HEIGHT - 40));
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
root.add(cardWrapper, gbc);
// 分层面板用于动画
layeredPane = new JLayeredPane();
layeredPane.setBounds(0, 0, DIALOG_WIDTH - 40, DIALOG_HEIGHT - 40);
cardWrapper.add(layeredPane);
// 卡片背景(圆角)
JPanel backgroundCard = new JPanel();
backgroundCard.setBackground(new Color(0x2A2E31));
backgroundCard.setBorder(new RoundBorder(16, new Color(0x3A3F42)));
backgroundCard.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight());
backgroundCard.setLayout(null);
layeredPane.add(backgroundCard, Integer.valueOf(0));
// 创建三个面板宽度等于容器宽度初始将登录面板放置在0位置
JPanel loginPanel = buildLoginPanel();
JPanel registerPanel = buildRegisterPanel();
JPanel forgotPanel = buildForgotPanel();
int w = layeredPane.getWidth();
int h = layeredPane.getHeight();
loginPanel.setBounds(0, 0, w, h);
registerPanel.setBounds(w, 0, w, h); // 右侧预置
forgotPanel.setBounds(w * 2, 0, w, h);
layeredPane.add(loginPanel, Integer.valueOf(1));
layeredPane.add(registerPanel, Integer.valueOf(1));
layeredPane.add(forgotPanel, Integer.valueOf(1));
dialog.pack();
// ensure layered sizes match after pack
SwingUtilities.invokeLater(() -> {
layeredPane.setBounds(0, 0, cardWrapper.getWidth(), cardWrapper.getHeight());
backgroundCard.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight());
int nw = layeredPane.getWidth(), nh = layeredPane.getHeight();
loginPanel.setBounds(0, 0, nw, nh);
registerPanel.setBounds(nw, 0, nw, nh);
forgotPanel.setBounds(nw * 2, 0, nw, nh);
});
}
// 切换动画direction = 1 表示向左滑动进入下一页(当前 -> 右侧),-1 表示向右滑动进入上一页
private void slideTo(int targetIndex) {
Component[] comps = layeredPane.getComponents();
// 每个面板宽度
int w = layeredPane.getWidth();
// 当前最左边的x位置找到最左的那个主要面板
// 我们采用简单方式:目标面板应该位于 x = targetIndex * w 0,1,2
int targetX = -targetIndex * w; // 我们会将所有面板整体平移,使目标显示在 x=0
// 获取当前 offset (使用第一个面板的 x 来代表整体偏移)
int startOffset = 0;
// find current offset by checking bounds of first panel (assume index 0 is login)
if (comps.length > 0) {
startOffset = comps[0].getX();
}
int start = startOffset;
int end = targetX;
int duration = 300; // ms
int fps = 60;
int delay = 1000 / fps;
int steps = Math.max(1, duration / delay);
final int[] step = {0};
Timer timer = new Timer(delay, null);
timer.addActionListener((ActionEvent e) -> {
step[0]++;
double t = (double) step[0] / steps;
// ease in-out cubic
double tt = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
int cur = (int) Math.round(start + (end - start) * tt);
// 平移所有在 layeredPane 中(除背景)组件
for (Component c : layeredPane.getComponents()) {
if (c instanceof JPanel && c.isVisible()) {
// 计算原始索引根据名字
String name = c.getName();
// 我们之前把panel放在 x = idx * w ; 现在把它设置为 idx*w + cur
int idx = 0;
if ("login".equals(name)) idx = 0;
else if ("register".equals(name)) idx = 1;
else if ("forgot".equals(name)) idx = 2;
c.setLocation(idx * w + cur, 0);
}
}
layeredPane.repaint();
if (step[0] >= steps) {
timer.stop();
}
});
timer.setInitialDelay(0);
timer.start();
}
private JPanel buildLoginPanel() {
JPanel p = new JPanel(null);
p.setOpaque(false);
p.setName("login");
int w = DIALOG_WIDTH - 40;
int h = DIALOG_HEIGHT - 40;
// 标题区
JLabel appTitle = new JLabel("AXIS");
appTitle.setFont(new Font("微软雅黑", Font.BOLD, 28));
appTitle.setForeground(Color.WHITE);
appTitle.setBounds(28, 20, w - 56, 36);
JLabel subtitle = new JLabel("安全认证 — 请登录以继续");
subtitle.setFont(new Font("微软雅黑", Font.PLAIN, 12));
subtitle.setForeground(new Color(0xA7AEB5));
subtitle.setBounds(28, 56, w - 56, 18);
p.add(appTitle);
p.add(subtitle);
// 卡片内控件起始 y
int startY = 96;
int fieldW = Math.min(360, w - 56);
int fieldX = (w - fieldW) / 2;
// Email
JLabel emailLabel = new JLabel("账号");
emailLabel.setForeground(new Color(0x99A0A7));
emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
emailLabel.setBounds(fieldX, startY, fieldW, 18);
loginEmailField = new JTextField();
styleInput(loginEmailField);
loginEmailField.setBounds(fieldX, startY + 22, fieldW, 44);
loginEmailField.putClientProperty("JTextField.placeholderText", "邮箱或手机号");
// Password
JLabel pwdLabel = new JLabel("密码");
pwdLabel.setForeground(new Color(0x99A0A7));
pwdLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
pwdLabel.setBounds(fieldX, startY + 22 + 44 + 12, fieldW, 18);
loginPasswordField = new JPasswordField();
styleInput(loginPasswordField);
loginPasswordField.setBounds(fieldX, startY + 22 + 44 + 12 + 20, fieldW - 48, 44);
loginPasswordField.putClientProperty("JTextField.placeholderText", "请输入密码");
// eye toggle
JToggleButton eyeBtn = new JToggleButton("显示");
eyeBtn.setFont(new Font("微软雅黑", Font.PLAIN, 12));
eyeBtn.setFocusable(false);
eyeBtn.setBorderPainted(false);
eyeBtn.setContentAreaFilled(true);
eyeBtn.setBackground(new Color(0x39424A));
eyeBtn.setForeground(Color.WHITE);
eyeBtn.setBounds(fieldX + fieldW - 44, startY + 22 + 44 + 12 + 20, 44, 44);
eyeBtn.addActionListener(e -> {
if (eyeBtn.isSelected()) loginPasswordField.setEchoChar((char) 0);
else loginPasswordField.setEchoChar('•');
});
// 登录按钮(占满宽度)
JButton loginBtn = new JButton("立即登录");
stylePrimaryButton(loginBtn);
loginBtn.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 22, fieldW, 48);
loginBtn.addActionListener(e -> doLogin());
// 链接区域(注册 / 忘记密码) — 点击切换到对应面板
JButton toRegister = createTextLink("注册账号");
toRegister.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 22 + 60, 120, 20);
toRegister.addActionListener(e -> slideTo(1));
JButton toForgot = createTextLink("忘记密码");
toForgot.setBounds(fieldX + fieldW - 120, startY + 22 + 44 + 12 + 20 + 44 + 22 + 60, 120, 20);
toForgot.addActionListener(e -> slideTo(2));
// footer
JLabel footer = new JLabel("使用你的 AXIS 账户进行登录。");
footer.setForeground(new Color(0x8F969C));
footer.setFont(new Font("微软雅黑", Font.PLAIN, 11));
footer.setBounds(fieldX, h - 44, fieldW, 18);
p.add(emailLabel);
p.add(loginEmailField);
p.add(pwdLabel);
p.add(loginPasswordField);
p.add(eyeBtn);
p.add(loginBtn);
p.add(toRegister);
p.add(toForgot);
p.add(footer);
return p;
}
private JPanel buildRegisterPanel() {
JPanel p = new JPanel(null);
p.setOpaque(false);
p.setName("register");
int w = DIALOG_WIDTH - 40;
int h = DIALOG_HEIGHT - 40;
JLabel title = new JLabel("创建账号");
title.setFont(new Font("微软雅黑", Font.BOLD, 24));
title.setForeground(Color.WHITE);
title.setBounds(28, 20, w - 56, 36);
JLabel desc = new JLabel("快速创建你的 AXIS 帐号");
desc.setFont(new Font("微软雅黑", Font.PLAIN, 12));
desc.setForeground(new Color(0xA7AEB5));
desc.setBounds(28, 56, w - 56, 18);
p.add(title);
p.add(desc);
int startY = 96;
int fieldW = Math.min(360, w - 56);
int fieldX = (w - fieldW) / 2;
// 用户名
JLabel nameLabel = new JLabel("用户名");
nameLabel.setForeground(new Color(0x99A0A7));
nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
nameLabel.setBounds(fieldX, startY, fieldW, 18);
JTextField nameField = new JTextField();
styleInput(nameField);
nameField.setBounds(fieldX, startY + 22, fieldW, 44);
// 邮箱
JLabel emailLabel = new JLabel("邮箱");
emailLabel.setForeground(new Color(0x99A0A7));
emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
emailLabel.setBounds(fieldX, startY + 22 + 44 + 12, fieldW, 18);
JTextField emailField = new JTextField();
styleInput(emailField);
emailField.setBounds(fieldX, startY + 22 + 44 + 12 + 20, fieldW, 44);
// 密码
JLabel pwdLabel = new JLabel("密码");
pwdLabel.setForeground(new Color(0x99A0A7));
pwdLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
pwdLabel.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12, fieldW, 18);
JPasswordField pwdField = new JPasswordField();
styleInput(pwdField);
pwdField.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12 + 20, fieldW, 44);
// 确认密码
JLabel confirmLabel = new JLabel("确认密码");
confirmLabel.setForeground(new Color(0x99A0A7));
confirmLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
confirmLabel.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12 + 20 + 44 + 12, fieldW, 18);
JPasswordField confirmField = new JPasswordField();
styleInput(confirmField);
confirmField.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12 + 20 + 44 + 12 + 20, fieldW, 44);
// 注册按钮
JButton regBtn = new JButton("创建账号");
stylePrimaryButton(regBtn);
regBtn.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12 + 20 + 44 + 12 + 20 + 44 + 18, fieldW, 48);
regBtn.addActionListener(e -> {
String name = nameField.getText().trim();
String email = emailField.getText().trim();
String pwd = new String(pwdField.getPassword());
String confirm = new String(confirmField.getPassword());
if (name.isEmpty() || email.isEmpty() || pwd.isEmpty() || confirm.isEmpty()) {
JOptionPane.showMessageDialog(dialog, "请完整填写注册信息", "注册错误", JOptionPane.ERROR_MESSAGE);
return;
}
if (!pwd.equals(confirm)) {
JOptionPane.showMessageDialog(dialog, "两次输入的密码不一致", "注册错误", JOptionPane.ERROR_MESSAGE);
return;
}
boolean success = VerificationService.registerUser(name, email, pwd);
if (success) {
JOptionPane.showMessageDialog(dialog, "注册成功,请登录", "注册成功", JOptionPane.INFORMATION_MESSAGE);
slideTo(0); // 回到登录页面
} else {
JOptionPane.showMessageDialog(dialog, "注册失败,请检查信息", "注册错误", JOptionPane.ERROR_MESSAGE);
}
});
// 返回登录链接
JButton back = createTextLink("返回登录");
back.setBounds(fieldX, regBtn.getY() + regBtn.getHeight() + 12, 120, 20);
back.addActionListener(e -> slideTo(0));
p.add(nameLabel);
p.add(nameField);
p.add(emailLabel);
p.add(emailField);
p.add(pwdLabel);
p.add(pwdField);
p.add(confirmLabel);
p.add(confirmField);
p.add(regBtn);
p.add(back);
return p;
}
private JPanel buildForgotPanel() {
JPanel p = new JPanel(null);
p.setOpaque(false);
p.setName("forgot");
int w = DIALOG_WIDTH - 40;
int h = DIALOG_HEIGHT - 40;
JLabel title = new JLabel("找回密码");
title.setFont(new Font("微软雅黑", Font.BOLD, 24));
title.setForeground(Color.WHITE);
title.setBounds(28, 20, w - 56, 36);
JLabel desc = new JLabel("通过注册邮箱重置密码");
desc.setFont(new Font("微软雅黑", Font.PLAIN, 12));
desc.setForeground(new Color(0xA7AEB5));
desc.setBounds(28, 56, w - 56, 18);
p.add(title);
p.add(desc);
int startY = 110;
int fieldW = Math.min(360, w - 56);
int fieldX = (w - fieldW) / 2;
JLabel emailLabel = new JLabel("注册邮箱");
emailLabel.setForeground(new Color(0x99A0A7));
emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
emailLabel.setBounds(fieldX, startY, fieldW, 18);
JTextField emailField = new JTextField();
styleInput(emailField);
emailField.setBounds(fieldX, startY + 22, fieldW, 44);
JButton sendBtn = new JButton("发送重置邮件");
stylePrimaryButton(sendBtn);
sendBtn.setBounds(fieldX, startY + 22 + 44 + 22, fieldW, 48);
sendBtn.addActionListener(e -> {
String email = emailField.getText().trim();
if (email.isEmpty()) {
JOptionPane.showMessageDialog(dialog, "请输入注册邮箱", "错误", JOptionPane.ERROR_MESSAGE);
return;
}
if (VerificationService.sendPasswordReset(email)) {
JOptionPane.showMessageDialog(dialog, "重置邮件已发送,请查收", "成功", JOptionPane.INFORMATION_MESSAGE);
slideTo(0);
} else {
JOptionPane.showMessageDialog(dialog, "发送失败或邮箱未注册", "失败", JOptionPane.ERROR_MESSAGE);
}
});
JButton back = createTextLink("返回登录");
back.setBounds(fieldX, sendBtn.getY() + sendBtn.getHeight() + 12, 120, 20);
back.addActionListener(e -> slideTo(0));
p.add(emailLabel);
p.add(emailField);
p.add(sendBtn);
p.add(back);
return p;
}
private void doLogin() {
String email = loginEmailField.getText().trim();
String password = new String(loginPasswordField.getPassword()).trim();
if (email.isEmpty() || password.isEmpty()) {
JOptionPane.showMessageDialog(dialog, "请输入完整的登录信息", "验证错误", JOptionPane.ERROR_MESSAGE);
return;
}
OnlineVerification onlineVerification = OnlineVerification.validateLogin(email, password);
if (onlineVerification == null) {
String err = OnlineVerification.errorMessage != null && !OnlineVerification.errorMessage.trim().isEmpty()
? OnlineVerification.errorMessage
: "验证失败,请重试";
JOptionPane.showMessageDialog(dialog, err, "验证错误", JOptionPane.ERROR_MESSAGE);
return;
}
loginResult.set(VerificationService.determineUserType(onlineVerification));
dialog.dispose();
}
// 通用输入框样式
private void styleInput(JComponent comp) {
comp.setFont(new Font("微软雅黑", Font.PLAIN, 14));
comp.setBackground(new Color(0x2F3336));
comp.setForeground(new Color(0xE8ECEF));
comp.setBorder(BorderFactory.createCompoundBorder(
new RoundBorder(8, new Color(0x3A3F42)),
BorderFactory.createEmptyBorder(10, 12, 10, 12)
));
if (comp instanceof JTextComponent) ((JTextComponent) comp).setCaretColor(new Color(0x9AA0A6));
}
// 主要操作按钮样式(主色)
private void stylePrimaryButton(AbstractButton b) {
b.setFont(new Font("微软雅黑", Font.BOLD, 14));
b.setForeground(Color.WHITE);
b.setBackground(new Color(0x2B79D0));
b.setBorderPainted(false);
b.setFocusPainted(false);
b.setOpaque(true);
b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
}
private JButton createTextLink(String text) {
JButton btn = new JButton(text);
btn.setFont(new Font("微软雅黑", Font.PLAIN, 12));
btn.setForeground(new Color(0x79A6FF));
btn.setBorderPainted(false);
btn.setContentAreaFilled(false);
btn.setFocusPainted(false);
btn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
return btn;
}
private void setupLookAndFeel() {
try {
UIManager.setLookAndFeel(new FlatDarculaLaf());
UIManager.put("Component.arc", 12);
UIManager.put("TextComponent.arc", 12);
UIManager.put("Button.arc", 10);
UIManager.put("Panel.background", new Color(0x202225));
UIManager.put("TextComponent.background", new Color(0x2F3336));
UIManager.put("TextComponent.foreground", new Color(0xE8ECEF));
} catch (UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
}
// 圆角边框
private static class RoundBorder extends LineBorder {
private final int radius;
public RoundBorder(int radius, Color color) {
super(color, 1);
this.radius = radius;
}
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(lineColor);
g2.drawRoundRect(x, y, width - 1, height - 1, radius, radius);
g2.dispose();
}
}
}

View File

@@ -14,14 +14,18 @@ import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.plaf.FontUIResource;
import javax.swing.plaf.LayerUI;
import javax.swing.plaf.PanelUI;
import javax.swing.plaf.basic.BasicScrollBarUI;
import javax.swing.plaf.basic.BasicTabbedPaneUI;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@@ -34,6 +38,8 @@ import java.util.concurrent.ConcurrentHashMap;
* - 侧栏颜色使用 #3C3F41按你的要求
* - 搜索框以居中为主,聚焦时带放大动画与圆角外观
* - 尽量避免离屏绘制遗留(使用 component.printAll() 截图)
* - 添加背景图片和玻璃模糊效果支持
* @author tzdwindows 7
*/
public class MainWindow extends JFrame {
private static final Logger logger = LogManager.getLogger(MainWindow.class);
@@ -56,27 +62,18 @@ public class MainWindow extends JFrame {
private JPanel sideBar;
private JPanel contentPanel;
private RoundedSearchField searchField;
private JLabel status;
// 背景图片相关字段
private Image backgroundImage;
private float blurAmount = 0.0f;
private float backgroundOpacity = 1.0f;
private BufferedImage cachedBlurredBackground;
private Dimension cachedBackgroundSize;
// settings dialog
private WindowsJDialog dialog;
// 侧边栏颜色(比面板背景稍暗)
private static Color SIDEBAR_COLOR = Optional.ofNullable(UIManager.getColor("Panel.background"))
.orElse(new Color(0x3C3F41));
// 卡片背景(深色模式适配,比侧边栏稍亮)
private static Color CARD_BG = Optional.ofNullable(UIManager.getColor("control"))
.map(bg -> ThemeColors.brighten(bg, 0.1f))
.orElse(new Color(0x4A4D50));
// 卡片边框(使用系统边框色或稍亮颜色)
private static Color CARD_BORDER = Optional.ofNullable(UIManager.getColor("controlHighlight"))
.orElse(new Color(0x5C5F61));
// 文本颜色(直接使用系统主题的文本颜色)
private static Color TEXT_COLOR = Optional.ofNullable(UIManager.getColor("textText"))
.orElse(new Color(0xE0E0E0));
public MainWindow() {
// 增强字体设置逻辑:优先使用系统支持的中文字体
String[] fontNames = {"Microsoft YaHei", "微软雅黑", "PingFang SC", "SimHei", "宋体", "新宋体", "SansSerif"};
@@ -123,13 +120,137 @@ public class MainWindow extends JFrame {
if (layeredPane != null && cardsPanel != null) {
cardsPanel.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight());
}
// 窗口大小改变时重新生成模糊背景
if (backgroundImage != null) {
cachedBlurredBackground = null;
repaint();
}
}
});
//setLocationRelativeTo(null);
addComponentListener(new ComponentAdapter() {
@Override
public void componentShown(ComponentEvent e) {
// 确保只执行一次
removeComponentListener(this);
setLocationRelativeTo(null);
}
});
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(1200, 800);
setSize(1060, 670);
}
/**
* 设置背景图片和玻璃模糊效果
* @param backgroundImage 背景图片
* @param blurAmount 模糊程度 (0.0f - 1.0f)0为不模糊1为最大模糊
* @param opacity 透明度 (0.0f - 1.0f)0为完全透明1为完全不透明
*/
public void setBackgroundWithGlassEffect(Image backgroundImage, float blurAmount, float opacity) {
this.backgroundImage = backgroundImage;
this.blurAmount = Math.max(0.0f, Math.min(1.0f, blurAmount));
this.backgroundOpacity = Math.max(0.0f, Math.min(1.0f, opacity));
this.cachedBlurredBackground = null;
this.cachedBackgroundSize = null;
AxisInnovatorsBox.getMain().reloadAllWindow();
}
/**
* 移除背景图片
*/
public void removeBackground() {
this.backgroundImage = null;
this.cachedBlurredBackground = null;
this.cachedBackgroundSize = null;
// 这是段重复的石山代码我不想改了,作用用py想都知道是更新窗口渲染
// 别问我为什么不用AxisInnovatorsBox.getMain().reloadAllWindow();
// 因为AxisInnovatorsBox.getMain().reloadAllWindow();会广播到所有窗口
getContentPane().removeAll();
repaint();
initUI();
updateTheme();
revalidate();
}
/**
* 应用高斯模糊到图片
*/
private BufferedImage applyGaussianBlur(BufferedImage image, float blurFactor) {
if (blurFactor <= 0.0f) return image;
// 根据模糊因子计算模糊半径 (1-15像素)
int radius = Math.max(1, (int)(blurFactor * 15));
// 确保半径为奇数
if (radius % 2 == 0) radius++;
int size = radius * 2 + 1;
float[] data = new float[size * size];
// 计算高斯核
float sigma = radius / 3.0f;
float twoSigmaSquare = 2.0f * sigma * sigma;
float sigmaRoot = (float) Math.sqrt(twoSigmaSquare * Math.PI);
float total = 0.0f;
for (int i = -radius; i <= radius; i++) {
for (int j = -radius; j <= radius; j++) {
float distance = i * i + j * j;
data[(i + radius) * size + (j + radius)] =
(float) Math.exp(-distance / twoSigmaSquare) / sigmaRoot;
total += data[(i + radius) * size + (j + radius)];
}
}
// 归一化
for (int i = 0; i < data.length; i++) {
data[i] /= total;
}
Kernel kernel = new Kernel(size, size, data);
ConvolveOp convolve = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
return convolve.filter(image, null);
}
/**
* 创建带模糊效果的背景图片
*/
private BufferedImage createBlurredBackground(Dimension size) {
if (backgroundImage == null) return null;
// 如果尺寸相同且已有缓存,直接返回缓存
if (cachedBlurredBackground != null && cachedBackgroundSize != null &&
cachedBackgroundSize.equals(size)) {
return cachedBlurredBackground;
}
// 创建背景图片
BufferedImage bgImage = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = bgImage.createGraphics();
// 设置渲染质量
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
// 绘制原始背景图片(缩放以适应窗口)
g2d.drawImage(backgroundImage, 0, 0, size.width, size.height, null);
g2d.dispose();
// 应用模糊效果
if (blurAmount > 0.0f) {
bgImage = applyGaussianBlur(bgImage, blurAmount);
}
// 缓存结果
cachedBlurredBackground = bgImage;
cachedBackgroundSize = new Dimension(size);
return bgImage;
}
/**
@@ -158,13 +279,56 @@ public class MainWindow extends JFrame {
setTitle(LanguageManager.getLoadedLanguages().getText("mainWindow.title"));
// 主容器
JPanel mainPanel = new JPanel(new BorderLayout());
// 主容器 - 使用自定义面板以支持背景绘制
JPanel mainPanel = new JPanel(new BorderLayout()) {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// 如果有背景图片,绘制背景
if (backgroundImage != null) {
Graphics2D g2d = (Graphics2D) g.create();
// 设置透明度
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, backgroundOpacity));
// 获取带模糊效果的背景
Dimension size = getSize();
BufferedImage bg = createBlurredBackground(size);
if (bg != null) {
g2d.drawImage(bg, 0, 0, null);
}
g2d.dispose();
}
}
};
mainPanel.setBorder(BorderFactory.createEmptyBorder());
mainPanel.setOpaque(true);
// 设置背景色(如果有背景图片,则使用半透明背景)
if (backgroundImage != null) {
// 当有背景图片时,使用半透明的背景色
Color panelBg = UIManager.getColor("Panel.background");
if (panelBg != null) {
// 降低背景色的不透明度,让背景图片透出来
Color semiTransparentBg = new Color(
panelBg.getRed(),
panelBg.getGreen(),
panelBg.getBlue(),
(int)(200 * backgroundOpacity) // 调整透明度
);
mainPanel.setBackground(semiTransparentBg);
}
} else {
// 没有背景图片时使用正常背景色
Color panelBg = UIManager.getColor("Panel.background");
if (panelBg == null) panelBg = new Color(245, 246, 248);
mainPanel.setBackground(panelBg);
}
mainPanel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
mainPanel.add(createHeader(), BorderLayout.NORTH);
@@ -288,7 +452,19 @@ public class MainWindow extends JFrame {
JComponent contentPane = (JComponent) content;
Color panelBg = UIManager.getColor("Panel.background");
if (panelBg == null) panelBg = new Color(245, 246, 248);
// 如果有背景图片,使用半透明背景
if (backgroundImage != null) {
Color semiTransparentBg = new Color(
panelBg.getRed(),
panelBg.getGreen(),
panelBg.getBlue(),
(int)(200 * backgroundOpacity)
);
contentPane.setBackground(semiTransparentBg);
} else {
contentPane.setBackground(panelBg);
}
contentPane.repaint();
}
@@ -309,7 +485,12 @@ public class MainWindow extends JFrame {
}
if (sideBar != null) {
if (backgroundImage != null) {
sideBar.setOpaque(false);
} else {
sideBar.setOpaque(true);
sideBar.setBackground(getSidebarColor());
}
sideBar.repaint();
}
@@ -539,7 +720,7 @@ public class MainWindow extends JFrame {
private JPanel createSideBar() {
JPanel sidebar = new JPanel(new BorderLayout());
sidebar.setOpaque(true);
sidebar.setBackground(SIDEBAR_COLOR);
//sidebar.setBackground(SIDEBAR_COLOR);
sidebar.setPreferredSize(new Dimension(220, getHeight()));
sidebar.setBorder(null);
@@ -574,7 +755,7 @@ public class MainWindow extends JFrame {
JScrollPane listScroll = new JScrollPane(listPanel);
listScroll.setBorder(null);
listScroll.setOpaque(false);
listScroll.getViewport().setOpaque(false);
listScroll.getViewport().setOpaque(false); // 确保视口透明
listScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
listScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
listScroll.getVerticalScrollBar().setUI(new CustomScrollBarUI());
@@ -680,7 +861,8 @@ public class MainWindow extends JFrame {
));
// 样式
button.setForeground(TEXT_COLOR);
button.setForeground(Optional.ofNullable(UIManager.getColor("textText"))
.orElse(new Color(0xE0E0E0)));
// 初始设为不填充,由 updateSelection 决定是否填充背景
button.setOpaque(false);
button.setContentAreaFilled(true); // 允许根据 opaque/background 填充mouse/selected 状态会切换 opaque
@@ -695,7 +877,14 @@ public class MainWindow extends JFrame {
boolean selected = Objects.equals(currentCategoryId, category.getId().toString());
if (selected) {
button.setOpaque(true);
/**
* Java我草泥马你马死了
*/
if (backgroundImage == null) {
button.setBackground(SELECT_FILL);
} else {
button.setBackground(new Color(0, 120, 215, 255));
}
} else {
button.setOpaque(false);
// 为避免残留,仍设置透明背景(透明颜色)
@@ -749,7 +938,15 @@ public class MainWindow extends JFrame {
// ---------- 工具卡/面板 ----------
private JPanel createToolsPanel(ToolCategory category) {
JPanel panel = new JPanel(new WrapLayout(FlowLayout.LEFT, 16, 16));
JPanel panel = new JPanel(new WrapLayout(FlowLayout.LEFT, 16, 16)) {
@Override
public Dimension getPreferredSize() {
// 计算容器宽度以恰好容纳3个卡片和间隙
int cardWidth = 240; // 卡片宽度
int gap = 16;
return new Dimension(cardWidth * 3 + gap * 2, super.getPreferredSize().height);
}
};
panel.setOpaque(false);
panel.setBorder(null);
@@ -764,6 +961,36 @@ public class MainWindow extends JFrame {
return wrapper;
}
/**
* 判断设置界面是否打开
* @return 设置界面是否可见
*/
public boolean isSettingsVisible(){
if (dialog == null) {
return false;
}
if (!dialog.isDisplayable()) {
dialog = null;
return false;
}
if (!dialog.isVisible()) {
return false;
}
try {
if (dialog.getParent() != this && dialog.getOwner() != this) {
dialog = null;
return false;
}
} catch (Exception e) {
dialog = null;
return false;
}
return true;
}
private JPanel createToolCard(ToolItem tool) {
JPanel card = new JPanel() {
@Override
@@ -778,6 +1005,14 @@ public class MainWindow extends JFrame {
Color cardBorder = getCardBorder();
Color shadowColor = isDarkTheme() ? new Color(30, 30, 30) : Color.BLACK;
// 如果有背景图片,卡片背景使用半透明
if (backgroundImage != null) {
cardBg = new Color(cardBg.getRed(), cardBg.getGreen(), cardBg.getBlue(),
(int)(200 * backgroundOpacity));
cardBorder = new Color(cardBorder.getRed(), cardBorder.getGreen(),
cardBorder.getBlue(), (int)(200 * backgroundOpacity));
}
// 1. 绘制阴影(根据主题调整透明度)
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, isDarkTheme() ? 0.12f : 0.06f));
g2.setColor(shadowColor);
@@ -826,8 +1061,10 @@ public class MainWindow extends JFrame {
titleLabel.setForeground(UIManager.getColor("Label.foreground"));
JTextArea descArea = new JTextArea(tool.getDescription());
descArea.setFont(new Font(selectFont("Segoe UI", "Microsoft YaHei", "SansSerif", 13).getName(), Font.PLAIN, 13));
titleLabel.setForeground(TEXT_COLOR);
descArea.setForeground(TEXT_COLOR.darker());
titleLabel.setForeground(Optional.ofNullable(UIManager.getColor("textText"))
.orElse(new Color(0xE0E0E0)));
descArea.setForeground(Optional.ofNullable(UIManager.getColor("textText"))
.orElse(new Color(0xE0E0E0)).darker());
descArea.setForeground(new Color(100,100,105));
descArea.setLineWrap(true);
descArea.setWrapStyleWord(true);
@@ -925,6 +1162,7 @@ public class MainWindow extends JFrame {
card.repaint();
if (currentElevation == targetElevation && Math.abs(cardScales.get(card) - targetScale) < 0.002f) ((Timer)e.getSource()).stop();
}
}).start();
}
@@ -1035,13 +1273,21 @@ public class MainWindow extends JFrame {
JPanel footer = new JPanel(new BorderLayout());
footer.setOpaque(false);
footer.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
JLabel status = new JLabel("Ready");
status = new JLabel("Ready");
status.setFont(new Font(selectFont("Segoe UI", "Microsoft YaHei", "SansSerif", 12).getName(), Font.PLAIN, 12));
status.setForeground(UIManager.getColor("Label.foreground"));
footer.add(status, BorderLayout.WEST);
return footer;
}
/**
* 更新状态
* @param text 状态名称
*/
public void updataStatus(String text) {
status.setText(text);
}
private void updateSideSelection(String categoryId) {
for (Map.Entry<String, JButton> e : sideButtons.entrySet()) {
String id = e.getKey();
@@ -1138,36 +1384,126 @@ public class MainWindow extends JFrame {
}
}
// ---------- showSettings 恢复实现 ----------
// ---------- showSettings 实现 ----------
public void showSettings() {
if (dialog == null || AxisInnovatorsBox.getMain().isWindowStartup(dialog)) {
dialog = new WindowsJDialog(this,
LanguageManager.getLoadedLanguages().getText("mainWindow.settings.title"), true);
}
dialog.setTitle(LanguageManager.getLoadedLanguages().getText("mainWindow.settings.title"));
dialog.setSize(680, 480);
dialog.setSize(750, 550);
dialog.setLocationRelativeTo(this);
JPanel content = new JPanel(new BorderLayout());
content.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
content.setOpaque(false);
// 使用 JLayer + LayerUI 来对整个内容做统一的淡入 + 下滑(仿 Apple 风格)动画,
// 这样子组件也会跟随一起动画,而不是只有背景绘制发生变化。
JPanel inner = new JPanel(new BorderLayout());
inner.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
inner.setOpaque(false); // 背景在 LayerUI 中绘制以便做阴影/圆角
JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.LEFT);
tabbedPane.setOpaque(false);
tabbedPane.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
JTabbedPane tabbedPane = new JTabbedPane();
List<RegistrationSettingsItem> registrationSettingsItemList
= RegistrationSettingsItem.getRegistrationSettingsItemList();
for (RegistrationSettingsItem registrationSettingsItem : registrationSettingsItemList) {
registrationSettingsItem.registration(tabbedPane);
}
content.add(tabbedPane, BorderLayout.CENTER);
GlobalEventBus.EVENT_BUS.post(new SettingsLoadEvents(dialog, content));
dialog.add(content);
inner.add(tabbedPane, BorderLayout.CENTER);
GlobalEventBus.EVENT_BUS.post(new SettingsLoadEvents(dialog, inner));
// LayerUI 实现:负责绘制圆角背景、阴影,并在 paint 中使用 alpha + translate 来实现动画
class FadeLayerUI extends LayerUI<JComponent> {
private float alpha = 0f;
private int translateY = 20;
public void setAlpha(float alpha) { this.alpha = Math.max(0f, Math.min(1f, alpha)); }
public void setTranslateY(int y) { this.translateY = y; }
@Override
public void paint (Graphics g, JComponent c) {
Graphics2D g2 = (Graphics2D) g.create();
int w = c.getWidth();
int h = c.getHeight();
// 平滑处理
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 先绘制柔和阴影(透明度与 alpha 关联)
int arc = 16;
float shadowAlpha = Math.min(0.35f, alpha * 0.35f);
if (shadowAlpha > 0f) {
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, shadowAlpha));
g2.setColor(new Color(0, 0, 0, 255));
// 画一个稍微向下偏移的矩形作为阴影基础
g2.fillRoundRect(8, 10 + translateY/3, w - 16, h - 20, arc, arc);
}
// 应用下滑位移 + 透明度到后续的内容绘制(包括子组件)
g2.translate(0, translateY);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
Color panelBg = UIManager.getColor("Panel.background");
if (panelBg == null)
panelBg = new Color(245, 246, 248);
// 绘制半透明/圆角的主背景
g2.setColor(panelBg);
g2.fillRoundRect(0, 0, w, h, arc, arc);
// 把转换后的 Graphics 传给 super.paint 来绘制子组件(子组件会被 alpha & translate 影响)
super.paint(g2, c);
g2.dispose();
}
}
FadeLayerUI fadeUI = new FadeLayerUI();
JLayer<JComponent> layered = new JLayer<>(inner, fadeUI);
layered.setOpaque(false);
dialog.getContentPane().removeAll();
dialog.getContentPane().setBackground(new Color(0,0,0,0)); // 保持透明(视平台支持情况)
dialog.add(layered);
// 动画 Timer350ms 的淡入 + 下滑(使用 ease-out 曲线)
final int duration = 350; // ms
final long start = System.currentTimeMillis();
Timer timer = new Timer(16, null); // ~60fps
timer.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
float t = (System.currentTimeMillis() - start) / (float) duration;
if (t >= 1f) t = 1f;
// ease-out (cubic)
float eased = 1 - (float) Math.pow(1 - t, 3);
fadeUI.setAlpha(eased);
// translate 从 20 -> 0
fadeUI.setTranslateY((int) ((1 - eased) * 20));
layered.repaint();
if (t >= 1f) {
((Timer) e.getSource()).stop();
}
}
});
timer.setCoalesce(true);
// 初始状态隐藏内容alpha=0启动动画
fadeUI.setAlpha(0f);
fadeUI.setTranslateY(20);
layered.setVisible(true);
timer.start();
dialog.revalidate();
if (AxisInnovatorsBox.getMain().isWindowStartup(dialog)) {
AxisInnovatorsBox.getMain().popupWindow(dialog);
}
}
// ---------- 内部类ToolCategory, ToolItem 等(保持原结构) ----------
public static class ToolCategory {
private final String name;
@@ -1176,24 +1512,29 @@ public class MainWindow extends JFrame {
private final String description;
private final UUID id = UUID.randomUUID();
private final List<ToolItem> tools = new ArrayList<>();
/**
* 分类
* @param name 分类名称
* @param icon 分类的图标
* @param description 分类的描述
*/
public ToolCategory(String name, String icon, String description) {
this.name = name; this.icon = icon; this.description = description; this.iconImage = null;
}
public ToolCategory(String name, ImageIcon icon, String description) {
this.name = name; this.iconImage = icon; this.description = description; this.icon = null;
}
/**
* 在当前分类中创建一个新的工具卡片
* @param tool 工具卡片
*/
public void addTool(ToolItem tool) { tools.add(tool); }
public String getDescription() { return description; }
public String getIcon() {
if (isDarkTheme()) {
int lastDotIndex = 0;
if (icon != null) {
lastDotIndex = icon.lastIndexOf('.');
}
if (lastDotIndex > 0) {
return icon.substring(0, lastDotIndex) + "_dark" + icon.substring(lastDotIndex);
}
return icon + "_dark";
return icon.replace(".png", "_dark.png");
}
return icon;
}
@@ -1210,6 +1551,15 @@ public class MainWindow extends JFrame {
private final String description;
private final int id;
private final Action action;
/**
* 创建一个新的工具
* @param title 工具的标题
* @param icon 工具的图标
* @param description 工具的介绍
* @param id 工具的id不要重复即可
* @param action 点击工具卡片后触发的事件
*/
public ToolItem(String title, String icon, String description, int id, Action action) {
this.title = title; this.icon = icon; this.description = description; this.id = id; this.action = action; this.imageIcon = null;
}

View File

@@ -0,0 +1,657 @@
package com.axis.innovators.box.window;
import com.axis.innovators.box.window.WindowsJDialog;
import org.tzd.explorer.LocalCall;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.geom.Point2D;
import java.awt.RadialGradientPaint;
import java.io.IOException;
import java.util.Map;
/**
* @author tzdwindows 7
*/
public class TaskbarAppearanceWindow extends WindowsJDialog {
private final JComboBox<LocalCall.TaskbarAccentMode> modeCombo;
private final JButton colorButton;
private final JSlider alphaSlider;
private final JButton applyButton;
private final JButton helpButton;
private final PreviewPanel previewPanel;
private final JButton sysTextColorButton;
private final JButton applySysTextColorButton;
private Color sysTextColor = Color.BLACK;
// 动画平滑过渡参数
private Color currentColor = new Color(0, 0, 0);
private Color targetColor = new Color(0, 0, 0);
private int currentAlpha = 255; // 用于动画(实际 0-255
private int targetAlpha = 255; // 用于动画(实际 0-255
private LocalCall.TaskbarAccentMode currentMode = LocalCall.TaskbarAccentMode.ACCENT_DISABLED;
private LocalCall.TaskbarAccentMode targetMode = LocalCall.TaskbarAccentMode.ACCENT_DISABLED;
private Timer animationTimer = new Timer(16, e -> {});
// UI 元素引用(需要动态修改标签)
private JPanel alphaLabeledPanel;
private JLabel alphaLabel;
private static final Map<LocalCall.TaskbarAccentMode, String> MODE_LABELS = Map.of(
LocalCall.TaskbarAccentMode.ACCENT_DISABLED, "禁用特效(无额外效果)",
LocalCall.TaskbarAccentMode.ACCENT_ENABLE_GRADIENT, "渐变(纯色/渐变)",
LocalCall.TaskbarAccentMode.ACCENT_ENABLE_TRANSPARENTGRADIENT, "透明渐变(半透明)",
LocalCall.TaskbarAccentMode.ACCENT_ENABLE_BLURBEHIND, "毛玻璃模糊(高档模糊)",
LocalCall.TaskbarAccentMode.ACCENT_ENABLE_ACRYLICBLURBEHIND, "亚克力模糊Fluent 风格)",
LocalCall.TaskbarAccentMode.ACCENT_INVALID_STATE, "无效状态(保留)"
);
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
TaskbarAppearanceWindow w = new TaskbarAppearanceWindow(null);
w.setVisible(true);
});
}
public TaskbarAppearanceWindow(Window owner) {
super(owner, "任务栏外观设置", ModalityType.MODELESS);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
// 基本窗口设置
setSize(1220, 750);
setLocationRelativeTo(owner);
((JComponent) getContentPane()).setBorder(new EmptyBorder(12, 12, 12, 12));
setLayout(new BorderLayout(12, 12));
// 顶部标题
JPanel top = new JPanel(new BorderLayout());
top.setOpaque(false);
JLabel header = new JLabel("任务栏外观 - 高级设置");
header.setFont(header.getFont().deriveFont(Font.BOLD, 18f));
JLabel sub = new JLabel("可实时预览任务栏效果。");
sub.setFont(sub.getFont().deriveFont(12f));
top.add(header, BorderLayout.NORTH);
top.add(sub, BorderLayout.SOUTH);
add(top, BorderLayout.NORTH);
// 左侧:控制区
JPanel left = new JPanel();
left.setLayout(new BoxLayout(left, BoxLayout.Y_AXIS));
left.setOpaque(false);
left.setPreferredSize(new Dimension(400, getHeight()));
left.setMaximumSize(new Dimension(420, Integer.MAX_VALUE));
left.setBorder(new EmptyBorder(6, 6, 6, 6));
// 模式选择
modeCombo = new JComboBox<>(LocalCall.TaskbarAccentMode.values());
modeCombo.setMaximumSize(new Dimension(Integer.MAX_VALUE, 34));
modeCombo.setRenderer((list, value, index, isSelected, cellHasFocus) -> {
JLabel lab = new JLabel();
if (value instanceof LocalCall.TaskbarAccentMode) {
LocalCall.TaskbarAccentMode m = (LocalCall.TaskbarAccentMode) value;
lab.setText(MODE_LABELS.getOrDefault(m, m.name()));
} else {
lab.setText(String.valueOf(value));
}
lab.setOpaque(isSelected);
if (isSelected) {
lab.setBackground(new Color(0, 120, 215, 40));
}
lab.setBorder(new EmptyBorder(4, 4, 4, 4));
return lab;
});
left.add(labeledComponent("模式", modeCombo));
left.add(Box.createVerticalStrut(10));
// 颜色选择
colorButton = new JButton("选择任务栏颜色");
colorButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, 34));
colorButton.setPreferredSize(new Dimension(200, 32));
colorButton.setFocusPainted(false);
colorButton.addActionListener(e -> {
Color chosen = JColorChooser.showDialog(this, "选择任务栏颜色", targetColor);
if (chosen != null) {
setTargetColor(chosen);
}
});
left.add(labeledComponent("颜色", colorButton));
left.add(Box.createVerticalStrut(10));
// alpha 滑块(我们要能动态修改其标签和范围)
alphaSlider = new JSlider(0, 255, 255);
alphaSlider.setMajorTickSpacing(64);
alphaSlider.setMinorTickSpacing(16);
alphaSlider.setPaintTicks(true);
alphaSlider.setPaintLabels(true);
alphaSlider.setMaximumSize(new Dimension(Integer.MAX_VALUE, 64));
alphaSlider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
// 根据当前模式决定如何解释滑块值如果是毛玻璃degree 0-64映射到 0-255否则直接为 alpha
int v = alphaSlider.getValue();
if (targetMode == LocalCall.TaskbarAccentMode.ACCENT_ENABLE_BLURBEHIND) {
// degree -> alpha (scale 0-64 to 0-255)
float ratio = Math.max(0f, Math.min(1f, v / 64f));
targetAlpha = Math.round(ratio * 255f);
} else {
targetAlpha = clamp(v);
}
targetSliderChanged(v);
}
});
// 用一个可变标签面板包裹
alphaLabeledPanel = createAlphaLabeledPanel("不透明度 (Alpha)", alphaSlider);
left.add(alphaLabeledPanel);
left.add(Box.createVerticalStrut(10));
// 去掉字节序提示(按要求删除)
// left.add(Box.createVerticalStrut(10)); // 不再添加提示
// 按钮行
JPanel btnRow = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
btnRow.setOpaque(false);
applyButton = new JButton("应用到任务栏");
helpButton = new JButton("查看帮助");
Dimension smallBtn = new Dimension(140, 30);
applyButton.setPreferredSize(smallBtn);
helpButton.setPreferredSize(smallBtn);
applyButton.setMaximumSize(smallBtn);
helpButton.setMaximumSize(smallBtn);
applyButton.setFocusable(false);
helpButton.setFocusable(false);
applyButton.setMargin(new Insets(6, 10, 6, 10));
helpButton.setMargin(new Insets(6, 10, 6, 10));
btnRow.add(helpButton);
btnRow.add(applyButton);
btnRow.setAlignmentX(Component.LEFT_ALIGNMENT);
left.add(btnRow);
left.add(Box.createVerticalStrut(14));
// 系统文字颜色(实验性)
sysTextColorButton = new JButton("选择系统文字颜色");
sysTextColorButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, 34));
sysTextColorButton.setPreferredSize(new Dimension(180, 30));
sysTextColorButton.setBackground(sysTextColor);
sysTextColorButton.setForeground(contrast(sysTextColor));
sysTextColorButton.setFocusPainted(false);
sysTextColorButton.addActionListener(e -> {
Color chosen = JColorChooser.showDialog(this, "选择要设置的系统文字颜色", sysTextColor);
if (chosen != null) {
sysTextColor = chosen;
sysTextColorButton.setBackground(sysTextColor);
sysTextColorButton.setForeground(contrast(sysTextColor));
}
});
left.add(labeledComponent("系统文字颜色(实验性)", sysTextColorButton));
left.add(Box.createVerticalStrut(8));
applySysTextColorButton = new JButton("应用系统文字颜色(修改注册表)");
applySysTextColorButton.setAlignmentX(Component.LEFT_ALIGNMENT);
applySysTextColorButton.setPreferredSize(new Dimension(220, 30));
applySysTextColorButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, 34));
applySysTextColorButton.setFocusPainted(false);
left.add(applySysTextColorButton);
left.add(Box.createVerticalGlue());
// 右侧:实时预览区
previewPanel = new PreviewPanel();
previewPanel.setPreferredSize(new Dimension(720, 520));
JPanel previewWrap = new JPanel(new BorderLayout());
previewWrap.setBorder(BorderFactory.createTitledBorder("实时预览"));
previewWrap.add(previewPanel, BorderLayout.CENTER);
// 主布局
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, left, previewWrap);
split.setResizeWeight(0);
split.setDividerLocation(420);
split.setOneTouchExpandable(false);
add(split, BorderLayout.CENTER);
// 事件绑定:模式切换时更新控件状态
modeCombo.addActionListener(e -> {
LocalCall.TaskbarAccentMode sel = (LocalCall.TaskbarAccentMode) modeCombo.getSelectedItem();
if (sel != null) {
setTargetMode(sel);
updateControlsForMode(sel);
}
});
// apply 按钮行为(在后台线程执行 LocalCall
applyButton.addActionListener(e -> {
setControlsEnabled(false);
LocalCall.TaskbarAccentMode mode = targetMode;
String colorArg = buildColorArg(targetColor); // 0xRRGGBB
int alphaToSend = targetAlpha; // targetAlpha 已经是 0-255如果毛玻璃按 degree 映射)
boolean restartExplorerAlways = false;
if (mode == LocalCall.TaskbarAccentMode.ACCENT_INVALID_STATE) {
restartExplorerAlways = true;
}
boolean finalRestartExplorerAlways = restartExplorerAlways;
SwingWorker<Void, Void> worker = new SwingWorker<>() {
Exception ex = null;
@Override
protected Void doInBackground() {
try {
LocalCall.setTaskbarAppearance(mode, colorArg, alphaToSend, finalRestartExplorerAlways);
} catch (IOException | InterruptedException e1) {
ex = e1;
}
return null;
}
@Override
protected void done() {
setControlsEnabled(true);
if (ex != null) {
JOptionPane.showMessageDialog(TaskbarAppearanceWindow.this,
"应用到任务栏失败: " + ex.getMessage(),
"错误", JOptionPane.ERROR_MESSAGE);
} else {
JOptionPane.showMessageDialog(TaskbarAppearanceWindow.this,
"已尝试应用设置。若未生效可能需要 Explorer 重启或注销。", "完成",
JOptionPane.INFORMATION_MESSAGE);
}
}
};
worker.execute();
});
// 帮助按钮
helpButton.addActionListener(e -> {
SwingWorker<Void, Void> w = new SwingWorker<>() {
Exception ex = null;
@Override
protected Void doInBackground() {
try {
LocalCall.showTaskbarHelp();
} catch (IOException | InterruptedException ioException) {
ex = ioException;
}
return null;
}
@Override
protected void done() {
if (ex != null) {
JOptionPane.showMessageDialog(TaskbarAppearanceWindow.this,
"调用帮助失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
};
w.execute();
});
// 应用系统文字颜色
applySysTextColorButton.addActionListener(e -> {
applySysTextColorButton.setEnabled(false);
sysTextColorButton.setEnabled(false);
SwingWorker<Void, Void> worker = new SwingWorker<>() {
Exception ex = null;
@Override
protected Void doInBackground() {
try {
setWindowsTextColor(sysTextColor);
} catch (IOException | InterruptedException ioException) {
ex = ioException;
}
return null;
}
@Override
protected void done() {
applySysTextColorButton.setEnabled(true);
sysTextColorButton.setEnabled(true);
if (ex != null) {
JOptionPane.showMessageDialog(TaskbarAppearanceWindow.this,
"应用系统文字颜色失败:" + ex.getMessage(),
"错误", JOptionPane.ERROR_MESSAGE);
} else {
JOptionPane.showMessageDialog(TaskbarAppearanceWindow.this,
"已尝试修改注册表并刷新用户外观参数(可能需要注销/重启以完全生效)。",
"提示", JOptionPane.INFORMATION_MESSAGE);
}
}
};
worker.execute();
});
// 双击预览快速体验
previewPanel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
modeCombo.setSelectedItem(LocalCall.TaskbarAccentMode.ACCENT_ENABLE_BLURBEHIND);
// 设置滑块为中等程度示例
alphaSlider.setValue(32);
applyButton.doClick();
}
}
});
// 初始化目标并启动动画 Timer
setTargetColor(currentColor);
// 初始化 slider 为 255默认
alphaSlider.setValue(255);
// 默认模式更新 UI
updateControlsForMode(currentMode);
animationTimer = new Timer(16, e -> {
boolean changed = false;
int rCur = currentColor.getRed(), gCur = currentColor.getGreen(), bCur = currentColor.getBlue();
int rT = targetColor.getRed(), gT = targetColor.getGreen(), bT = targetColor.getBlue();
int rNew = lerp(rCur, rT, 0.12f);
int gNew = lerp(gCur, gT, 0.12f);
int bNew = lerp(bCur, bT, 0.12f);
Color newColor = new Color(clamp(rNew), clamp(gNew), clamp(bNew));
if (!newColor.equals(currentColor)) {
currentColor = newColor;
changed = true;
}
int aNew = lerp(currentAlpha, targetAlpha, 0.14f);
if (aNew != currentAlpha) {
currentAlpha = aNew;
changed = true;
}
if (currentMode != targetMode) {
if (Math.abs(currentAlpha - targetAlpha) < 3 &&
Math.abs(currentColor.getRed() - targetColor.getRed()) < 3 &&
Math.abs(currentColor.getGreen() - targetColor.getGreen()) < 3 &&
Math.abs(currentColor.getBlue() - targetColor.getBlue()) < 3) {
currentMode = targetMode;
changed = true;
}
}
if (changed) {
previewPanel.setPreviewState(currentColor, currentAlpha, currentMode);
} else {
animationTimer.stop();
}
});
}
// Enable / disable 控件
private void setControlsEnabled(boolean enabled) {
// 模式选择保持可用(用户可以改变)
modeCombo.setEnabled(true);
//applyButton.setEnabled(enabled);
helpButton.setEnabled(enabled);
colorButton.setEnabled(enabled && targetMode != LocalCall.TaskbarAccentMode.ACCENT_ENABLE_BLURBEHIND
&& targetMode != LocalCall.TaskbarAccentMode.ACCENT_INVALID_STATE);
alphaSlider.setEnabled(enabled && targetMode != LocalCall.TaskbarAccentMode.ACCENT_INVALID_STATE);
applySysTextColorButton.setEnabled(enabled);
sysTextColorButton.setEnabled(enabled);
}
// 带标签的组件包装(左对齐)
private JPanel labeledComponent(String label, JComponent comp) {
JPanel p = new JPanel();
p.setLayout(new BorderLayout(8, 8));
p.setOpaque(false);
JLabel l = new JLabel(label);
l.setPreferredSize(new Dimension(140, 24));
p.add(l, BorderLayout.WEST);
p.add(comp, BorderLayout.CENTER);
p.setAlignmentX(Component.LEFT_ALIGNMENT);
return p;
}
// 创建 alpha 带可变标签的面板
private JPanel createAlphaLabeledPanel(String labelText, JComponent comp) {
JPanel p = new JPanel(new BorderLayout(8, 8));
p.setOpaque(false);
alphaLabel = new JLabel(labelText);
alphaLabel.setPreferredSize(new Dimension(140, 24));
p.add(alphaLabel, BorderLayout.WEST);
p.add(comp, BorderLayout.CENTER);
p.setAlignmentX(Component.LEFT_ALIGNMENT);
return p;
}
private void setTargetColor(Color c) {
if (c == null) return;
targetColor = c;
colorButton.setBackground(c);
colorButton.setForeground(contrast(c));
previewPanel.setPreviewState(targetColor, targetAlpha, targetMode);
startAnimationIfNeeded();
}
// 当滑块值改变v 为滑块上的值:可能是 0-255 或 0-64
private void targetSliderChanged(int v) {
// 更新预览targetAlpha 已在 ChangeListener 中计算)
previewPanel.setPreviewState(targetColor, targetAlpha, targetMode);
startAnimationIfNeeded();
}
private void setTargetMode(LocalCall.TaskbarAccentMode mode) {
if (mode == null) return;
targetMode = mode;
// 如果进入毛玻璃模式,确保 targetAlpha 基于当前 slider映射
if (targetMode == LocalCall.TaskbarAccentMode.ACCENT_ENABLE_BLURBEHIND) {
int degree = alphaSlider.getValue();
float ratio = Math.max(0f, Math.min(1f, degree / 64f));
targetAlpha = Math.round(ratio * 255f);
}
previewPanel.setPreviewState(targetColor, targetAlpha, targetMode);
startAnimationIfNeeded();
}
private void updateControlsForMode(LocalCall.TaskbarAccentMode mode) {
// 默认启用全部(模式选择除外)
boolean disableAll = (mode == LocalCall.TaskbarAccentMode.ACCENT_INVALID_STATE);
boolean isBlur = (mode == LocalCall.TaskbarAccentMode.ACCENT_ENABLE_BLURBEHIND);
// 如果无效状态:什么都不能设置(除模式之外)
colorButton.setEnabled(!disableAll && !isBlur);
sysTextColorButton.setEnabled(!disableAll);
applySysTextColorButton.setEnabled(!disableAll);
helpButton.setEnabled(!disableAll);
//applyButton.setEnabled(!disableAll);
// 配置滑块范围与标签
if (isBlur) {
// 毛玻璃:滑块行为为 程度 0-64
alphaLabel.setText("程度 (0-64)");
alphaSlider.setMinimum(0);
alphaSlider.setMaximum(64);
alphaSlider.setMajorTickSpacing(16);
alphaSlider.setMinorTickSpacing(4);
alphaSlider.setPaintLabels(true);
// 如果之前值超范围则设置为上限
if (alphaSlider.getValue() > 64) alphaSlider.setValue(32);
// 计算 targetAlpha映射到 0-255
int degree = alphaSlider.getValue();
float ratio = Math.max(0f, Math.min(1f, degree / 64f));
targetAlpha = Math.round(ratio * 255f);
} else {
// 常规模式:不透明度 0-255
alphaLabel.setText("不透明度 (Alpha)");
alphaSlider.setMinimum(0);
alphaSlider.setMaximum(255);
alphaSlider.setMajorTickSpacing(64);
alphaSlider.setMinorTickSpacing(16);
alphaSlider.setPaintLabels(true);
// 若当前 slider 值超过 255调整
if (alphaSlider.getValue() > 255) alphaSlider.setValue(255);
targetAlpha = clamp(alphaSlider.getValue());
}
// 预览和控件可用性
alphaSlider.setEnabled(!disableAll);
setControlsEnabled(!disableAll);
// 立即更新预览状态
previewPanel.setPreviewState(targetColor, targetAlpha, mode);
startAnimationIfNeeded();
}
private void startAnimationIfNeeded() {
if (!animationTimer.isRunning()) animationTimer.start();
}
private static int lerp(int a, int b, float t) {
return Math.round(a + (b - a) * t);
}
private static int clamp(int v) {
return Math.min(255, Math.max(0, v));
}
private static Color contrast(Color bg) {
double luminance = (0.299 * bg.getRed() + 0.587 * bg.getGreen() + 0.114 * bg.getBlue()) / 255.0;
return luminance > 0.6 ? Color.BLACK : Color.WHITE;
}
/**
* 构建传入本地工具的颜色字符串(固定为 0xRRGGBB
* 说明为避免字节序混淆Java 端始终发送 RGB不包含 alpha例如 #0099CC -> 0x0099CC
*/
private static String buildColorArg(Color c) {
if (c == null) return "0x000000";
int r = c.getRed();
int g = c.getGreen();
int b = c.getBlue();
return String.format("0x%02X%02X%02X", r, g, b);
}
/**
* 通过修改注册表设置系统文字颜色(实验性)
*/
private static void setWindowsTextColor(Color c) throws IOException, InterruptedException {
String rgb = String.format("%d %d %d", c.getRed(), c.getGreen(), c.getBlue());
String key = "HKCU\\Control Panel\\Colors";
String[] values = new String[]{"WindowText", "HilightText", "ButtonText"};
for (String v : values) {
String cmd = String.format("reg add \"%s\" /v %s /t REG_SZ /d \"%s\" /f", key, v, rgb);
Process p = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd});
p.waitFor();
}
Process refresh = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c",
"RUNDLL32.EXE user32.dll,UpdatePerUserSystemParameters"});
refresh.waitFor();
}
/**
* 预览面板:绘制模拟任务栏效果(更现代的表现)
*/
private static class PreviewPanel extends JPanel {
private Color previewColor = new Color(0, 0, 0);
private int previewAlpha = 255;
private LocalCall.TaskbarAccentMode previewMode = LocalCall.TaskbarAccentMode.ACCENT_DISABLED;
PreviewPanel() {
setOpaque(true);
setBackground(new Color(0xF4F7FB));
}
void setPreviewState(Color color, int alpha, LocalCall.TaskbarAccentMode mode) {
this.previewColor = color != null ? color : new Color(0,0,0);
this.previewAlpha = clamp(alpha);
this.previewMode = mode != null ? mode : LocalCall.TaskbarAccentMode.ACCENT_DISABLED;
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int w = getWidth();
int h = getHeight();
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 背景渐变
GradientPaint bg = new GradientPaint(0, 0, new Color(0xEAF3FF), 0, h, new Color(0xE6F0FF));
g2.setPaint(bg);
g2.fillRect(0, 0, w, h);
// 任务栏容器
int barH = Math.max(76, h / 6);
int barY = h - barH - 36;
int margin = 36;
int barX = margin;
int barW = w - margin * 2;
RoundRectangle2D.Float barRect = new RoundRectangle2D.Float(barX, barY, barW, barH, 18, 18);
// 根据模式绘制
switch (previewMode) {
case ACCENT_DISABLED:
Color solid = new Color(previewColor.getRed(), previewColor.getGreen(), previewColor.getBlue(),
Math.max(200, previewAlpha));
g2.setPaint(solid);
g2.fill(barRect);
break;
case ACCENT_ENABLE_GRADIENT:
case ACCENT_ENABLE_TRANSPARENTGRADIENT:
Color c1 = new Color(previewColor.getRed(), previewColor.getGreen(), previewColor.getBlue(),
Math.max(60, previewAlpha / 2));
Color c2 = new Color(clamp(previewColor.getRed() + 30), clamp(previewColor.getGreen() + 30),
clamp(previewColor.getBlue() + 30), Math.max(60, previewAlpha / 2));
GradientPaint gp = new GradientPaint(barX, barY, c1, barX + barW, barY + barH, c2);
g2.setPaint(gp);
g2.fill(barRect);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.08f));
g2.fill(new RoundRectangle2D.Float(barX, barY, barW, barH / 2f, 16, 16));
g2.setComposite(AlphaComposite.SrcOver);
break;
case ACCENT_ENABLE_BLURBEHIND:
Color base = new Color(previewColor.getRed(), previewColor.getGreen(), previewColor.getBlue(),
Math.max(8, previewAlpha / 3));
g2.setPaint(base);
g2.fill(barRect);
Point2D center = new Point2D.Float(barX + barW / 2f, barY + barH / 2f);
float radius = Math.max(barW, barH);
RadialGradientPaint rgp = new RadialGradientPaint(center, radius,
new float[]{0f, 1f},
new Color[]{new Color(255, 255, 255, Math.min(100, previewAlpha / 2)),
new Color(0, 0, 0, Math.min(40, previewAlpha / 6))});
g2.setPaint(rgp);
g2.fill(barRect);
g2.setStroke(new BasicStroke(1.2f));
for (int i = 0; i < 5; i++) {
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.04f + i * 0.02f));
g2.draw(new RoundRectangle2D.Float(barX + i, barY + i, barW - i * 2f, barH - i * 2f, 16, 16));
}
g2.setComposite(AlphaComposite.SrcOver);
break;
case ACCENT_ENABLE_ACRYLICBLURBEHIND:
case ACCENT_INVALID_STATE:
default:
Color acr = new Color(previewColor.getRed(), previewColor.getGreen(), previewColor.getBlue(),
Math.max(20, previewAlpha / 2));
g2.setPaint(acr);
g2.fill(barRect);
GradientPaint spot = new GradientPaint(barX, barY, new Color(255, 255, 255, 140),
barX + barW / 2, barY + barH / 2, new Color(255, 255, 255, 0));
g2.setPaint(spot);
g2.fill(new RoundRectangle2D.Float(barX, barY, barW / 2f, barH, 16, 16));
for (int i = 0; i < barW; i += 10) {
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.03f));
g2.drawLine(barX + i, barY + 6, barX + i + 6, barY + barH - 6);
}
g2.setComposite(AlphaComposite.SrcOver);
break;
}
// 绘制图标占位
int iconY = barY + 14;
int iconSize = Math.min(36, barH - 28);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.95f));
g2.setColor(new Color(255, 255, 255, 200));
g2.fillOval(barX + 16, iconY, iconSize, iconSize);
int trayX = barX + barW - 18 - (iconSize + 10) * 4;
for (int i = 0; i < 4; i++) {
int x = trayX + i * (iconSize + 10);
g2.fillRoundRect(x, iconY, iconSize, iconSize, 8, 8);
}
g2.dispose();
}
}
}

View File

@@ -0,0 +1,8 @@
package com.chuangzhou.vivid2D;
public class Main {
public static void main(String[] args) {
}
}

View File

@@ -0,0 +1,221 @@
package com.chuangzhou.vivid2D.ai;
import com.chuangzhou.vivid2D.ai.anime_face_segmentation.AnimeModelWrapper;
import com.chuangzhou.vivid2D.ai.anime_segmentation.Anime2VividModelWrapper;
import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetVividModelWrapper;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 模型管理器 - 负责模型的注册、分类和检索
*/
public class ModelManagement {
private final Map<String, Class<?>> models = new ConcurrentHashMap<>();
private final Map<String, List<String>> modelsByCategory = new ConcurrentHashMap<>();
private final List<String> modelDisplayNames = new ArrayList<>();
private final Map<String, String> displayNameToRegistrationName = new ConcurrentHashMap<>();
private ModelManagement() {
initializeDefaultCategories();
registerDefaultModels();
}
/**
* 初始化默认分类
*/
private void initializeDefaultCategories() {
modelsByCategory.put("Image Segmentation", new ArrayList<>());
modelsByCategory.put("Image Processing", new ArrayList<>());
modelsByCategory.put("Image Generation", new ArrayList<>());
modelsByCategory.put("Image Inpainting", new ArrayList<>());
modelsByCategory.put("Image Completion", new ArrayList<>());
modelsByCategory.put("Face Analysis", new ArrayList<>());
}
/**
* 注册默认模型
*/
private void registerDefaultModels() {
registerModel("segmentation:anime_face", "Anime Face Segmentation",
AnimeModelWrapper.class, "Image Segmentation");
registerModel("segmentation:anime", "Anime Image Segmentation",
Anime2VividModelWrapper.class, "Image Segmentation");
registerModel("segmentation:face_parsing", "Face Parsing",
BiSeNetVividModelWrapper.class, "Image Segmentation");
}
/**
* 注册模型
* @param modelRegistrationName 注册名称,格式必须为 "category:model_name"
* @param modelDisplayName 模型显示名称
* @param modelClass 模型类
* @param category 模型类别
*/
public void registerModel(String modelRegistrationName, String modelDisplayName,
Class<?> modelClass, String category) {
if (!isValidRegistrationName(modelRegistrationName)) {
throw new IllegalArgumentException(
"Invalid registration name format. Expected 'category:model_name', got: " + modelRegistrationName);
}
if (models.containsKey(modelRegistrationName)) {
throw new IllegalArgumentException(
"Model registration name already exists: " + modelRegistrationName);
}
if (displayNameToRegistrationName.containsKey(modelDisplayName)) {
throw new IllegalArgumentException(
"Model display name already exists: " + modelDisplayName);
}
if (!modelsByCategory.containsKey(category)) {
modelsByCategory.put(category, new ArrayList<>());
}
models.put(modelRegistrationName, modelClass);
displayNameToRegistrationName.put(modelDisplayName, modelRegistrationName);
modelDisplayNames.add(modelDisplayName);
modelsByCategory.get(category).add(modelRegistrationName);
}
/**
* 验证注册名称格式
*/
private boolean isValidRegistrationName(String name) {
return name != null && name.matches("^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+$");
}
/**
* 通过显示名称获取模型类
*/
public Class<?> getModel(String modelDisplayName) {
String registrationName = displayNameToRegistrationName.get(modelDisplayName);
return registrationName != null ? models.get(registrationName) : null;
}
/**
* 通过索引获取模型类
*/
public Class<?> getModel(int modelIndex) {
if (modelIndex >= 0 && modelIndex < modelDisplayNames.size()) {
String displayName = modelDisplayNames.get(modelIndex);
return getModel(displayName);
}
return null;
}
/**
* 通过注册名称获取模型类
*/
public Class<?> getModelByRegistrationName(String registrationName) {
return models.get(registrationName);
}
/**
* 通过类名获取模型类
*/
public Class<?> getModelByClassName(String className) {
for (Class<?> modelClass : models.values()) {
if (modelClass.getName().equals(className)) {
return modelClass;
}
}
return null;
}
/**
* 获取所有模型的显示名称
*/
public List<String> getAllModelDisplayNames() {
return Collections.unmodifiableList(modelDisplayNames);
}
/**
* 获取所有模型的注册名称
*/
public Set<String> getAllModelRegistrationNames() {
return Collections.unmodifiableSet(models.keySet());
}
/**
* 按类别获取模型注册名称
*/
public List<String> getModelsByCategory(String category) {
return Collections.unmodifiableList(
modelsByCategory.getOrDefault(category, new ArrayList<>())
);
}
/**
* 获取所有可用的类别
*/
public Set<String> getAllCategories() {
return Collections.unmodifiableSet(modelsByCategory.keySet());
}
/**
* 获取模型数量
*/
public int getModelCount() {
return modelDisplayNames.size();
}
/**
* 获取模型显示名称对应的注册名称
*/
public String getRegistrationName(String modelDisplayName) {
return displayNameToRegistrationName.get(modelDisplayName);
}
/**
* 获取模型注册名称对应的显示名称
*/
public String getDisplayName(String registrationName) {
for (Map.Entry<String, String> entry : displayNameToRegistrationName.entrySet()) {
if (entry.getValue().equals(registrationName)) {
return entry.getKey();
}
}
return null;
}
/**
* 检查模型是否存在
*/
public boolean containsModel(String modelDisplayName) {
return displayNameToRegistrationName.containsKey(modelDisplayName);
}
/**
* 检查注册名称是否存在
*/
public boolean containsRegistrationName(String registrationName) {
return models.containsKey(registrationName);
}
/**
* 移除模型
*/
public boolean removeModel(String modelDisplayName) {
String registrationName = displayNameToRegistrationName.get(modelDisplayName);
if (registrationName != null) {
// 从所有存储中移除
models.remove(registrationName);
displayNameToRegistrationName.remove(modelDisplayName);
modelDisplayNames.remove(modelDisplayName);
// 从类别中移除
for (List<String> categoryModels : modelsByCategory.values()) {
categoryModels.remove(registrationName);
}
return true;
}
return false;
}
private static final class InstanceHolder {
private static final ModelManagement instance = new ModelManagement();
}
public static ModelManagement getInstance() {
return InstanceHolder.instance;
}
}

View File

@@ -0,0 +1,33 @@
package com.chuangzhou.vivid2D.ai;
import java.awt.image.BufferedImage;
import java.util.Map;
public class SegmentationResult {
// 分割掩码图(每个像素的颜色为对应类别颜色)
private final BufferedImage maskImage;
// 类别索引 -> 类别名称
private final Map<Integer, String> labels;
// 类别名称 -> ARGB 颜色
private final Map<String, Integer> palette;
public SegmentationResult(BufferedImage maskImage, Map<Integer, String> labels, Map<String, Integer> palette) {
this.maskImage = maskImage;
this.labels = labels;
this.palette = palette;
}
public BufferedImage getMaskImage() {
return maskImage;
}
public Map<Integer, String> getLabels() {
return labels;
}
public Map<String, Integer> getPalette() {
return palette;
}
}

View File

@@ -0,0 +1,154 @@
package com.chuangzhou.vivid2D.ai;
import ai.djl.MalformedModelException;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
import ai.djl.ndarray.NDManager;
import ai.djl.ndarray.types.DataType;
import ai.djl.repository.zoo.Criteria;
import ai.djl.repository.zoo.ModelNotFoundException;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.translate.Batchifier;
import ai.djl.translate.TranslateException;
import ai.djl.translate.Translator;
import ai.djl.translate.TranslatorContext;
import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetLabelPalette;
import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetSegmentationResult;
import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetSegmenter;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
public abstract class Segmenter implements AutoCloseable {
// 内部类用于从Translator安全地传出数据
public static class SegmentationData {
public final int[] indices;
public final long[] shape;
public SegmentationData(int[] indices, long[] shape) {
this.indices = indices;
this.shape = shape;
}
}
private String engine = "PyTorch";
protected final ZooModel<Image, Segmenter.SegmentationData> modelWrapper;
protected final Predictor<Image, Segmenter.SegmentationData> predictor;
protected final List<String> labels;
protected final Map<String, Integer> palette;
public Segmenter(Path modelDir, List<String> labels) throws IOException, MalformedModelException, ModelNotFoundException {
this.labels = new ArrayList<>(labels);
this.palette = BiSeNetLabelPalette.defaultPalette();
Translator<Image, Segmenter.SegmentationData> translator = new Translator<Image, Segmenter.SegmentationData>() {
@Override
public NDList processInput(TranslatorContext ctx, Image input) {
return Segmenter.this.processInput(ctx, input);
}
@Override
public Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list) {
return Segmenter.this.processOutput(ctx, list);
}
@Override
public Batchifier getBatchifier() {
return Segmenter.this.getBatchifier();
}
};
Criteria<Image, Segmenter.SegmentationData> criteria = Criteria.builder()
.setTypes(Image.class, Segmenter.SegmentationData.class)
.optModelPath(modelDir)
.optEngine(engine)
.optTranslator(translator)
.build();
this.modelWrapper = criteria.loadModel();
this.predictor = modelWrapper.newPredictor();
}
/**
* 处理模型输入
* @param ctx translator 上下文
* @param input 图片
* @return 模型输入
*/
public abstract NDList processInput(TranslatorContext ctx, Image input);
/**
* 处理模型输出
* @param ctx translator 上下文
* @param list 模型输出
* @return 模型输出
*/
public abstract Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list);
/**
* 获取批量处理方式
* @return 批量处理方式
*/
public Batchifier getBatchifier(){
return null;
}
public SegmentationResult segment(File imgFile) throws TranslateException, IOException {
Image img = ImageFactory.getInstance().fromFile(imgFile.toPath());
// predict 方法现在直接返回安全的 Java 对象
Segmenter.SegmentationData data = predictor.predict(img);
long[] shp = data.shape;
int[] indices = data.indices;
int height, width;
if (shp.length == 2) {
height = (int) shp[0];
width = (int) shp[1];
} else {
throw new RuntimeException("Unexpected classMap shape from SegmentationData: " + Arrays.toString(shp));
}
// 后续处理完全基于 Java 对象,不再有 Native resource 问题
BufferedImage mask = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Map<Integer, String> labelsMap = new HashMap<>();
for (int i = 0; i < labels.size(); i++) {
labelsMap.put(i, labels.get(i));
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = indices[y * width + x];
String label = labelsMap.getOrDefault(idx, "unknown");
int argb = palette.getOrDefault(label, 0xFF00FF00);
mask.setRGB(x, y, argb);
}
}
return new SegmentationResult(mask, labelsMap, palette);
}
public void setEngine(String engine) {
this.engine = engine;
}
@Override
public void close() {
try {
predictor.close();
} catch (Exception ignore) {
}
try {
modelWrapper.close();
} catch (Exception ignore) {
}
}
}

View File

@@ -0,0 +1,113 @@
package com.chuangzhou.vivid2D.ai;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.List;
public abstract class VividModelWrapper<s extends Segmenter> implements AutoCloseable{
protected final s segmenter;
protected final List<String> labels; // index -> name
protected final Map<String, Integer> palette; // name -> ARGB
protected VividModelWrapper(s segmenter, List<String> labels, Map<String, Integer> palette) {
this.segmenter = segmenter;
this.labels = labels;
this.palette = palette;
}
public List<String> getLabels() {
return Collections.unmodifiableList(labels);
}
public Map<String, Integer> getPalette() {
return Collections.unmodifiableMap(palette);
}
/**
* 直接返回分割结果SegmentationResult
*/
public SegmentationResult segment(File inputImage) throws Exception {
return segmenter.segment(inputImage);
}
/**
* 把指定 targets标签名集合从输入图片中分割并保存到 outDir。
* 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。
* <p>
* 返回值Map<labelName, ResultFiles>ResultFiles 包含 maskFile、overlayFile两个 PNG
*/
public abstract Map<String, ResultFiles> segmentAndSave(File inputImage, Set<String> targets, Path outDir) throws Exception;
protected static String safeFileName(String s) {
return s.replaceAll("[^a-zA-Z0-9_\\-\\.]", "_");
}
protected static Set<String> parseTargetsSet(Set<String> in) {
if (in == null || in.isEmpty()) return Collections.emptySet();
// 若包含单个 "all"
if (in.size() == 1) {
String only = in.iterator().next();
if ("all".equalsIgnoreCase(only.trim())) {
return Set.of("all");
}
}
// 直接返回 trim 后的小写不变集合(保持用户传入的名字)
Set<String> out = new LinkedHashSet<>();
for (String s : in) {
if (s != null) out.add(s.trim());
}
return out;
}
/**
* 关闭底层资源
*/
@Override
public void close() {
try {
segmenter.close();
} catch (Exception ignore) {}
}
/* ================= helper: 从 modelDir 读取 synset.txt ================= */
protected static Optional<List<String>> loadLabelsFromSynset(Path modelDir) {
Path syn = modelDir.resolve("synset.txt");
if (Files.exists(syn)) {
try {
List<String> lines = Files.readAllLines(syn);
List<String> cleaned = new ArrayList<>();
for (String l : lines) {
String s = l.trim();
if (!s.isEmpty()) cleaned.add(s);
}
if (!cleaned.isEmpty()) return Optional.of(cleaned);
} catch (IOException ignore) {}
}
return Optional.empty();
}
/**
* 存放结果文件路径
*/
public static class ResultFiles {
private final File maskFile;
private final File overlayFile;
public ResultFiles(File maskFile, File overlayFile) {
this.maskFile = maskFile;
this.overlayFile = overlayFile;
}
public File getMaskFile() {
return maskFile;
}
public File getOverlayFile() {
return overlayFile;
}
}
}

View File

@@ -0,0 +1,62 @@
package com.chuangzhou.vivid2D.ai.anime_face_segmentation;
import java.util.*;
/**
* Anime-Face-Segmentation UNet 模型的标签和颜色调色板。
* 基于 Anime-Face-Segmentation 项目的 util.py 中的颜色定义。
* 标签索引必须与模型输出索引一致0-6
*/
public class AnimeLabelPalette {
/**
* Anime-Face-Segmentation UNet 模型的标准标签7个类别索引 0-6
*/
public static List<String> defaultLabels() {
return Arrays.asList(
"background", // 0 - 青色 (0,255,255)
"hair", // 1 - 蓝色 (255,0,0)
"eye", // 2 - 红色 (0,0,255)
"mouth", // 3 - 白色 (255,255,255)
"face", // 4 - 绿色 (0,255,0)
"skin", // 5 - 黄色 (255,255,0)
"clothes" // 6 - 紫色 (255,0,255)
);
}
/**
* 返回对应的调色板:类别名 -> ARGB 颜色值。
* 颜色值基于 util.py 中的 PALETTE 数组的 RGB 值转换为 ARGB 格式 (0xFFRRGGBB)。
*/
public static Map<String, Integer> defaultPalette() {
Map<String, Integer> map = new HashMap<>();
// 索引 0: background -> (0,255,255) 青色
map.put("background", 0xFF00FFFF);
// 索引 1: hair -> (255,0,0) 蓝色
map.put("hair", 0xFFFF0000);
// 索引 2: eye -> (0,0,255) 红色
map.put("eye", 0xFF0000FF);
// 索引 3: mouth -> (255,255,255) 白色
map.put("mouth", 0xFFFFFFFF);
// 索引 4: face -> (0,255,0) 绿色
map.put("face", 0xFF00FF00);
// 索引 5: skin -> (255,255,0) 黄色
map.put("skin", 0xFFFFFF00);
// 索引 6: clothes -> (255,0,255) 紫色
map.put("clothes", 0xFFFF00FF);
return map;
}
/**
* 获取类别索引到名称的映射
*/
public static Map<Integer, String> getIndexToLabelMap() {
List<String> labels = defaultLabels();
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < labels.size(); i++) {
map.put(i, labels.get(i));
}
return map;
}
}

View File

@@ -0,0 +1,322 @@
package com.chuangzhou.vivid2D.ai.anime_face_segmentation;
import com.chuangzhou.vivid2D.ai.VividModelWrapper;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.List;
/**
* AnimeModelWrapper - 专门为 Anime-Face-Segmentation 模型封装的 Wrapper
*/
public class AnimeModelWrapper extends VividModelWrapper<AnimeSegmenter> {
private AnimeModelWrapper(AnimeSegmenter segmenter, List<String> labels, Map<String, Integer> palette) {
super(segmenter, labels, palette);
}
/**
* 加载模型
*/
public static AnimeModelWrapper load(Path modelDir) throws Exception {
List<String> labels = loadLabelsFromSynset(modelDir).orElseGet(AnimeLabelPalette::defaultLabels);
AnimeSegmenter segmenter = new AnimeSegmenter(modelDir, labels);
Map<String, Integer> palette = AnimeLabelPalette.defaultPalette();
return new AnimeModelWrapper(segmenter, labels, palette);
}
/**
* 直接返回分割结果(在丢给底层 segmenter 前会做通用预处理RGB 转换 + 等比 letterbox 缩放到模型输入尺寸)
*/
public AnimeSegmentationResult segment(File inputImage) throws Exception {
File pre = null;
try {
pre = preprocessAndSave(inputImage);
// 将预处理后的临时文件丢给底层 segmenter
return segmenter.segment(pre);
} finally {
if (pre != null && pre.exists()) {
try { Files.deleteIfExists(pre.toPath()); } catch (Exception ignore) {}
}
}
}
/**
* 分割并保存结果
*/
public Map<String, ResultFiles> segmentAndSave(File inputImage, Set<String> targets, Path outDir) throws Exception {
if (!Files.exists(outDir)) {
Files.createDirectories(outDir);
}
AnimeSegmentationResult res = segment(inputImage);
BufferedImage original = ImageIO.read(inputImage);
BufferedImage maskImage = res.getMaskImage();
int maskW = maskImage.getWidth();
int maskH = maskImage.getHeight();
// 解析 targets
Set<String> realTargets = parseTargetsSet(targets);
Map<String, ResultFiles> saved = new LinkedHashMap<>();
for (String target : realTargets) {
if (!palette.containsKey(target)) {
// 尝试忽略大小写匹配
String finalTarget = target;
Optional<String> matched = palette.keySet().stream()
.filter(k -> k.equalsIgnoreCase(finalTarget))
.findFirst();
if (matched.isPresent()) target = matched.get();
else {
System.err.println("Warning: unknown label '" + target + "' - skip.");
continue;
}
}
int targetColor = palette.get(target);
// 1) 生成透明背景的二值掩码(只保留 target 像素)
BufferedImage partMask = new BufferedImage(maskW, maskH, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < maskH; y++) {
for (int x = 0; x < maskW; x++) {
int c = maskImage.getRGB(x, y);
if (c == targetColor) {
partMask.setRGB(x, y, targetColor | 0xFF000000); // 保证不透明
} else {
partMask.setRGB(x, y, 0x00000000);
}
}
}
// 2) 将 mask 缩放到与原图一致(如果需要),并生成 overlay半透明
BufferedImage maskResized = partMask;
if (original.getWidth() != maskW || original.getHeight() != maskH) {
maskResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = maskResized.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(partMask, 0, 0, original.getWidth(), original.getHeight(), null);
g.dispose();
}
BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = overlay.createGraphics();
g2.drawImage(original, 0, 0, null);
// 半透明颜色alpha = 0x88
int rgbOnly = (targetColor & 0x00FFFFFF);
int translucent = (0x88 << 24) | rgbOnly;
BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < colorOverlay.getHeight(); y++) {
for (int x = 0; x < colorOverlay.getWidth(); x++) {
int mc = maskResized.getRGB(x, y);
if ((mc & 0x00FFFFFF) == (targetColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) {
colorOverlay.setRGB(x, y, translucent);
} else {
colorOverlay.setRGB(x, y, 0x00000000);
}
}
}
g2.drawImage(colorOverlay, 0, 0, null);
g2.dispose();
// 保存
String safe = safeFileName(target);
File maskOut = outDir.resolve(safe + "_mask.png").toFile();
File overlayOut = outDir.resolve(safe + "_overlay.png").toFile();
ImageIO.write(maskResized, "png", maskOut);
ImageIO.write(overlay, "png", overlayOut);
saved.put(target, new ResultFiles(maskOut, overlayOut));
}
return saved;
}
/**
* 专门提取眼睛的方法(在丢给底层 segmenter 前做预处理)
*/
public ResultFiles extractEyes(File inputImage, Path outDir) throws Exception {
if (!Files.exists(outDir)) {
Files.createDirectories(outDir);
}
File pre = null;
BufferedImage eyes;
try {
pre = preprocessAndSave(inputImage);
eyes = segmenter.extractEyes(pre);
} finally {
if (pre != null && pre.exists()) {
try { Files.deleteIfExists(pre.toPath()); } catch (Exception ignore) {}
}
}
File eyesMask = outDir.resolve("eyes_mask.png").toFile();
ImageIO.write(eyes, "png", eyesMask);
// 创建眼睛的 overlay原有逻辑保持不变
BufferedImage original = ImageIO.read(inputImage);
BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = overlay.createGraphics();
g2.drawImage(original, 0, 0, null);
// 缩放眼睛掩码到原图尺寸
BufferedImage eyesResized = eyes;
if (original.getWidth() != eyes.getWidth() || original.getHeight() != eyes.getHeight()) {
eyesResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = eyesResized.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(eyes, 0, 0, original.getWidth(), original.getHeight(), null);
g.dispose();
}
int eyeColor = palette.getOrDefault("eye", 0xFF00FF); // 若没有 eye给个显眼默认色
int rgbOnly = (eyeColor & 0x00FFFFFF);
int translucent = (0x88 << 24) | rgbOnly;
BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < colorOverlay.getHeight(); y++) {
for (int x = 0; x < colorOverlay.getWidth(); x++) {
int mc = eyesResized.getRGB(x, y);
if ((mc & 0x00FFFFFF) == (eyeColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) {
colorOverlay.setRGB(x, y, translucent);
} else {
colorOverlay.setRGB(x, y, 0x00000000);
}
}
}
g2.drawImage(colorOverlay, 0, 0, null);
g2.dispose();
File eyesOverlay = outDir.resolve("eyes_overlay.png").toFile();
ImageIO.write(overlay, "png", eyesOverlay);
return new ResultFiles(eyesMask, eyesOverlay);
}
/**
* 关闭底层资源
*/
@Override
public void close() {
try {
segmenter.close();
} catch (Exception ignore) {}
}
// ========== 新增:预处理并保存到临时文件 ==========
private File preprocessAndSave(File inputImage) throws IOException {
BufferedImage img = ImageIO.read(inputImage);
if (img == null) throw new IOException("无法读取图片: " + inputImage);
// 转成标准 RGB去掉 alpha / 保证三通道)
BufferedImage rgb = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g = rgb.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(img, 0, 0, null);
g.dispose();
// 获取模型输入尺寸(尝试反射读取,找不到则使用默认 512x512
int[] size = getModelInputSize();
int targetW = size[0], targetH = size[1];
// 等比缩放并居中填充letterbox背景用白色
double scale = Math.min((double) targetW / rgb.getWidth(), (double) targetH / rgb.getHeight());
int newW = Math.max(1, (int) Math.round(rgb.getWidth() * scale));
int newH = Math.max(1, (int) Math.round(rgb.getHeight() * scale));
BufferedImage resized = new BufferedImage(targetW, targetH, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = resized.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.setColor(Color.WHITE);
g2.fillRect(0, 0, targetW, targetH);
int x = (targetW - newW) / 2;
int y = (targetH - newH) / 2;
g2.drawImage(rgb, x, y, newW, newH, null);
g2.dispose();
// 保存为临时 PNG 文件(确保无压缩失真)
File tmp = Files.createTempFile("anime_pre_", ".png").toFile();
ImageIO.write(resized, "png", tmp);
return tmp;
}
// ========== 新增:尝试通过反射从 segmenter 上读取模型输入尺寸 ==========
private int[] getModelInputSize() {
// 默认值
int defaultSize = 512;
int w = defaultSize, h = defaultSize;
try {
Class<?> cls = segmenter.getClass();
// 尝试方法 getInputWidth/getInputHeight
try {
Method mw = cls.getMethod("getInputWidth");
Method mh = cls.getMethod("getInputHeight");
Object ow = mw.invoke(segmenter);
Object oh = mh.invoke(segmenter);
if (ow instanceof Number && oh instanceof Number) {
int iw = ((Number) ow).intValue();
int ih = ((Number) oh).intValue();
if (iw > 0 && ih > 0) {
return new int[]{iw, ih};
}
}
} catch (NoSuchMethodException ignored) {}
// 尝试方法 getInputSize 返回 int[] 或 Dimension
try {
Method ms = cls.getMethod("getInputSize");
Object os = ms.invoke(segmenter);
if (os instanceof int[] && ((int[]) os).length >= 2) {
int iw = ((int[]) os)[0];
int ih = ((int[]) os)[1];
if (iw > 0 && ih > 0) return new int[]{iw, ih};
} else if (os != null) {
// 处理 java.awt.Dimension
try {
Method gw = os.getClass().getMethod("getWidth");
Method gh = os.getClass().getMethod("getHeight");
Object ow2 = gw.invoke(os);
Object oh2 = gh.invoke(os);
if (ow2 instanceof Number && oh2 instanceof Number) {
int iw = ((Number) ow2).intValue();
int ih = ((Number) oh2).intValue();
if (iw > 0 && ih > 0) return new int[]{iw, ih};
}
} catch (Exception ignored2) {}
}
} catch (NoSuchMethodException ignored) {}
// 尝试字段 inputWidth/inputHeight
try {
try {
java.lang.reflect.Field fw = cls.getDeclaredField("inputWidth");
java.lang.reflect.Field fh = cls.getDeclaredField("inputHeight");
fw.setAccessible(true); fh.setAccessible(true);
Object ow = fw.get(segmenter);
Object oh = fh.get(segmenter);
if (ow instanceof Number && oh instanceof Number) {
int iw = ((Number) ow).intValue();
int ih = ((Number) oh).intValue();
if (iw > 0 && ih > 0) return new int[]{iw, ih};
}
} catch (NoSuchFieldException ignoredField) {}
} catch (Exception ignored) {}
} catch (Exception ignored) {
// 任何反射异常都回退到默认值
}
return new int[]{w, h};
}
}

View File

@@ -0,0 +1,64 @@
package com.chuangzhou.vivid2D.ai.anime_face_segmentation;
import com.chuangzhou.vivid2D.ai.SegmentationResult;
import java.awt.image.BufferedImage;
import java.util.Map;
/**
* 动漫分割结果容器
*/
public class AnimeSegmentationResult extends SegmentationResult {
// 分割掩码图(每个像素的颜色为对应类别颜色)
private final BufferedImage maskImage;
// 分割概率图(每个像素的类别概率分布)
private final float[][][] probabilityMap;
// 类别索引 -> 类别名称
private final Map<Integer, String> labels;
// 类别名称 -> ARGB 颜色
private final Map<String, Integer> palette;
public AnimeSegmentationResult(BufferedImage maskImage, float[][][] probabilityMap,
Map<Integer, String> labels, Map<String, Integer> palette) {
super(maskImage, labels, palette);
this.maskImage = maskImage;
this.probabilityMap = probabilityMap;
this.labels = labels;
this.palette = palette;
}
public BufferedImage getMaskImage() {
return maskImage;
}
public float[][][] getProbabilityMap() {
return probabilityMap;
}
public Map<Integer, String> getLabels() {
return labels;
}
public Map<String, Integer> getPalette() {
return palette;
}
/**
* 获取指定类别的概率图
*/
public float[][] getClassProbability(int classIndex) {
if (probabilityMap == null) return null;
int height = probabilityMap.length;
int width = probabilityMap[0].length;
float[][] result = new float[height][width];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
result[y][x] = probabilityMap[y][x][classIndex];
}
}
return result;
}
}

View File

@@ -0,0 +1,214 @@
package com.chuangzhou.vivid2D.ai.anime_face_segmentation;
import ai.djl.MalformedModelException;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
import ai.djl.ndarray.NDManager;
import ai.djl.ndarray.types.DataType;
import ai.djl.ndarray.types.Shape;
import ai.djl.repository.zoo.Criteria;
import ai.djl.repository.zoo.ModelNotFoundException;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.translate.Batchifier;
import ai.djl.translate.TranslateException;
import ai.djl.translate.Translator;
import ai.djl.translate.TranslatorContext;
import com.chuangzhou.vivid2D.ai.Segmenter;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
/**
* AnimeSegmenter: 专门为 Anime-Face-Segmentation UNet 模型设计的分割器
*/
public class AnimeSegmenter extends Segmenter {
// 模型默认输入大小(与训练时一致)。若模型不同可以修改为实际值或让 caller 通过构造参数传入。
private static final int MODEL_INPUT_W = 512;
private static final int MODEL_INPUT_H = 512;
// 内部类用于从Translator安全地传出数据
public static class SegmentationData {
final int[] indices; // 类别索引 [H * W]
final float[][][] probMap; // 概率图 [H][W][C]
final long[] shape; // 形状 [H, W]
public SegmentationData(int[] indices, float[][][] probMap, long[] shape) {
this.indices = indices;
this.probMap = probMap;
this.shape = shape;
}
}
private final ZooModel<Image, SegmentationData> modelWrapper;
private final Predictor<Image, SegmentationData> predictor;
private final Map<String, Integer> palette;
public AnimeSegmenter(Path modelDir, List<String> labels) throws IOException, MalformedModelException, ModelNotFoundException {
super(modelDir, labels);
this.palette = AnimeLabelPalette.defaultPalette();
Translator<Image, SegmentationData> translator = new Translator<Image, SegmentationData>() {
@Override
public NDList processInput(TranslatorContext ctx, Image input) {
NDManager manager = ctx.getNDManager();
// 如果图片已经是模型输入大小则不再 resize避免重复缩放导致失真
Image toUse = input;
if (!(input.getWidth() == MODEL_INPUT_W && input.getHeight() == MODEL_INPUT_H)) {
toUse = input.resize(MODEL_INPUT_W, MODEL_INPUT_H, true);
}
// 转换为NDArray并预处理
NDArray array = toUse.toNDArray(manager);
// DJL 返回 HWC 格式数组,转换为 CHW并标准化到 [0,1]
array = array.transpose(2, 0, 1) // HWC -> CHW
.toType(DataType.FLOAT32, false)
.div(255f) // 归一化到[0,1]
.expandDims(0); // 添加batch维度 [1,3,H,W]
return new NDList(array);
}
@Override
public SegmentationData processOutput(TranslatorContext ctx, NDList list) {
if (list == null || list.isEmpty()) {
throw new IllegalStateException("Model did not return any output.");
}
NDArray output = list.get(0); // 期望形状 [1,C,H,W] 或 [1,C,W,H](以训练时一致为准)
// 确保维度:把 output 视作 [1, C, H, W]
Shape outShape = output.getShape();
if (outShape.dimension() != 4) {
throw new IllegalStateException("Unexpected output shape: " + outShape);
}
// 1. 获取类别索引argmax -> [H, W]
NDArray squeezed = output.squeeze(0); // [C,H,W]
NDArray classMap = squeezed.argMax(0).toType(DataType.INT32, false); // argMax over channel维度
// 2. 获取概率图softmax 输出或模型已经输出概率),转换为 [H,W,C]
NDArray probabilities = squeezed.transpose(1, 2, 0) // [H,W,C]
.toType(DataType.FLOAT32, false);
// 3. 转换为Java数组
long[] shape = classMap.getShape().getShape(); // [H, W]
int[] indices = classMap.toIntArray();
long[] probShape = probabilities.getShape().getShape(); // [H, W, C]
int height = (int) probShape[0];
int width = (int) probShape[1];
int classes = (int) probShape[2];
float[] flatProbMap = probabilities.toFloatArray();
float[][][] probMap = new float[height][width][classes];
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
for (int k = 0; k < classes; k++) {
int index = i * width * classes + j * classes + k;
probMap[i][j][k] = flatProbMap[index];
}
}
}
return new SegmentationData(indices, probMap, shape);
}
@Override
public Batchifier getBatchifier() {
return null;
}
};
Criteria<Image, SegmentationData> criteria = Criteria.builder()
.setTypes(Image.class, SegmentationData.class)
.optModelPath(modelDir)
.optEngine("PyTorch")
.optTranslator(translator)
.build();
this.modelWrapper = criteria.loadModel();
this.predictor = modelWrapper.newPredictor();
}
@Override
public NDList processInput(TranslatorContext ctx, Image input) {
return null;
}
@Override
public Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list) {
return null;
}
public AnimeSegmentationResult segment(File imgFile) throws TranslateException, IOException {
Image img = ImageFactory.getInstance().fromFile(imgFile.toPath());
// 预测并获取分割数据
SegmentationData data = predictor.predict(img);
long[] shp = data.shape;
int[] indices = data.indices;
float[][][] probMap = data.probMap;
int height = (int) shp[0];
int width = (int) shp[1];
// 创建掩码图像
BufferedImage mask = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Map<Integer, String> labelsMap = AnimeLabelPalette.getIndexToLabelMap();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = indices[y * width + x];
String label = labelsMap.getOrDefault(idx, "unknown");
int argb = palette.getOrDefault(label, 0xFF00FF00); // 默认绿色
mask.setRGB(x, y, argb);
}
}
return new AnimeSegmentationResult(mask, probMap, labelsMap, palette);
}
/**
* 专门针对眼睛的分割方法
*/
public BufferedImage extractEyes(File imgFile) throws TranslateException, IOException {
AnimeSegmentationResult result = segment(imgFile);
BufferedImage mask = result.getMaskImage();
BufferedImage eyeMask = new BufferedImage(mask.getWidth(), mask.getHeight(), BufferedImage.TYPE_INT_ARGB);
int eyeColor = palette.get("eye");
for (int y = 0; y < mask.getHeight(); y++) {
for (int x = 0; x < mask.getWidth(); x++) {
int rgb = mask.getRGB(x, y);
if (rgb == eyeColor) {
eyeMask.setRGB(x, y, eyeColor);
} else {
eyeMask.setRGB(x, y, 0x00000000); // 透明
}
}
}
return eyeMask;
}
@Override
public void close() {
try {
predictor.close();
} catch (Exception ignore) {
}
try {
modelWrapper.close();
} catch (Exception ignore) {
}
}
}

View File

@@ -0,0 +1,46 @@
package com.chuangzhou.vivid2D.ai.anime_segmentation;
import java.util.*;
/**
* 动漫分割模型的标签和颜色调色板。
* 这是一个二分类模型:背景和前景(动漫人物)
*/
public class Anime2LabelPalette {
/**
* 动漫分割模型的标准标签2个类别
*/
public static List<String> defaultLabels() {
return Arrays.asList(
"background", // 0
"foreground" // 1
);
}
/**
* 返回动漫分割模型的调色板
*/
public static Map<String, Integer> defaultPalette() {
Map<String, Integer> map = new HashMap<>();
// 索引 0: background - 黑色
map.put("background", 0xFF000000);
// 索引 1: foreground - 白色
map.put("foreground", 0xFFFFFFFF);
return map;
}
/**
* 专门为动漫分割模型设计的调色板(可视化更友好)
*/
public static Map<String, Integer> animeSegmentationPalette() {
Map<String, Integer> map = new HashMap<>();
// 背景 - 透明
map.put("background", 0x00000000);
// 前景 - 红色(用于可视化)
map.put("foreground", 0xFFFF0000);
return map;
}
}

View File

@@ -0,0 +1,15 @@
package com.chuangzhou.vivid2D.ai.anime_segmentation;
import com.chuangzhou.vivid2D.ai.SegmentationResult;
import java.awt.image.BufferedImage;
import java.util.Map;
/**
* 动漫分割结果容器
*/
public class Anime2SegmentationResult extends SegmentationResult {
public Anime2SegmentationResult(BufferedImage maskImage, Map<Integer, String> labels, Map<String, Integer> palette) {
super(maskImage, labels, palette);
}
}

View File

@@ -0,0 +1,106 @@
package com.chuangzhou.vivid2D.ai.anime_segmentation;
import ai.djl.MalformedModelException;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
import ai.djl.ndarray.NDManager;
import ai.djl.ndarray.types.DataType;
import ai.djl.repository.zoo.ModelNotFoundException;
import ai.djl.translate.TranslateException;
import ai.djl.translate.TranslatorContext;
import com.chuangzhou.vivid2D.ai.SegmentationResult;
import com.chuangzhou.vivid2D.ai.Segmenter;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
/**
* Anime2Segmenter: 专门用于动漫分割模型
* 处理 anime-segmentation 模型的二值分割输出
*/
public class Anime2Segmenter extends Segmenter {
public Anime2Segmenter(Path modelDir, List<String> labels) throws IOException, MalformedModelException, ModelNotFoundException {
super(modelDir, labels);
}
@Override
public NDList processInput(TranslatorContext ctx, Image input) {
NDManager manager = ctx.getNDManager();
// 调整输入图像尺寸到模型期望的大小 (1024x1024)
Image resized = input.resize(1024, 1024, true);
NDArray array = resized.toNDArray(manager);
// 转换为 CHW 格式并归一化
array = array.transpose(2, 0, 1).toType(DataType.FLOAT32, false);
array = array.div(255f);
array = array.expandDims(0); // 添加batch维度
return new NDList(array);
}
@Override
public SegmentationData processOutput(TranslatorContext ctx, NDList list) {
if (list == null || list.isEmpty()) {
throw new IllegalStateException("Model did not return any output.");
}
NDArray out = list.get(0);
// 动漫分割模型输出形状: [1, 1, H, W] - 单通道概率图
// 应用sigmoid并二值化
NDArray probabilities = out.div(out.neg().exp().add(1));
NDArray binaryMask = probabilities.gt(0.5).toType(DataType.INT32, false);
if (binaryMask.getShape().dimension() == 4) {
binaryMask = binaryMask.squeeze(0).squeeze(0);
}
long[] finalShape = binaryMask.getShape().getShape();
int[] indices = binaryMask.toIntArray();
return new SegmentationData(indices, finalShape);
}
@Override
public SegmentationResult segment(File imgFile) throws TranslateException, IOException {
Image img = ImageFactory.getInstance().fromFile(imgFile.toPath());
Segmenter.SegmentationData data = predictor.predict(img);
long[] shp = data.shape;
int[] indices = data.indices;
int height, width;
if (shp.length == 2) {
height = (int) shp[0];
width = (int) shp[1];
} else {
throw new RuntimeException("Unexpected classMap shape from SegmentationData: " + Arrays.toString(shp));
}
BufferedImage mask = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Map<Integer, String> labelsMap = new HashMap<>();
for (int i = 0; i < labels.size(); i++) {
labelsMap.put(i, labels.get(i));
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = indices[y * width + x];
String label = labelsMap.getOrDefault(idx, "unknown");
int argb = palette.getOrDefault(label, 0xFFFF0000);
mask.setRGB(x, y, argb);
}
}
return new SegmentationResult(mask, labelsMap, palette);
}
@Override
public void close() {
try {
predictor.close();
} catch (Exception ignore) {
}
try {
modelWrapper.close();
} catch (Exception ignore) {
}
}
}

View File

@@ -0,0 +1,154 @@
package com.chuangzhou.vivid2D.ai.anime_segmentation;
import com.chuangzhou.vivid2D.ai.SegmentationResult;
import com.chuangzhou.vivid2D.ai.VividModelWrapper;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.List;
/**
* Anime2VividModelWrapper - 对之前 Anime2Segmenter 的封装提供更便捷的API
* <p>
* 用法示例:
* Anime2VividModelWrapper wrapper = Anime2VividModelWrapper.load(Paths.get("/path/to/modelDir"));
* Map<String, Anime2VividModelWrapper.ResultFiles> out = wrapper.segmentAndSave(
* new File("input.jpg"),
* Set.of("foreground"), // 动漫分割主要关注前景
* Paths.get("outDir")
* );
* // out contains 每个目标标签对应的 mask+overlay 文件路径
* wrapper.close();
*/
public class Anime2VividModelWrapper extends VividModelWrapper<Anime2Segmenter> {
private Anime2VividModelWrapper(Anime2Segmenter segmenter, List<String> labels, Map<String, Integer> palette) {
super(segmenter, labels, palette);
}
/**
* 读取 modelDir/synset.txt每行一个标签若不存在则使用 Anime2LabelPalette.defaultLabels()
* 并创建 Anime2Segmenter 实例。
*/
public static Anime2VividModelWrapper load(Path modelDir) throws Exception {
List<String> labels = loadLabelsFromSynset(modelDir).orElseGet(Anime2LabelPalette::defaultLabels);
Anime2Segmenter s = new Anime2Segmenter(modelDir, labels);
Map<String, Integer> palette = Anime2LabelPalette.animeSegmentationPalette();
return new Anime2VividModelWrapper(s, labels, palette);
}
public List<String> getLabels() {
return Collections.unmodifiableList(labels);
}
public Map<String, Integer> getPalette() {
return Collections.unmodifiableMap(palette);
}
/**
* 直接返回分割结果Anime2SegmentationResult
*/
public SegmentationResult segment(File inputImage) throws Exception {
return segmenter.segment(inputImage);
}
/**
* 把指定 targets标签名集合从输入图片中分割并保存到 outDir。
* 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。
* <p>
* 返回值Map<labelName, ResultFiles>ResultFiles 包含 maskFile、overlayFile两个 PNG
*/
public Map<String, VividModelWrapper.ResultFiles> segmentAndSave(File inputImage, Set<String> targets, Path outDir) throws Exception {
if (!Files.exists(outDir)) {
Files.createDirectories(outDir);
}
SegmentationResult res = segment(inputImage);
BufferedImage original = ImageIO.read(inputImage);
BufferedImage maskImage = res.getMaskImage();
int maskW = maskImage.getWidth();
int maskH = maskImage.getHeight();
// 解析 targets
Set<String> realTargets = parseTargetsSet(targets);
Map<String, ResultFiles> saved = new LinkedHashMap<>();
for (String target : realTargets) {
if (!palette.containsKey(target)) {
String finalTarget = target;
Optional<String> matched = palette.keySet().stream()
.filter(k -> k.equalsIgnoreCase(finalTarget))
.findFirst();
if (matched.isPresent()) target = matched.get();
else {
System.err.println("Warning: unknown label '" + target + "' - skip.");
continue;
}
}
int targetColor = palette.get(target);
// 1) 生成透明背景的二值掩码(只保留 target 像素)
BufferedImage partMask = new BufferedImage(maskW, maskH, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < maskH; y++) {
for (int x = 0; x < maskW; x++) {
int c = maskImage.getRGB(x, y);
if (c == targetColor) {
partMask.setRGB(x, y, targetColor | 0xFF000000); // 保证不透明
} else {
partMask.setRGB(x, y, 0x00000000);
}
}
}
// 2) 将 mask 缩放到与原图一致(如果需要),并生成 overlay半透明
BufferedImage maskResized = partMask;
if (original.getWidth() != maskW || original.getHeight() != maskH) {
maskResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = maskResized.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(partMask, 0, 0, original.getWidth(), original.getHeight(), null);
g.dispose();
}
BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = overlay.createGraphics();
g2.drawImage(original, 0, 0, null);
// 半透明颜色alpha = 0x88
int rgbOnly = (targetColor & 0x00FFFFFF);
int translucent = (0x88 << 24) | rgbOnly;
BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < colorOverlay.getHeight(); y++) {
for (int x = 0; x < colorOverlay.getWidth(); x++) {
int mc = maskResized.getRGB(x, y);
if ((mc & 0x00FFFFFF) == (targetColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) {
colorOverlay.setRGB(x, y, translucent);
} else {
colorOverlay.setRGB(x, y, 0x00000000);
}
}
}
g2.drawImage(colorOverlay, 0, 0, null);
g2.dispose();
// 保存
String safe = safeFileName(target);
File maskOut = outDir.resolve(safe + "_mask.png").toFile();
File overlayOut = outDir.resolve(safe + "_overlay.png").toFile();
ImageIO.write(maskResized, "png", maskOut);
ImageIO.write(overlay, "png", overlayOut);
saved.put(target, new ResultFiles(maskOut, overlayOut));
}
return saved;
}
}

View File

@@ -0,0 +1,89 @@
package com.chuangzhou.vivid2D.ai.face_parsing;
import java.util.*;
/**
* BiSeNet 人脸解析模型的标准标签和颜色调色板。
* 颜色值基于 zllrunning/face-parsing.PyTorch 仓库的 test.py 文件。
* 标签索引必须与模型输出索引一致0-18
*/
public class BiSeNetLabelPalette {
/**
* BiSeNet 人脸解析模型的标准标签19个类别索引 0-18
*/
public static List<String> defaultLabels() {
return Arrays.asList(
"background", // 0
"skin", // 1
"nose", // 2
"eye_left", // 3
"eye_right", // 4
"eyebrow_left", // 5
"eyebrow_right",// 6
"ear_left", // 7
"ear_right", // 8
"mouth", // 9
"lip_upper", // 10
"lip_lower", // 11
"hair", // 12
"hat", // 13
"earring", // 14
"necklace", // 15
"clothes", // 16
"facial_hair",// 17
"neck" // 18
);
}
/**
* 返回一个对应的调色板:类别名 -> ARGB 颜色值。
* 颜色值基于 test.py 中 part_colors 数组的 RGB 值转换为 ARGB 格式 (0xFFRRGGBB)。
*/
public static Map<String, Integer> defaultPalette() {
Map<String, Integer> map = new HashMap<>();
// 索引 0: background
map.put("background", 0xFF000000); // 黑色
// 索引 1-18: 对应 part_colors 数组的前 18 个颜色
// 注意:这里假设 part_colors[i-1] 对应 索引 i 的标签。
// 索引 1: skin -> [255, 0, 0]
map.put("skin", 0xFFFF0000);
// 索引 2: nose -> [255, 85, 0]
map.put("nose", 0xFFFF5500);
// 索引 3: eye_left -> [255, 170, 0]
map.put("eye_left", 0xFFFFAA00);
// 索引 4: eye_right -> [255, 0, 85]
map.put("eye_right", 0xFFFF0055);
// 索引 5: eyebrow_left -> [255, 0, 170]
map.put("eyebrow_left",0xFFFF00AA);
// 索引 6: eyebrow_right -> [0, 255, 0]
map.put("eyebrow_right",0xFF00FF00);
// 索引 7: ear_left -> [85, 255, 0]
map.put("ear_left", 0xFF55FF00);
// 索引 8: ear_right -> [170, 255, 0]
map.put("ear_right", 0xFFAAFF00);
// 索引 9: mouth -> [0, 255, 85]
map.put("mouth", 0xFF00FF55);
// 索引 10: lip_upper -> [0, 255, 170]
map.put("lip_upper", 0xFF00FFAA);
// 索引 11: lip_lower -> [0, 0, 255]
map.put("lip_lower", 0xFF0000FF);
// 索引 12: hair -> [85, 0, 255]
map.put("hair", 0xFF5500FF);
// 索引 13: hat -> [170, 0, 255]
map.put("hat", 0xFFAA00FF);
// 索引 14: earring -> [0, 85, 255]
map.put("earring", 0xFF0055FF);
// 索引 15: necklace -> [0, 170, 255]
map.put("necklace", 0xFF00AAFF);
// 索引 16: clothes -> [255, 255, 0]
map.put("clothes", 0xFFFFFF00);
// 索引 17: facial_hair -> [255, 85, 85]
map.put("facial_hair", 0xFFFF5555);
// 索引 18: neck -> [255, 170, 170]
map.put("neck", 0xFFFFAAAA);
return map;
}
}

View File

@@ -0,0 +1,15 @@
package com.chuangzhou.vivid2D.ai.face_parsing;
import com.chuangzhou.vivid2D.ai.SegmentationResult;
import java.awt.image.BufferedImage;
import java.util.Map;
/**
* 分割结果容器
*/
public class BiSeNetSegmentationResult extends SegmentationResult {
public BiSeNetSegmentationResult(BufferedImage maskImage, Map<Integer, String> labels, Map<String, Integer> palette) {
super(maskImage, labels, palette);
}
}

View File

@@ -0,0 +1,101 @@
package com.chuangzhou.vivid2D.ai.face_parsing;
import ai.djl.MalformedModelException;
import ai.djl.inference.Predictor;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
import ai.djl.ndarray.NDManager;
import ai.djl.ndarray.types.DataType;
import ai.djl.repository.zoo.Criteria;
import ai.djl.repository.zoo.ModelNotFoundException;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.translate.Batchifier;
import ai.djl.translate.TranslateException;
import ai.djl.translate.Translator;
import ai.djl.translate.TranslatorContext;
import com.chuangzhou.vivid2D.ai.SegmentationResult;
import com.chuangzhou.vivid2D.ai.Segmenter;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
/**
* Segmenter: 加载模型并对图片做语义分割
*
* 说明:
* - Translator.processOutput 在翻译器层就把模型输出处理成 (H, W) 的类别索引 NDArray
* 并把该 NDArray 拷贝到 persistentManager 中返回,从而避免后续 native 资源被释放的问题。
* - 这里改为在 Translator 内部把 classMap 转为 Java int[](通过 classMap.toIntArray()
* 再用 persistentManager.create(int[], shape) 创建新的 NDArray 返回,确保安全。
*/
public class BiSeNetSegmenter extends Segmenter {
public BiSeNetSegmenter(Path modelDir, List<String> labels) throws IOException, MalformedModelException, ModelNotFoundException {
super(modelDir, labels);
}
@Override
public NDList processInput(TranslatorContext ctx, Image input) {
NDManager manager = ctx.getNDManager();
NDArray array = input.toNDArray(manager);
array = array.transpose(2, 0, 1).toType(DataType.FLOAT32, false);
array = array.div(255f);
array = array.expandDims(0);
return new NDList(array);
}
@Override
public Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list) {
if (list == null || list.isEmpty()) {
throw new IllegalStateException("Model did not return any output.");
}
NDArray out = list.get(0);
NDArray classMap;
// 1. 解析模型输出,得到类别图谱 (classMap)
long[] shape = out.getShape().getShape();
if (shape.length == 4 && shape[1] > 1) {
classMap = out.argMax(1);
} else if (shape.length == 3) {
classMap = (shape[0] == 1) ? out : out.argMax(0);
} else if (shape.length == 2) {
classMap = out;
} else {
throw new IllegalStateException("Unexpected output shape: " + Arrays.toString(shape));
}
if (classMap.getShape().dimension() == 3) {
classMap = classMap.squeeze(0);
}
// 2. *** 关键步骤 ***
// 在 NDArray 仍然有效的上下文中,将其转换为 Java 原生类型
// 首先,确保数据类型是 INT32
NDArray int32ClassMap = classMap.toType(DataType.INT32, false);
// 然后,获取形状和 int[] 数组
long[] finalShape = int32ClassMap.getShape().getShape();
int[] indices = int32ClassMap.toIntArray();
// 3. 将 Java 对象封装并返回
return new SegmentationData(indices, finalShape);
}
@Override
public SegmentationResult segment(File imgFile) throws TranslateException, IOException {
return super.segment(imgFile);
}
@Override
public void close() {
super.close();
}
}

View File

@@ -0,0 +1,147 @@
package com.chuangzhou.vivid2D.ai.face_parsing;
import com.chuangzhou.vivid2D.ai.SegmentationResult;
import com.chuangzhou.vivid2D.ai.VividModelWrapper;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.List;
/**
* VividModelWrapper - 对之前 Segmenter / SegmenterExample 的封装
*
* 用法示例:
* VividModelWrapper wrapper = VividModelWrapper.load(Paths.get("/path/to/modelDir"));
* Map<String, VividModelWrapper.ResultFiles> out = wrapper.segmentAndSave(
* new File("input.jpg"),
* Set.of("eye","face"), // 或 Set.of(all labels...);若想全部传 "all" 可以用 helper parseTargets
* Paths.get("outDir")
* );
* // out contains 每个目标标签对应的 mask+overlay 文件路径
* wrapper.close();
*/
public class BiSeNetVividModelWrapper extends VividModelWrapper<BiSeNetSegmenter> {
private BiSeNetVividModelWrapper(BiSeNetSegmenter segmenter, List<String> labels, Map<String, Integer> palette) {
super(segmenter, labels, palette);
}
/**
* 读取 modelDir/synset.txt每行一个标签若不存在则使用 LabelPalette.defaultLabels()
* 并创建 Segmenter 实例。
*/
public static BiSeNetVividModelWrapper load(Path modelDir) throws Exception {
List<String> labels = loadLabelsFromSynset(modelDir).orElseGet(BiSeNetLabelPalette::defaultLabels);
BiSeNetSegmenter s = new BiSeNetSegmenter(modelDir, labels);
Map<String, Integer> palette = BiSeNetLabelPalette.defaultPalette();
return new BiSeNetVividModelWrapper(s, labels, palette);
}
public List<String> getLabels() {
return super.getLabels();
}
public Map<String, Integer> getPalette() {
return super.getPalette();
}
/**
* 直接返回分割结果SegmentationResult
*/
public SegmentationResult segment(File inputImage) throws Exception {
return segmenter.segment(inputImage);
}
/**
* 把指定 targets标签名集合从输入图片中分割并保存到 outDir。
* 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。
* <p>
* 返回值Map<labelName, ResultFiles>ResultFiles 包含 maskFile、overlayFile两个 PNG
*/
public Map<String, VividModelWrapper.ResultFiles> segmentAndSave(File inputImage, Set<String> targets, Path outDir) throws Exception {
if (!Files.exists(outDir)) {
Files.createDirectories(outDir);
}
SegmentationResult res = segment(inputImage);
BufferedImage original = ImageIO.read(inputImage);
BufferedImage maskImage = res.getMaskImage();
int maskW = maskImage.getWidth();
int maskH = maskImage.getHeight();
Set<String> realTargets = parseTargetsSet(targets);
Map<String, ResultFiles> saved = new LinkedHashMap<>();
for (String target : realTargets) {
if (!palette.containsKey(target)) {
String finalTarget = target;
Optional<String> matched = palette.keySet().stream()
.filter(k -> k.equalsIgnoreCase(finalTarget))
.findFirst();
if (matched.isPresent()) target = matched.get();
else {
System.err.println("Warning: unknown label '" + target + "' - skip.");
continue;
}
}
int targetColor = palette.get(target);
BufferedImage partMask = new BufferedImage(maskW, maskH, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < maskH; y++) {
for (int x = 0; x < maskW; x++) {
int c = maskImage.getRGB(x, y);
if (c == targetColor) {
partMask.setRGB(x, y, targetColor | 0xFF000000); // 保证不透明
} else {
partMask.setRGB(x, y, 0x00000000);
}
}
}
BufferedImage maskResized = partMask;
if (original.getWidth() != maskW || original.getHeight() != maskH) {
maskResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = maskResized.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(partMask, 0, 0, original.getWidth(), original.getHeight(), null);
g.dispose();
}
BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = overlay.createGraphics();
g2.drawImage(original, 0, 0, null);
int rgbOnly = (targetColor & 0x00FFFFFF);
int translucent = (0x88 << 24) | rgbOnly;
BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < colorOverlay.getHeight(); y++) {
for (int x = 0; x < colorOverlay.getWidth(); x++) {
int mc = maskResized.getRGB(x, y);
if ((mc & 0x00FFFFFF) == (targetColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) {
colorOverlay.setRGB(x, y, translucent);
} else {
colorOverlay.setRGB(x, y, 0x00000000);
}
}
}
g2.drawImage(colorOverlay, 0, 0, null);
g2.dispose();
String safe = safeFileName(target);
File maskOut = outDir.resolve(safe + "_mask.png").toFile();
File overlayOut = outDir.resolve(safe + "_overlay.png").toFile();
ImageIO.write(maskResized, "png", maskOut);
ImageIO.write(overlay, "png", overlayOut);
saved.put(target, new ResultFiles(maskOut, overlayOut));
}
return saved;
}
/**
* 关闭底层资源
*/
@Override
public void close() {
try {
segmenter.close();
} catch (Exception ignore) {}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,336 @@
package com.chuangzhou.vivid2D.render;
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator;
import org.joml.Vector2f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL11;
/**
* 现代化选择框渲染器(性能优化版)
* 主要优化点:
* 1) 复用 Tesselator 单例 BufferBuilder减少频繁的 GPU 资源创建/销毁
* 2) 批量提交顶点:把同一 primitiveLINES / TRIANGLES / LINE_LOOP与同一颜色的顶点尽量合并到一次 begin/end
* 3) 手柄使用实心矩形(两三角形)批量绘制,保持美观且高效
* 4) 增加轻微外发光(透明大边框)和阴影感以达到“现代”外观
* <p>
* 注意:本类依赖你工程中已有的 RenderSystem/Tesselator/BufferBuilder/BufferUploader 实现。
*/
public class MultiSelectionBoxRenderer {
// 常量定义(视觉可调)
public static final float DEFAULT_CORNER_SIZE = 10.0f;
public static final float DEFAULT_BORDER_THICKNESS = 6.0f;
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); // 手柄白
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 void drawSelectBox(BoundingBox bounds, Vector2f pivot) {
if (!bounds.isValid()) return;
float minX = bounds.getMinX();
float minY = bounds.getMinY();
float maxX = bounds.getMaxX();
float maxY = bounds.getMaxY();
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();
// 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();
// 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();
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();
// 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 + 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();
// 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();
// 圆环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();
}
/**
* 绘制多选框(现代化外观,批量提交)
*/
public static void drawMultiSelectionBox(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);
// 虚线边框 - 将所有虚线段放在同一个 GL_LINES 批次
int estimatedSegments = Math.max(4,
(int) Math.ceil((2f * ((maxX - minX) + (maxY - minY))) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH)));
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);
tesselator.end();
// 手柄(一次性 TRIANGLES 批次)
bb.begin(RenderSystem.GL_TRIANGLES, 6 * 8);
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();
// 中心点
Vector2f center = new Vector2f((minX + maxX) / 2f, (minY + maxY) / 2f);
bb.begin(GL11.GL_LINES, 4);
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();
// 旋转手柄(沿用单选逻辑)
drawMultiSelectionRotationHandle(bb, minX, minY, maxX, maxY);
}
// ------ 辅助顶点生成方法(批量写入当前 begin() 的 BufferBuilder ------
// 向当前 TRIANGLES 批次添加一个填充矩形(两三角形)
private static void addFilledQuadTriangles(BufferBuilder bb, float x0, float y0, float x1, float y1) {
// 三角形 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
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);
}
// 向当前 LINES 批次添加一段虚线(将多个线段顶点 push 到当前 begin()
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;
float dirX = dx / len, dirY = dy / len;
float seg = dashLen + gapLen;
int count = (int) Math.ceil(len / seg);
for (int i = 0; i < count; i++) {
float s = i * seg;
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();
}
}

View File

@@ -0,0 +1,336 @@
package com.chuangzhou.vivid2D.render;
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.Vector4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL33;
import org.lwjgl.stb.STBTTAlignedQuad;
import org.lwjgl.stb.STBTTBakedChar;
import org.lwjgl.system.MemoryStack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import static org.lwjgl.stb.STBTruetype.stbtt_BakeFontBitmap;
import static org.lwjgl.stb.STBTruetype.stbtt_GetBakedQuad;
/**
* 支持 ASCII + 中文的 OpenGL 文本渲染器
*
* @author tzdwindows 7
*/
public final class TextRenderer {
private static final Logger logger = LoggerFactory.getLogger(TextRenderer.class);
private final int bitmapWidth;
private final int bitmapHeight;
private final int firstChar;
private final int charCount;
private STBTTBakedChar.Buffer asciiCharData;
private STBTTBakedChar.Buffer chineseCharData;
private int asciiTextureId;
private int chineseTextureId;
private boolean initialized = false;
// 中文字符起始编码(选择一个不冲突的范围)
private static final int CHINESE_FIRST_CHAR = 0x4E00; // CJK Unified Ideographs 常用汉字起始范围
private static final int CHINESE_CHAR_COUNT = 20000;
public TextRenderer(int bitmapWidth, int bitmapHeight, int firstChar, int charCount) {
this.bitmapWidth = bitmapWidth;
this.bitmapHeight = bitmapHeight;
this.firstChar = firstChar;
this.charCount = charCount;
}
/**
* 初始化字体渲染器
*/
public void initialize(ByteBuffer fontData, float fontHeight) {
if (initialized) {
logger.warn("TextRenderer already initialized");
return;
}
if (fontData == null || fontData.capacity() == 0 || fontHeight <= 0) {
logger.error("Invalid font data or font height");
return;
}
ShaderProgram shader = ShaderManagement.getShaderProgram("TextShader");
if (shader == null) {
logger.error("TextShader not found");
return;
}
shader.use();
try {
// 烘焙 ASCII
asciiCharData = STBTTBakedChar.malloc(charCount);
ByteBuffer asciiBitmap = ByteBuffer.allocateDirect(bitmapWidth * bitmapHeight);
int asciiRes = stbtt_BakeFontBitmap(fontData, fontHeight, asciiBitmap,
bitmapWidth, bitmapHeight, firstChar, asciiCharData);
if (asciiRes <= 0) {
logger.error("ASCII font bake failed, result: {}", asciiRes);
cleanup();
return;
}
asciiTextureId = createTextureFromBitmap(bitmapWidth, bitmapHeight, asciiBitmap);
if (asciiTextureId == 0) {
logger.error("Failed to create ASCII texture");
cleanup();
return;
}
// 烘焙中文 - 使用更大的纹理和正确的字符范围
int chineseTexSize = 4096; // 中文字符需要更大的纹理
// 分配足够的空间来存储 CHINESE_CHAR_COUNT 个字符的数据
chineseCharData = STBTTBakedChar.malloc(CHINESE_CHAR_COUNT);
ByteBuffer chineseBitmap = ByteBuffer.allocateDirect(chineseTexSize * chineseTexSize);
// 关键:烘焙从 CHINESE_FIRST_CHAR 开始的 CHINESE_CHAR_COUNT 个连续字符
int chineseRes = stbtt_BakeFontBitmap(fontData, fontHeight, chineseBitmap,
chineseTexSize, chineseTexSize, CHINESE_FIRST_CHAR, chineseCharData);
if (chineseRes <= 0) {
logger.error("Chinese font bake failed, result: {}", chineseRes);
cleanup();
return;
}
chineseTextureId = createTextureFromBitmap(chineseTexSize, chineseTexSize, chineseBitmap);
if (chineseTextureId == 0) {
logger.error("Failed to create Chinese texture");
cleanup();
return;
}
initialized = true;
logger.debug("TextRenderer initialized, ASCII tex={}, Chinese tex={}", asciiTextureId, chineseTextureId);
} catch (Exception e) {
logger.error("Exception during TextRenderer init: {}", e.getMessage(), e);
cleanup();
} finally {
shader.stop();
}
}
/**
* 渲染文字
*/
public void renderText(String text, float x, float y, Vector4f color) {
renderText(text, x, y, color, 1.0f);
}
/**
* 获取一行文字的宽度(单位:像素)
*/
public float getTextWidth(String text) {
return getTextWidth(text, 1.0f);
}
/**
* 获取一行文字的宽度(带缩放)
*/
public float getTextWidth(String text, float scale) {
if (!initialized || text == null || text.isEmpty()) return 0f;
float width = 0f;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c >= firstChar && c < firstChar + charCount) {
STBTTBakedChar bakedChar = asciiCharData.get(c - firstChar);
width += bakedChar.xadvance() * scale;
} else {
// 修复中文索引逻辑:检查字符是否在烘焙的连续范围内
if (c >= CHINESE_FIRST_CHAR && c < CHINESE_FIRST_CHAR + CHINESE_CHAR_COUNT) {
int idx = c - CHINESE_FIRST_CHAR; // 关键:使用 Unicode 差值作为索引
STBTTBakedChar bakedChar = chineseCharData.get(idx);
width += bakedChar.xadvance() * scale;
} else {
// 对于未找到的字符,使用空格宽度
width += 0.5f * scale; // 估计值
}
}
}
return width;
}
public void renderText(String text, float x, float y, Vector4f color, float scale) {
if (!initialized || text == null || text.isEmpty()) return;
if (scale <= 0f) scale = 1.0f;
RenderSystem.assertOnRenderThread();
RenderSystem.pushState();
try {
ShaderProgram shader = ShaderManagement.getShaderProgram("TextShader");
if (shader == null) {
logger.error("TextShader not found");
return;
}
shader.use();
ShaderManagement.setUniformVec4(shader, "uColor", color);
ShaderManagement.setUniformInt(shader, "uTexture", 0);
RenderSystem.enableBlend();
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
RenderSystem.disableDepthTest();
try (MemoryStack stack = MemoryStack.stackPush()) {
STBTTAlignedQuad q = STBTTAlignedQuad.malloc(stack);
float[] xpos = {x};
float[] ypos = {y};
Tesselator t = Tesselator.getInstance();
BufferBuilder builder = t.getBuilder();
// 按字符类型分组渲染以减少纹理切换
int currentTexture = -1;
boolean batchStarted = false;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
int targetTexture;
STBTTBakedChar.Buffer charBuffer;
int texWidth, texHeight;
if (c >= firstChar && c < firstChar + charCount) {
targetTexture = asciiTextureId;
charBuffer = asciiCharData;
texWidth = bitmapWidth;
texHeight = bitmapHeight;
stbtt_GetBakedQuad(charBuffer, texWidth, texHeight, c - firstChar, xpos, ypos, q, true);
} else {
// 修复中文索引逻辑:检查字符是否在烘焙的连续范围内
if (c >= CHINESE_FIRST_CHAR && c < CHINESE_FIRST_CHAR + CHINESE_CHAR_COUNT) {
targetTexture = chineseTextureId;
charBuffer = chineseCharData;
texWidth = 4096;
texHeight = 4096;
// 关键修复:索引是字符的 Unicode 减去起始 Unicode
int idx = c - CHINESE_FIRST_CHAR;
stbtt_GetBakedQuad(charBuffer, texWidth, texHeight, idx, xpos, ypos, q, true);
} else {
continue; // 跳过不支持的字符
}
}
// 如果纹理改变,结束当前批次
if (targetTexture != currentTexture) {
if (batchStarted) {
t.end();
batchStarted = false;
}
RenderSystem.bindTexture(targetTexture);
currentTexture = targetTexture;
}
// 开始新批次(如果需要)
if (!batchStarted) {
builder.begin(RenderSystem.DRAW_TRIANGLES, (text.length() - i) * 6);
batchStarted = true;
}
// 应用缩放并计算顶点
float sx0 = x + (q.x0() - x) * scale;
float sx1 = x + (q.x1() - x) * scale;
float sy0 = y + (q.y0() - y) * scale;
float sy1 = y + (q.y1() - y) * scale;
builder.vertex(sx0, sy0, q.s0(), q.t0());
builder.vertex(sx1, sy0, q.s1(), q.t0());
builder.vertex(sx0, sy1, q.s0(), q.t1());
builder.vertex(sx1, sy0, q.s1(), q.t0());
builder.vertex(sx1, sy1, q.s1(), q.t1());
builder.vertex(sx0, sy1, q.s0(), q.t1());
}
// 结束最后一个批次
if (batchStarted) {
t.end();
}
}
} catch (Exception e) {
logger.error("Error rendering text: {}", e.getMessage(), e);
} finally {
RenderSystem.popState();
}
}
private int createTextureFromBitmap(int width, int height, ByteBuffer pixels) {
RenderSystem.assertOnRenderThread();
try {
int textureId = RenderSystem.genTextures();
RenderSystem.bindTexture(textureId);
RenderSystem.pixelStore(GL11.GL_UNPACK_ALIGNMENT, 1);
RenderSystem.texImage2D(GL11.GL_TEXTURE_2D, 0, GL30.GL_R8, width, height, 0,
GL11.GL_RED, GL11.GL_UNSIGNED_BYTE, pixels);
RenderSystem.setTextureMinFilter(GL11.GL_LINEAR);
RenderSystem.setTextureMagFilter(GL11.GL_LINEAR);
RenderSystem.setTextureWrapS(GL12.GL_CLAMP_TO_EDGE);
RenderSystem.setTextureWrapT(GL12.GL_CLAMP_TO_EDGE);
// 设置纹理swizzle以便单通道纹理在着色器中显示为白色
RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_R, GL11.GL_RED);
RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_G, GL11.GL_RED);
RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_B, GL11.GL_RED);
RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_A, GL11.GL_RED);
RenderSystem.pixelStore(GL11.GL_UNPACK_ALIGNMENT, 4);
RenderSystem.bindTexture(0);
return textureId;
} catch (Exception e) {
logger.error("Failed to create texture from bitmap: {}", e.getMessage(), e);
return 0;
}
}
public void cleanup() {
RenderSystem.assertOnRenderThread();
if (asciiTextureId != 0) {
RenderSystem.deleteTextures(asciiTextureId);
asciiTextureId = 0;
}
if (chineseTextureId != 0) {
RenderSystem.deleteTextures(chineseTextureId);
chineseTextureId = 0;
}
if (asciiCharData != null) {
asciiCharData.free();
asciiCharData = null;
}
if (chineseCharData != null) {
chineseCharData.free();
chineseCharData = null;
}
initialized = false;
logger.debug("TextRenderer cleaned up");
}
public boolean isInitialized() {
return initialized;
}
public int getAsciiTextureId() {
return asciiTextureId;
}
public int getChineseTextureId() {
return chineseTextureId;
}
}

View File

@@ -0,0 +1,4 @@
package com.chuangzhou.vivid2D.render.awt;
public class ModelAIPanel {
}

View File

@@ -0,0 +1,53 @@
package com.chuangzhou.vivid2D.render.awt;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
/**
* 模型点击事件监听器接口
*
* @author tzdwindows 7
*/
public interface ModelClickListener {
/**
* 当点击模型时触发
*
* @param mesh 被点击的网格,如果点击在空白处则为 null
* @param modelX 模型坐标系中的 X 坐标
* @param modelY 模型坐标系中的 Y 坐标
* @param screenX 屏幕坐标系中的 X 坐标
* @param screenY 屏幕坐标系中的 Y 坐标
*/
void onModelClicked(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY);
/**
* 当鼠标在模型上移动时触发
*
* @param mesh 鼠标下方的网格,如果不在任何网格上则为 null
* @param modelX 模型坐标系中的 X 坐标
* @param modelY 模型坐标系中的 Y 坐标
* @param screenX 屏幕坐标系中的 X 坐标
* @param screenY 屏幕坐标系中的 Y 坐标
*/
default void onModelHover(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) {
}
default void onLiquifyModeExited() {
}
default void onLiquifyModeEntered(Mesh2D targetMesh, ModelPart liquifyTargetPart) {
}
default void onSecondaryVertexModeEntered(Mesh2D secondaryVertexTargetMesh) {
}
default void onSecondaryVertexModeExited() {
}
default void onPuppetModeEntered(Mesh2D puppetTargetMesh) {
}
default void onPuppetModeExited() {
}
}

View File

@@ -0,0 +1,845 @@
// ModelLayerPanel.java (现代化重构)
package com.chuangzhou.vivid2D.render.awt;
import com.chuangzhou.vivid2D.render.awt.manager.LayerOperationManager;
import com.chuangzhou.vivid2D.render.awt.manager.ThumbnailManager;
import com.chuangzhou.vivid2D.render.awt.util.*;
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.util.Texture;
import org.joml.Vector2f;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class ModelLayerPanel extends JPanel {
private Model2D model;
private ModelRenderPanel renderPanel;
private DefaultListModel<ModelPart> listModel;
private JList<ModelPart> layerList;
// 现代化UI组件
private ModernButton addButton;
private ModernButton removeButton;
private ModernButton upButton;
private ModernButton downButton;
private ModernButton bindTextureButton;
private JSlider opacitySlider;
private JLabel opacityValueLabel;
private boolean isDragging = false;
private ModelPart draggedPart = null;
private Vector2f dragStartPosition = null;
private volatile boolean ignoreSliderEvents = false;
// 使用重构后的工具类
private ThumbnailManager thumbnailManager;
private PSDImporter psdImporter;
private LayerOperationManager operationManager;
// 现代化颜色方案
private static final Color BACKGROUND_COLOR = new Color(45, 45, 48);
private static final Color SURFACE_COLOR = new Color(62, 62, 66);
private static final Color ACCENT_COLOR = new Color(0, 122, 204);
private static final Color TEXT_COLOR = new Color(241, 241, 241);
private static final Color BORDER_COLOR = new Color(87, 87, 87);
public ModelLayerPanel(Model2D model) {
this(model, null);
}
public ModelLayerPanel(Model2D model, ModelRenderPanel renderPanel) {
this.model = model;
this.renderPanel = renderPanel;
// 设置现代化外观
setupModernLookAndFeel();
// 初始化工具类
this.thumbnailManager = new ThumbnailManager(renderPanel);
this.psdImporter = new PSDImporter(model, renderPanel, this);
this.operationManager = new LayerOperationManager(model);
initComponents();
reloadFromModel();
generateAllThumbnails();
}
private void setupModernLookAndFeel() {
setBackground(BACKGROUND_COLOR);
setBorder(new EmptyBorder(10, 10, 10, 10));
// 设置现代化UI默认值
UIManager.put("List.background", SURFACE_COLOR);
UIManager.put("List.foreground", TEXT_COLOR);
UIManager.put("List.selectionBackground", ACCENT_COLOR);
UIManager.put("List.selectionForeground", Color.WHITE);
UIManager.put("ScrollPane.background", SURFACE_COLOR);
UIManager.put("ScrollPane.border", BorderFactory.createLineBorder(BORDER_COLOR));
UIManager.put("Slider.background", SURFACE_COLOR);
UIManager.put("Slider.foreground", ACCENT_COLOR);
}
// ============== 缩略图相关方法 ==============
private void generateAllThumbnails() {
if (model == null) return;
thumbnailManager.clearCache();
for (int i = 0; i < listModel.getSize(); i++) {
ModelPart part = listModel.get(i);
thumbnailManager.generateThumbnail(part);
}
layerList.repaint();
}
private void refreshSelectedThumbnail() {
ModelPart selected = layerList.getSelectedValue();
if (selected != null) {
thumbnailManager.generateThumbnail(selected);
layerList.repaint();
}
}
// ============== 现代化组件初始化 ==============
private void initComponents() {
setLayout(new BorderLayout(10, 10));
listModel = new DefaultListModel<>();
layerList = createModernList();
// 创建现代化布局
createHeaderPanel();
createCenterPanel();
createControlPanel();
}
private JList<ModelPart> createModernList() {
JList<ModelPart> list = new JList<>(listModel);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setBackground(SURFACE_COLOR);
list.setForeground(TEXT_COLOR);
list.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
list.setFixedCellHeight(70); // 增加行高以显示缩略图
// 使用独立的渲染器
LayerCellRenderer cellRenderer = new LayerCellRenderer(this, thumbnailManager);
cellRenderer.attachMouseListener(list, listModel);
list.setCellRenderer(cellRenderer);
// 使用独立的拖拽处理器
list.setDragEnabled(true);
list.setTransferHandler(new LayerReorderTransferHandler(this));
list.setDropMode(DropMode.INSERT);
// 双击重命名
list.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
int idx = list.locationToIndex(e.getPoint());
if (idx >= 0) {
showRenameDialog(listModel.get(idx));
}
}
}
});
// 选择变更监听器
list.addListSelectionListener(e -> updateUIState());
return list;
}
private void createHeaderPanel() {
JPanel headerPanel = new JPanel(new BorderLayout());
headerPanel.setBackground(BACKGROUND_COLOR);
headerPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
JLabel titleLabel = new JLabel("图层管理");
titleLabel.setForeground(TEXT_COLOR);
titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 16f));
headerPanel.add(titleLabel, BorderLayout.WEST);
add(headerPanel, BorderLayout.NORTH);
}
private void createCenterPanel() {
JScrollPane scrollPane = new JScrollPane(layerList);
scrollPane.setBorder(createModernBorder("图层列表"));
scrollPane.getViewport().setBackground(SURFACE_COLOR);
// 自定义滚动条
JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar();
//verticalScrollBar.setUI(new ModernScrollBarUI());
add(scrollPane, BorderLayout.CENTER);
}
private void createControlPanel() {
JPanel controlPanel = new JPanel(new BorderLayout(10, 10));
controlPanel.setBackground(BACKGROUND_COLOR);
// 顶部按钮面板
controlPanel.add(createButtonPanel(), BorderLayout.NORTH);
// 底部设置面板
controlPanel.add(createSettingsPanel(), BorderLayout.SOUTH);
add(controlPanel, BorderLayout.SOUTH);
}
private JPanel createButtonPanel() {
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 8));
buttonPanel.setBackground(BACKGROUND_COLOR);
buttonPanel.setBorder(createModernBorder("操作"));
// 创建现代化按钮
addButton = createIconButton("", "添加图层", this::showAddMenu);
removeButton = createIconButton("", "删除选中图层", this::onRemoveLayer);
upButton = createIconButton("", "上移图层", this::moveSelectedUp);
downButton = createIconButton("", "下移图层", this::moveSelectedDown);
bindTextureButton = createIconButton("📷", "绑定贴图", this::bindTextureToSelectedPart);
// 初始禁用状态
removeButton.setEnabled(false);
upButton.setEnabled(false);
downButton.setEnabled(false);
bindTextureButton.setEnabled(false);
buttonPanel.add(addButton);
buttonPanel.add(removeButton);
buttonPanel.add(upButton);
buttonPanel.add(downButton);
buttonPanel.add(bindTextureButton);
return buttonPanel;
}
private JPanel createSettingsPanel() {
JPanel settingsPanel = new JPanel(new BorderLayout(10, 5));
settingsPanel.setBackground(BACKGROUND_COLOR);
settingsPanel.setBorder(createModernBorder("图层设置"));
// 不透明度控制
JPanel opacityPanel = new JPanel(new BorderLayout(8, 0));
opacityPanel.setBackground(BACKGROUND_COLOR);
JLabel opacityLabel = new JLabel("不透明度:");
opacityLabel.setForeground(TEXT_COLOR);
opacitySlider = createModernSlider();
opacityValueLabel = new JLabel("100%");
opacityValueLabel.setForeground(TEXT_COLOR);
opacityValueLabel.setPreferredSize(new Dimension(40, 20));
opacitySlider.addChangeListener(e -> {
if (ignoreSliderEvents) return;
onOpacityChanged();
});
opacityPanel.add(opacityLabel, BorderLayout.WEST);
opacityPanel.add(opacitySlider, BorderLayout.CENTER);
opacityPanel.add(opacityValueLabel, BorderLayout.EAST);
settingsPanel.add(opacityPanel, BorderLayout.CENTER);
return settingsPanel;
}
private JSlider createModernSlider() {
JSlider slider = new JSlider(0, 100, 100);
slider.setBackground(BACKGROUND_COLOR);
slider.setForeground(ACCENT_COLOR);
// 自定义滑块UI
//slider.setUI(new ModernSliderUI());
return slider;
}
private ModernButton createIconButton(String icon, String tooltip, Runnable action) {
ModernButton button = new ModernButton(icon);
button.setToolTipText(tooltip);
button.addActionListener(e -> action.run());
return button;
}
private void showAddMenu() {
JPopupMenu addMenu = new ModernPopupMenu();
String[] menuItems = {
"📄 创建空图层",
"🖼️ 从文件创建图层",
"🎨 创建透明图层",
"---",
"📂 从PSD文件导入"
};
Runnable[] actions = {
this::createEmptyPart,
this::createPartWithTextureFromFile,
this::createPartWithTransparentTexture,
null,
this::importPSDFile
};
for (int i = 0; i < menuItems.length; i++) {
if (menuItems[i].equals("---")) {
addMenu.add(new JSeparator());
} else {
JMenuItem item = new ModernMenuItem(menuItems[i]);
if (actions[i] != null) {
int finalI = i;
item.addActionListener(e -> actions[finalI].run());
}
addMenu.add(item);
}
}
addMenu.show(addButton, 0, addButton.getHeight());
}
private void createEmptyPart() {
String name = JOptionPane.showInputDialog(this, "新图层名称:", "新图层");
if (name == null || name.trim().isEmpty()) return;
operationManager.addLayer(name);
reloadFromModel();
// 选中新创建的部件
ModelPart newPart = findPartByName(name);
if (newPart != null) {
selectPart(newPart);
thumbnailManager.generateThumbnail(newPart);
}
}
private ModelPart findPartByName(String name) {
if (model == null) return null;
Map<String, ModelPart> partMap = model.getPartMap();
return partMap != null ? partMap.get(name) : null;
}
public Map<String, ModelPart> getModelPartMap() {
if (model == null) return null;
return model.getPartMap();
}
// ============== 现代化对话框方法 ==============
private void showRenameDialog(ModelPart part) {
String newName = (String) JOptionPane.showInputDialog(
this,
"输入新名称:",
"重命名图层",
JOptionPane.PLAIN_MESSAGE,
null,
null,
part.getName()
);
if (newName != null && !newName.trim().isEmpty()) {
renamePart(part, newName);
reloadFromModel();
refreshSelectedThumbnail();
}
}
// ============== 原有业务方法(保持不变) ==============
public void setModel(Model2D model) {
this.model = model;
this.psdImporter = new PSDImporter(model, renderPanel, this);
this.operationManager = new LayerOperationManager(model);
reloadFromModel();
generateAllThumbnails();
}
public void setRenderPanel(ModelRenderPanel panel) {
this.renderPanel = panel;
this.thumbnailManager = new ThumbnailManager(panel);
this.psdImporter = new PSDImporter(model, panel, this);
}
private void importPSDFile() {
JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("PSD文件", "psd"));
int r = chooser.showOpenDialog(this);
if (r == JFileChooser.APPROVE_OPTION) {
psdImporter.importPSDFile(chooser.getSelectedFile());
}
}
private void updateUIState() {
ModelPart sel = layerList.getSelectedValue();
boolean hasSelection = sel != null;
if (hasSelection) {
updateOpacitySlider(sel);
}
removeButton.setEnabled(hasSelection);
upButton.setEnabled(hasSelection);
downButton.setEnabled(hasSelection);
bindTextureButton.setEnabled(hasSelection);
}
private void updateOpacitySlider(ModelPart part) {
float opacity = extractOpacity(part);
int value = Math.round(opacity * 100);
ignoreSliderEvents = true;
try {
opacitySlider.setValue(value);
opacityValueLabel.setText(value + "%");
} finally {
ignoreSliderEvents = false;
}
}
private float extractOpacity(ModelPart part) {
try {
Method method = part.getClass().getMethod("getOpacity");
Object value = method.invoke(part);
if (value instanceof Float) return (Float) value;
} catch (Exception e) {
try {
Field field = part.getClass().getDeclaredField("opacity");
field.setAccessible(true);
Object value = field.get(part);
if (value instanceof Float) return (Float) value;
} catch (Exception ignored) {}
}
return 1.0f;
}
private void onOpacityChanged() {
ModelPart sel = layerList.getSelectedValue();
if (sel == null) return;
int value = opacitySlider.getValue();
opacityValueLabel.setText(value + "%");
setPartOpacity(sel, value / 100.0f);
if (model != null) model.markNeedsUpdate();
layerList.repaint();
refreshSelectedThumbnail();
}
private void setPartOpacity(ModelPart part, float opacity) {
try {
Method method = part.getClass().getMethod("setOpacity", float.class);
method.invoke(part, opacity);
} catch (Exception e) {
try {
Field field = part.getClass().getDeclaredField("opacity");
field.setAccessible(true);
field.setFloat(part, opacity);
} catch (Exception ignored) {}
}
}
// 现代化边框
private TitledBorder createModernBorder(String title) {
TitledBorder border = BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(BORDER_COLOR, 1, true),
title
);
border.setTitleColor(TEXT_COLOR);
border.setTitleFont(border.getTitleFont().deriveFont(Font.BOLD, 12f));
return border;
}
public Texture createTextureFromBufferedImage(BufferedImage img, String texName) {
BufferedImage flippedImage = flipImageVertically(img);
return Texture.createFromBufferedImage(texName, flippedImage);
}
private BufferedImage flipImageVertically(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
BufferedImage flipped = new BufferedImage(width, height, img.getType());
Graphics2D g = flipped.createGraphics();
g.drawImage(img, 0, height, width, -height, null);
g.dispose();
return flipped;
}
public void refreshCurrentThumbnail() {
refreshSelectedThumbnail();
}
// 现代化按钮类
private class ModernButton extends JButton {
public ModernButton(String text) {
super(text);
setupModernStyle();
}
private void setupModernStyle() {
setBackground(SURFACE_COLOR);
setForeground(TEXT_COLOR);
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(BORDER_COLOR, 1, true),
BorderFactory.createEmptyBorder(8, 12, 8, 12)
));
setFocusPainted(false);
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
if (isEnabled()) {
setBackground(ACCENT_COLOR);
setForeground(Color.WHITE);
}
}
@Override
public void mouseExited(MouseEvent e) {
setBackground(SURFACE_COLOR);
setForeground(TEXT_COLOR);
}
});
}
}
// 现代化菜单项类
private class ModernMenuItem extends JMenuItem {
public ModernMenuItem(String text) {
super(text);
setBackground(SURFACE_COLOR);
setForeground(TEXT_COLOR);
setBorder(BorderFactory.createEmptyBorder(8, 12, 8, 12));
addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
setBackground(ACCENT_COLOR);
setForeground(Color.WHITE);
}
@Override
public void mouseExited(MouseEvent e) {
setBackground(SURFACE_COLOR);
setForeground(TEXT_COLOR);
}
});
}
}
// 现代化弹出菜单
private class ModernPopupMenu extends JPopupMenu {
public ModernPopupMenu() {
setBackground(SURFACE_COLOR);
setBorder(BorderFactory.createLineBorder(BORDER_COLOR));
}
}
// 其余原有方法保持不变...
// (reloadFromModel, performVisualReorder, bindTextureToSelectedPart等)
public void reloadFromModel() {
ModelPart selected = layerList.getSelectedValue();
listModel.clear();
if (model == null) return;
try {
List<ModelPart> parts = model.getParts();
if (parts != null) {
for (int i = parts.size() - 1; i >= 0; i--) {
listModel.addElement(parts.get(i));
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
if (selected != null) {
for (int i = 0; i < listModel.getSize(); i++) {
if (listModel.get(i) == selected) {
layerList.setSelectedIndex(i);
break;
}
}
}
}
public void performVisualReorder(int visualFrom, int visualTo) {
if (model == null) return;
try {
int size = listModel.getSize();
if (visualFrom < 0 || visualFrom >= size) return;
if (visualTo < 0) visualTo = 0;
if (visualTo > size - 1) visualTo = size - 1;
ModelPart moved = listModel.get(visualFrom);
if (!isDragging) {
isDragging = true;
draggedPart = moved;
dragStartPosition = new Vector2f(moved.getPosition());
}
List<ModelPart> visual = new ArrayList<>(size);
for (int i = 0; i < size; i++) visual.add(listModel.get(i));
moved = visual.remove(visualFrom);
visual.add(visualTo, moved);
ignoreSliderEvents = true;
try {
listModel.clear();
for (ModelPart p : visual) listModel.addElement(p);
} finally {
ignoreSliderEvents = false;
}
operationManager.moveLayer(visual);
selectPart(moved);
} catch (Exception ex) {
ex.printStackTrace();
}
}
private void selectPart(ModelPart part) {
if (part == null) return;
for (int i = 0; i < listModel.getSize(); i++) {
if (listModel.get(i) == part) {
layerList.setSelectedIndex(i);
layerList.ensureIndexIsVisible(i);
return;
}
}
}
private void renamePart(ModelPart part, String newName) {
if (part == null) return;
part.setName(newName);
}
public Model2D getModel() {
return model;
}
private void bindTextureToSelectedPart() {
ModelPart sel = layerList.getSelectedValue();
if (sel == null) return;
JFileChooser chooser = new JFileChooser();
int r = chooser.showOpenDialog(this);
if (r != JFileChooser.APPROVE_OPTION) return;
File f = chooser.getSelectedFile();
try {
BufferedImage img = null;
try {
img = ImageIO.read(f);
} catch (Exception ignored) {
}
Mesh2D targetMesh = null;
try {
List<Mesh2D> list = sel.getMeshes();
if (!list.isEmpty() && list.get(0) != null) {
targetMesh = list.get(0);
}
} catch (Exception ignored) {
}
if (targetMesh == null) {
if (img == null) {
img = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB);
}
targetMesh = MeshTextureUtil.createQuadForImage(img, sel.getName() + "_mesh");
try {
sel.addMesh(targetMesh);
} catch (Exception ex) {
ex.printStackTrace();
}
}
final Mesh2D meshToBind = targetMesh;
final String filePath = f.getAbsolutePath();
final String texName = sel.getName() + "_tex";
if (renderPanel != null) {
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
Texture texture = Texture.createFromFile(texName, filePath);
meshToBind.setTexture(texture);
model.addTexture(texture);
model.markNeedsUpdate();
} catch (Throwable ex) {
ex.printStackTrace();
}
});
} else {
if (img == null) img = ImageIO.read(f);
Texture mem = MeshTextureUtil.tryCreateTextureFromImageMemory(img, texName);
if (mem != null) {
meshToBind.setTexture(mem);
model.addTexture(mem);
model.markNeedsUpdate();
} else {
System.err.println("无法在无 GL 上下文中创建纹理: " + filePath);
}
}
reloadFromModel();
selectPart(sel);
refreshSelectedThumbnail();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "绑定贴图失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
ex.printStackTrace();
}
}
private void onRemoveLayer() {
ModelPart sel = layerList.getSelectedValue();
if (sel == null) return;
int r = JOptionPane.showConfirmDialog(this, "确认删除图层:" + sel.getName() + " ?", "确认删除", JOptionPane.YES_NO_OPTION);
if (r != JOptionPane.YES_OPTION) return;
try {
operationManager.removeLayer(sel);
thumbnailManager.removeThumbnail(sel);
reloadFromModel();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "删除失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
private void moveSelectedUp() {
int idx = layerList.getSelectedIndex();
if (idx <= 0) return;
performVisualReorder(idx, idx - 1);
}
private void moveSelectedDown() {
int idx = layerList.getSelectedIndex();
if (idx < 0 || idx >= listModel.getSize() - 1) return;
performVisualReorder(idx, idx + 1);
}
private void createPartWithTextureFromFile() {
JFileChooser chooser = new JFileChooser();
int r = chooser.showOpenDialog(this);
if (r != JFileChooser.APPROVE_OPTION) return;
File f = chooser.getSelectedFile();
try {
BufferedImage img = ImageIO.read(f);
if (img == null) throw new IOException("无法读取图片:" + f.getAbsolutePath());
String name = JOptionPane.showInputDialog(this, "新图层名称:", f.getName());
if (name == null || name.trim().isEmpty()) name = f.getName();
// 创建部件与 Mesh
ModelPart part = model.createPart(name);
Mesh2D mesh = MeshTextureUtil.createQuadForImage(img, name + "_mesh");
mesh.createDefaultSecondaryVertices();
part.addMesh(mesh);
// 创建纹理
if (renderPanel != null) {
final String texName = name + "_tex";
final String filePath = f.getAbsolutePath();
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
Texture texture = Texture.createFromFile(texName, filePath);
List<Mesh2D> partMeshes = part.getMeshes();
Mesh2D actualMesh = null;
if (partMeshes != null && !partMeshes.isEmpty()) {
actualMesh = partMeshes.get(partMeshes.size() - 1);
}
if (actualMesh != null) {
actualMesh.setTexture(texture);
} else {
mesh.setTexture(texture);
}
model.addTexture(texture);
model.markNeedsUpdate();
} catch (Throwable ex) {
ex.printStackTrace();
}
});
} else {
Texture memTex = MeshTextureUtil.tryCreateTextureFromImageMemory(img, name + "_tex");
if (memTex != null) {
mesh.setTexture(memTex);
model.addTexture(memTex);
model.markNeedsUpdate();
}
}
reloadFromModel();
selectPart(part);
thumbnailManager.generateThumbnail(part);
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "创建带贴图图层失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
}
public void endDragOperation() {
if (isDragging && draggedPart != null && dragStartPosition != null) {
Vector2f endPosition = draggedPart.getPosition();
if (!endPosition.equals(dragStartPosition)) {
recordDragOperation(draggedPart, dragStartPosition, endPosition);
}
isDragging = false;
draggedPart = null;
dragStartPosition = null;
}
}
private void recordDragOperation(ModelPart part, Vector2f startPos, Vector2f endPos) {
OperationHistoryManager manager = OperationHistoryManager.getInstance();
if (manager != null) {
manager.recordOperation("DRAG_PART", part, startPos, endPos);
}
}
private void createPartWithTransparentTexture() {
String name = JOptionPane.showInputDialog(this, "新图层名称(透明):", "透明图层");
if (name == null || name.trim().isEmpty()) return;
int w = 128, h = 128;
try {
String wh = JOptionPane.showInputDialog(this, "输入尺寸宽x高例如 128x128或留空使用 128x128", "128x128");
if (wh != null && wh.contains("x")) {
String[] sp = wh.split("x");
w = Math.max(1, Integer.parseInt(sp[0].trim()));
h = Math.max(1, Integer.parseInt(sp[1].trim()));
}
} catch (Exception ignored) {
}
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
ModelPart part = model.createPart(name);
Mesh2D mesh = MeshTextureUtil.createQuadForImage(img, name + "_mesh");
part.addMesh(mesh);
Texture memTex = MeshTextureUtil.tryCreateTextureFromImageMemory(img, name + "_tex");
if (memTex != null) {
mesh.setTexture(memTex);
model.addTexture(memTex);
}
model.markNeedsUpdate();
reloadFromModel();
selectPart(part);
thumbnailManager.generateThumbnail(part);
}
}

View File

@@ -0,0 +1,675 @@
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.OperationHistoryGlobal;
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.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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.Timer;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
/**
* vivid2D 模型的 Java 渲染面板
*
* <p>该类提供了 vivid2D 模型在 Java 环境下的图形渲染功能,
* 包含基本的 2D 图形绘制、模型显示和交互操作。</p>
*
* @author tzdwindows 7
* @version 1.1
* @see TestModelGLPanel
* @since 2025-10-13
*/
public class ModelRenderPanel extends JPanel {
private static final Logger logger = LoggerFactory.getLogger(ModelRenderPanel.class);
private final GLContextManager glContextManager;
private final MouseManagement mouseManagement;
private final CameraManagement cameraManagement;
private final WorldManagement worldManagement;
private final AtomicReference<Model2D> modelRef = new AtomicReference<>();
private final KeyboardManager keyboardManager;
private final CopyOnWriteArrayList<ModelClickListener> clickListeners = new CopyOnWriteArrayList<>();
private final StatusRecordManagement statusRecordManagement;
private final RanderToolsManager randerToolsManager = RanderToolsManager.getInstance();
public static final float BORDER_THICKNESS = 6.0f;
public static final float CORNER_SIZE = 12.0f;
// ================== 摄像机控制相关字段 ==================
private final Timer doubleClickTimer;
private volatile long lastClickTime = 0;
private static final int DOUBLE_CLICK_INTERVAL = 300; // 双击间隔(毫秒)
private final ToolManagement toolManagement;
// ================== 摄像机控制方法 ==================
/**
* 获取摄像机实例
*/
public Camera getCamera() {
return ModelRender.getCamera();
}
/**
* 重置摄像机
*/
public void resetCamera() {
glContextManager.executeInGLContext(ModelRender::resetCamera);
}
/**
* 构造函数:使用模型路径
*/
public ModelRenderPanel(String modelPath, int width, int height) {
this.glContextManager = new GLContextManager(modelPath, width, height);
this.statusRecordManagement = new StatusRecordManagement(this, OperationHistoryGlobal.getInstance());
this.keyboardManager = new KeyboardManager(this);
this.worldManagement = new WorldManagement(this, glContextManager);
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 -> {
handleSingleClick();
});
doubleClickTimer.setRepeats(false);
}
/**
* 构造函数:使用已加载模型
*/
public ModelRenderPanel(Model2D model, int width, int height) {
this.glContextManager = new GLContextManager(model, width, height);
this.modelRef.set(model);
this.statusRecordManagement = new StatusRecordManagement(this, OperationHistoryGlobal.getInstance());
this.keyboardManager = new KeyboardManager(this);
this.worldManagement = new WorldManagement(this, glContextManager);
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 -> {
// 单单击超时处理
handleSingleClick();
});
doubleClickTimer.setRepeats(false);
}
/**
* 处理双击事件
*/
private void handleDoubleClick(MouseEvent e) {
float[] modelCoords = worldManagement.screenToModelCoordinates(e.getX(), e.getY());
// 如果有激活的工具,优先交给工具处理
if (toolManagement.hasActiveTool() && modelCoords != null) {
glContextManager.executeInGLContext(() -> {
toolManagement.handleMouseDoubleClicked(e, modelCoords[0], modelCoords[1]);
});
return;
}
}
/**
* 处理单单击事件
*/
private void handleSingleClick() {
// 单单击逻辑已迁移到各个工具中
}
/**
* 添加模型点击监听器
*/
public void addModelClickListener(ModelClickListener listener) {
clickListeners.add(listener);
}
/**
* 移除模型点击监听器
*/
public void removeModelClickListener(ModelClickListener listener) {
clickListeners.remove(listener);
}
/**
* 获取当前选中的模型部件
* @return 选中的模型部件列表
*/
public List<ModelPart> getSelectedParts() {
Tool currentTool = toolManagement.getCurrentTool();
if (currentTool instanceof SelectionTool) {
return ((SelectionTool) currentTool).getSelectedParts();
}
return java.util.Collections.emptyList();
}
/**
* 获取当前选中的网格
*/
public Mesh2D getSelectedMesh() {
// 委托给工具管理系统的当前工具
Tool currentTool = toolManagement.getCurrentTool();
if (currentTool instanceof SelectionTool) {
return ((SelectionTool) currentTool).getSelectedMesh();
} else if (toolManagement.getDefaultTool() instanceof SelectionTool selectedMesh) {
return selectedMesh.getSelectedMesh();
}
return null;
}
/**
* 获取当前选中的所有网格
*/
public java.util.Set<Mesh2D> getSelectedMeshes() {
Tool currentTool = toolManagement.getCurrentTool();
if (currentTool instanceof SelectionTool) {
return ((SelectionTool) currentTool).getSelectedMeshes();
} else if (toolManagement.getDefaultTool() instanceof SelectionTool selectedMeshes) {
return selectedMeshes.getSelectedMeshes();
}
return java.util.Collections.emptySet();
}
/**
* 清空所有选中的网格
*/
public void clearSelectedMeshes() {
glContextManager.executeInGLContext(() -> {
// 委托给工具管理系统的当前工具
Tool currentTool = toolManagement.getCurrentTool();
if (currentTool instanceof SelectionTool) {
((SelectionTool) currentTool).clearSelectedMeshes();
} else {
toolManagement.switchToDefaultTool();
}
logger.debug("清空所有选中网格");
});
}
/**
* 全选所有网格
*/
public void selectAllMeshes() {
glContextManager.executeInGLContext(() -> {
// 委托给工具管理系统的当前工具
Tool currentTool = toolManagement.getCurrentTool();
if (currentTool instanceof SelectionTool) {
((SelectionTool) currentTool).selectAllMeshes();
logger.info("已全选网格");
}
});
}
/**
* 获取当前选中的部件
*/
public ModelPart getSelectedPart() {
Mesh2D selectedMesh = getSelectedMesh();
return selectedMesh != null ? findPartByMesh(selectedMesh) : null;
}
/**
* 获取鼠标悬停的网格
*/
public Mesh2D getHoveredMesh() {
// 委托给工具管理系统的当前工具
Tool currentTool = toolManagement.getCurrentTool();
if (currentTool instanceof SelectionTool) {
return ((SelectionTool) currentTool).getHoveredMesh();
}
return null;
}
/**
* 获取液化工具实例
*/
public LiquifyTool getLiquifyTool() {
Tool tool = toolManagement.getTool("液化工具");
return tool instanceof LiquifyTool ? (LiquifyTool) tool : null;
}
/**
* 设置液化画笔大小
*/
public void setLiquifyBrushSize(float size) {
LiquifyTool liquifyTool = getLiquifyTool();
if (liquifyTool != null) {
liquifyTool.setLiquifyBrushSize(size);
}
}
/**
* 设置液化画笔强度
*/
public void setLiquifyBrushStrength(float strength) {
LiquifyTool liquifyTool = getLiquifyTool();
if (liquifyTool != null) {
liquifyTool.setLiquifyBrushStrength(strength);
}
}
/**
* 设置液化模式
*/
public void setLiquifyMode(ModelPart.LiquifyMode mode) {
LiquifyTool liquifyTool = getLiquifyTool();
if (liquifyTool != null) {
liquifyTool.setLiquifyMode(mode);
}
}
/**
* 获取当前液化状态
*/
public boolean isInLiquifyMode() {
Tool currentTool = toolManagement.getCurrentTool();
return currentTool instanceof LiquifyTool;
}
private void initialize() {
setLayout(new BorderLayout());
setPreferredSize(new Dimension(glContextManager.getWidth(), glContextManager.getHeight()));
// 添加鼠标监听器
mouseManagement.addMouseListeners();
// 创建渲染线程
glContextManager.startRendering();
this.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
int w = getWidth();
int h = getHeight();
if (w <= 0 || h <= 0) return;
if (w == glContextManager.getWidth() && h == glContextManager.getHeight()) return;
resize(w, h);
}
});
glContextManager.setRepaintCallback(this::repaint);
}
/**
* 处理鼠标按下事件
*/
public void handleMousePressed(MouseEvent e) {
if (!glContextManager.isContextInitialized()) return;
final int screenX = e.getX();
final int screenY = e.getY();
requestFocusInWindow();
// 首先处理中键拖拽(摄像机控制),在任何模式下都可用
if (SwingUtilities.isMiddleMouseButton(e)) {
glContextManager.setCameraDragging(true);
cameraManagement.setLastCameraDragX(screenX);
cameraManagement.setLastCameraDragY(screenY);
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
return;
}
float[] modelCoords = worldManagement.screenToModelCoordinates(screenX, screenY);
// 如果有激活的工具,优先交给工具处理
if (toolManagement.hasActiveTool() && modelCoords != null) {
glContextManager.executeInGLContext(() -> toolManagement.handleMousePressed(e, modelCoords[0], modelCoords[1]));
}
}
public ToolManagement getToolManagement() {
return toolManagement;
}
public void switchTool(String toolName) {
glContextManager.executeInGLContext(() -> toolManagement.switchTool(toolName));
}
/**
* 切换到液化工具
*/
public void switchToLiquifyTool() {
switchTool("液化工具");
}
public Tool getCurrentTool() {
return toolManagement.getCurrentTool();
}
/**
* 处理鼠标拖拽事件
*/
public void handleMouseDragged(MouseEvent e) {
if (glContextManager.isCameraDragging()) {
final int screenX = e.getX();
final int screenY = e.getY();
// 计算鼠标移动距离
final int deltaX = screenX - cameraManagement.getLastCameraDragX();
final int deltaY = screenY - cameraManagement.getLastCameraDragY();
// 更新最后拖拽位置
cameraManagement.setLastCameraDragX(screenX);
cameraManagement.setLastCameraDragY(screenY);
// 确保在 GL 上下文线程中执行摄像机移动
glContextManager.executeInGLContext(() -> {
try {
Camera camera = ModelRender.getCamera();
float zoom = camera.getZoom();
float worldDeltaX = -deltaX / zoom;
float worldDeltaY = -deltaY / zoom;
// 应用摄像机移动
camera.move(worldDeltaX, worldDeltaY);
} catch (Exception ex) {
logger.error("处理摄像机拖拽时出错", ex);
}
});
return;
}
final float[][] modelCoords = {worldManagement.screenToModelCoordinates(e.getX(), e.getY())};
// 如果有激活的工具,优先交给工具处理
if (toolManagement.hasActiveTool() && modelCoords[0] != null) {
glContextManager.executeInGLContext(() -> toolManagement.handleMouseDragged(e, modelCoords[0][0], modelCoords[0][1]));
return;
}
}
/**
* 处理鼠标释放事件
*/
public void handleMouseReleased(MouseEvent e) {
// 首先处理摄像机拖拽释放
if (glContextManager.isCameraDragging() && SwingUtilities.isMiddleMouseButton(e)) {
glContextManager.setCameraDragging(false);
// 恢复悬停状态的光标
updateCursorForHoverState();
return;
}
float[] modelCoords = worldManagement.screenToModelCoordinates(e.getX(), e.getY());
// 如果有激活的工具,优先交给工具处理
if (toolManagement.hasActiveTool() && modelCoords != null) {
toolManagement.handleMouseReleased(e, modelCoords[0], modelCoords[1]);
}
}
/**
* 处理鼠标点击事件
*/
public void handleMouseClick(MouseEvent e) {
if (!glContextManager.isContextInitialized()) return;
final int screenX = e.getX();
final int screenY = e.getY();
long currentTime = System.currentTimeMillis();
boolean isDoubleClick = (currentTime - lastClickTime) < DOUBLE_CLICK_INTERVAL;
lastClickTime = currentTime;
if (isDoubleClick) {
// 取消单单击计时器
doubleClickTimer.stop();
handleDoubleClick(e);
} else {
float[] modelCoords = worldManagement.screenToModelCoordinates(screenX, screenY);
// 如果有激活的工具,优先交给工具处理
if (toolManagement.hasActiveTool() && modelCoords != null) {
toolManagement.handleMouseClicked(e, modelCoords[0], modelCoords[1]);
return;
}
glContextManager.executeInGLContext(() -> {
try {
if (modelCoords == null) return;
float modelX = modelCoords[0];
float modelY = modelCoords[1];
logger.debug("点击位置:({}, {})", modelX, modelY);
// 触发点击事件
for (ModelClickListener listener : clickListeners) {
try {
listener.onModelClicked(null, modelX, modelY, screenX, screenY);
} catch (Exception ex) {
logger.error("点击事件监听器执行出错", ex);
}
}
} catch (Exception ex) {
logger.error("处理鼠标点击时出错", ex);
}
});
doubleClickTimer.restart();
}
}
/**
* 处理鼠标移动事件
*/
public void handleMouseMove(MouseEvent e) {
if (!glContextManager.isContextInitialized()) return;
final int screenX = e.getX();
final int screenY = e.getY();
if (glContextManager.isCameraDragging()) {
return;
}
float[] modelCoords = worldManagement.screenToModelCoordinates(screenX, screenY);
// 如果有激活的工具,优先交给工具处理
if (toolManagement.hasActiveTool() && modelCoords != null) {
toolManagement.handleMouseMoved(e, modelCoords[0], modelCoords[1]);
return;
}
}
/**
* 根据悬停状态更新光标(无坐标版本,用于鼠标释放后)
*/
private void updateCursorForHoverState() {
Point mousePos = getMousePosition();
if (mousePos != null) {
float[] modelCoords = worldManagement.screenToModelCoordinates(mousePos.x, mousePos.y);
if (modelCoords != null) {
// 委托给工具管理系统的当前工具
Tool currentTool = toolManagement.getCurrentTool();
if (currentTool != null) {
setCursor(currentTool.getToolCursor());
}
}
} else {
// 鼠标不在面板内,恢复默认光标
setCursor(Cursor.getDefaultCursor());
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
try {
BufferedImage imgToDraw = glContextManager.getCurrentFrame()
!= null ? glContextManager.getCurrentFrame() : glContextManager.getLastFrame();
int panelW = getWidth();
int panelH = getHeight();
if (imgToDraw != null) {
g2d.drawImage(imgToDraw, 0, 0, panelW, panelH, null);
} else {
g2d.setColor(Color.DARK_GRAY);
g2d.fillRect(0, 0, panelW, panelH);
}
if (modelRef.get() == null) {
g2d.setColor(new Color(255, 255, 0, 200));
g2d.drawString("模型未加载", 10, 20);
}
} finally {
g2d.dispose();
}
}
/**
* 设置模型
*/
public void setModel(Model2D model) {
glContextManager.executeInGLContext(() -> {
modelRef.set(model);
logger.info("模型已更新");
});
}
/**
* 获取当前渲染的模型
*/
public Model2D getModel() {
return modelRef.get();
}
/**
* 重新设置面板大小
* <p>
* 说明:当 Swing 面板被放大时,需要同时调整离屏 GLFW 窗口像素大小、GL 视口以及重分配像素读取缓冲,
* 否则将把较小分辨率的图像拉伸到更大面板上导致模糊。
*/
public void resize(int newWidth, int newHeight) {
// 更新 Swing 尺寸
setPreferredSize(new Dimension(newWidth, newHeight));
revalidate();
glContextManager.resize(newWidth, newHeight);
}
/**
* 获取全局操作历史管理器
*/
public StatusRecordManagement getStatusRecordManagement() {
return statusRecordManagement;
}
/**
* 获取 GL 上下文管理器
*/
public GLContextManager getGlContextManager() {
return glContextManager;
}
/**
* 获取鼠标管理器
*/
public MouseManagement getMouseManagement() {
return mouseManagement;
}
/**
* 获取相机管理器
*/
public CameraManagement getCameraManagement() {
return cameraManagement;
}
/**
* 获取键盘管理器
*/
public KeyboardManager getKeyboardManager() {
return keyboardManager;
}
// ================== 保留的辅助方法 ==================
/**
* 通过网格查找对应的 ModelPart
*/
public ModelPart findPartByMesh(Mesh2D mesh) {
Model2D model = modelRef.get();
if (model == null) return null;
for (ModelPart part : model.getParts()) {
ModelPart found = findPartByMeshRecursive(part, mesh);
if (found != null) {
return found;
}
}
return null;
}
/**
* 递归查找包含指定网格的部件
*/
private ModelPart findPartByMeshRecursive(ModelPart part, Mesh2D targetMesh) {
if (part == null || targetMesh == null) return null;
// 检查当前部件的网格
for (Mesh2D mesh : part.getMeshes()) {
if (mesh == targetMesh) {
return part;
}
}
// 递归检查子部件
for (ModelPart child : part.getChildren()) {
ModelPart found = findPartByMeshRecursive(child, targetMesh);
if (found != null) {
return found;
}
}
return null;
}
public enum DragMode {
NONE, // 无拖拽
MOVE, // 移动部件
RESIZE_LEFT, // 调整左边
RESIZE_RIGHT, // 调整右边
RESIZE_TOP, // 调整上边
RESIZE_BOTTOM, // 调整下边
RESIZE_TOP_LEFT, // 调整左上角
RESIZE_TOP_RIGHT, // 调整右上角
RESIZE_BOTTOM_LEFT, // 调整左下角
RESIZE_BOTTOM_RIGHT, // 调整右下角
ROTATE, // 新增:旋转
MOVE_PIVOT, // 新增:移动中心点
MOVE_SECONDARY_VERTEX, // 新增:移动二级顶点
MOVE_PUPPET_PIN // 新增:移动 puppetPin
}
}

View File

@@ -0,0 +1,716 @@
package com.chuangzhou.vivid2D.render.awt;
import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal;
import com.chuangzhou.vivid2D.render.model.ModelEvent;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import org.joml.Vector2f;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author tzdwindows 7
*/
public class TransformPanel extends JPanel implements ModelEvent {
private final ModelRenderPanel renderPanel;
private final List<ModelPart> selectedParts = new ArrayList<>();
private boolean isMultiSelection = false;
// 位置控制
private JTextField positionXField;
private JTextField positionYField;
// 旋转控制
private JTextField rotationField;
// 缩放控制
private JTextField scaleXField;
private JTextField scaleYField;
// 中心点控制
private JTextField pivotXField;
private JTextField pivotYField;
// 按钮
private JButton flipXButton;
private JButton flipYButton;
private JButton rotate90CWButton;
private JButton rotate90CCWButton;
private JButton resetScaleButton;
private boolean updatingUI = false; // 防止UI更新时触发事件
private javax.swing.Timer transformTimer; // 用于延迟处理变换输入
private final OperationHistoryGlobal operationHistory;
public TransformPanel(ModelRenderPanel renderPanel) {
this.renderPanel = renderPanel;
this.operationHistory = OperationHistoryGlobal.getInstance();
initComponents();
setupListeners();
updateUIState();
}
private void initComponents() {
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(3, 5, 3, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
int row = 0;
// 位置控制
gbc.gridx = 0;
gbc.gridy = row;
add(new JLabel("位置 X:"), gbc);
gbc.gridx = 1;
gbc.gridy = row;
positionXField = new JTextField("0.0");
add(positionXField, gbc);
gbc.gridx = 2;
gbc.gridy = row;
add(new JLabel("Y:"), gbc);
gbc.gridx = 3;
gbc.gridy = row++;
positionYField = new JTextField("0.0");
add(positionYField, gbc);
// 旋转控制
gbc.gridx = 0;
gbc.gridy = row;
add(new JLabel("旋转角度:"), gbc);
gbc.gridx = 1;
gbc.gridy = row;
rotationField = new JTextField("0.0");
add(rotationField, gbc);
gbc.gridx = 2;
gbc.gridy = row;
gbc.gridwidth = 2;
rotate90CWButton = new JButton("+90°");
rotate90CWButton.setToolTipText("顺时针旋转90度");
add(rotate90CWButton, gbc);
gbc.gridx = 0;
gbc.gridy = ++row;
gbc.gridwidth = 4;
rotate90CCWButton = new JButton("-90°");
rotate90CCWButton.setToolTipText("逆时针旋转90度");
add(rotate90CCWButton, gbc);
// 缩放控制
gbc.gridx = 0;
gbc.gridy = ++row;
gbc.gridwidth = 1;
add(new JLabel("缩放 X:"), gbc);
gbc.gridx = 1;
gbc.gridy = row;
scaleXField = new JTextField("1.0");
add(scaleXField, gbc);
gbc.gridx = 2;
gbc.gridy = row;
add(new JLabel("Y:"), gbc);
gbc.gridx = 3;
gbc.gridy = row;
scaleYField = new JTextField("1.0");
add(scaleYField, gbc);
gbc.gridx = 0;
gbc.gridy = ++row;
gbc.gridwidth = 2;
flipXButton = new JButton("水平翻转");
add(flipXButton, gbc);
gbc.gridx = 2;
gbc.gridy = row;
gbc.gridwidth = 2;
flipYButton = new JButton("垂直翻转");
add(flipYButton, gbc);
gbc.gridx = 0;
gbc.gridy = ++row;
gbc.gridwidth = 4;
resetScaleButton = new JButton("重置缩放");
resetScaleButton.setToolTipText("重置为1:1缩放");
add(resetScaleButton, gbc);
// 中心点控制
gbc.gridx = 0;
gbc.gridy = ++row;
gbc.gridwidth = 1;
add(new JLabel("中心点 X:"), gbc);
gbc.gridx = 1;
gbc.gridy = row;
pivotXField = new JTextField("0.0");
add(pivotXField, gbc);
gbc.gridx = 2;
gbc.gridy = row;
add(new JLabel("Y:"), gbc);
gbc.gridx = 3;
gbc.gridy = row;
pivotYField = new JTextField("0.0");
add(pivotYField, gbc);
// Set border
setBorder(BorderFactory.createTitledBorder("变换控制"));
// 初始化定时器,用于延迟处理变换输入
transformTimer = new javax.swing.Timer(300, e -> applyTransformChanges());
transformTimer.setRepeats(false); // 只执行一次
}
private void setupListeners() {
// 为所有文本框添加文档监听器
DocumentListener documentListener = new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
scheduleTransformUpdate();
}
@Override
public void removeUpdate(DocumentEvent e) {
scheduleTransformUpdate();
}
@Override
public void changedUpdate(DocumentEvent e) {
scheduleTransformUpdate();
}
};
positionXField.getDocument().addDocumentListener(documentListener);
positionYField.getDocument().addDocumentListener(documentListener);
rotationField.getDocument().addDocumentListener(documentListener);
scaleXField.getDocument().addDocumentListener(documentListener);
scaleYField.getDocument().addDocumentListener(documentListener);
pivotXField.getDocument().addDocumentListener(documentListener);
pivotYField.getDocument().addDocumentListener(documentListener);
// 添加焦点监听,当失去焦点时立即应用
java.awt.event.FocusAdapter focusAdapter = new java.awt.event.FocusAdapter() {
@Override
public void focusLost(java.awt.event.FocusEvent e) {
transformTimer.stop();
applyTransformChanges();
}
};
positionXField.addFocusListener(focusAdapter);
positionYField.addFocusListener(focusAdapter);
rotationField.addFocusListener(focusAdapter);
scaleXField.addFocusListener(focusAdapter);
scaleYField.addFocusListener(focusAdapter);
pivotXField.addFocusListener(focusAdapter);
pivotYField.addFocusListener(focusAdapter);
// 添加回车键监听
java.awt.event.ActionListener enterListener = e -> {
transformTimer.stop();
applyTransformChanges();
};
positionXField.addActionListener(enterListener);
positionYField.addActionListener(enterListener);
rotationField.addActionListener(enterListener);
scaleXField.addActionListener(enterListener);
scaleYField.addActionListener(enterListener);
pivotXField.addActionListener(enterListener);
pivotYField.addActionListener(enterListener);
// 旋转按钮监听器修改(支持多选)
rotate90CWButton.addActionListener(e -> {
if (!selectedParts.isEmpty()) {
renderPanel.getGlContextManager().executeInGLContext(() -> {
Map<ModelPart, Float> oldRotations = new HashMap<>();
Map<ModelPart, Float> newRotations = new HashMap<>();
for (ModelPart part : selectedParts) {
float oldRotation = part.getRotation();
oldRotations.put(part, oldRotation);
float currentRotation = (float) Math.toDegrees(oldRotation);
float newRotation = normalizeAngle(currentRotation + 90.0f);
part.setRotation((float) Math.toRadians(newRotation));
newRotations.put(part, part.getRotation());
}
// 记录多选操作历史
recordMultiPartOperation("ROTATION",
new HashMap<>(oldRotations),
new HashMap<>(newRotations));
renderPanel.repaint();
});
}
});
rotate90CCWButton.addActionListener(e -> {
if (!selectedParts.isEmpty()) {
renderPanel.getGlContextManager().executeInGLContext(() -> {
Map<ModelPart, Float> oldRotations = new HashMap<>();
Map<ModelPart, Float> newRotations = new HashMap<>();
for (ModelPart part : selectedParts) {
float oldRotation = part.getRotation();
oldRotations.put(part, oldRotation);
float currentRotation = (float) Math.toDegrees(oldRotation);
float newRotation = normalizeAngle(currentRotation - 90.0f);
part.setRotation((float) Math.toRadians(newRotation));
newRotations.put(part, part.getRotation());
}
// 记录多选操作历史
recordMultiPartOperation("ROTATION",
new HashMap<>(oldRotations),
new HashMap<>(newRotations));
renderPanel.repaint();
});
}
});
// 翻转按钮监听器修改(支持多选)
flipXButton.addActionListener(e -> {
if (!selectedParts.isEmpty()) {
renderPanel.getGlContextManager().executeInGLContext(() -> {
Map<ModelPart, Vector2f> oldScales = new HashMap<>();
Map<ModelPart, Vector2f> newScales = new HashMap<>();
for (ModelPart part : selectedParts) {
Vector2f oldScale = new Vector2f(part.getScale());
oldScales.put(part, oldScale);
float currentScaleX = part.getScaleX();
float currentScaleY = part.getScaleY();
part.setScale(currentScaleX * -1, currentScaleY);
newScales.put(part, new Vector2f(part.getScale()));
}
// 记录多选操作历史
recordMultiPartOperation("SCALE",
new HashMap<>(oldScales),
new HashMap<>(newScales));
renderPanel.repaint();
});
}
});
flipYButton.addActionListener(e -> {
if (!selectedParts.isEmpty()) {
renderPanel.getGlContextManager().executeInGLContext(() -> {
Map<ModelPart, Vector2f> oldScales = new HashMap<>();
Map<ModelPart, Vector2f> newScales = new HashMap<>();
for (ModelPart part : selectedParts) {
Vector2f oldScale = new Vector2f(part.getScale());
oldScales.put(part, oldScale);
float currentScaleX = part.getScaleX();
float currentScaleY = part.getScaleY();
part.setScale(currentScaleX, currentScaleY * -1);
newScales.put(part, new Vector2f(part.getScale()));
}
// 记录多选操作历史
recordMultiPartOperation("SCALE",
new HashMap<>(oldScales),
new HashMap<>(newScales));
renderPanel.repaint();
});
}
});
// 重置缩放按钮监听器修改(支持多选)
resetScaleButton.addActionListener(e -> {
if (!selectedParts.isEmpty()) {
renderPanel.getGlContextManager().executeInGLContext(() -> {
Map<ModelPart, Vector2f> oldScales = new HashMap<>();
Map<ModelPart, Vector2f> newScales = new HashMap<>();
for (ModelPart part : selectedParts) {
Vector2f oldScale = new Vector2f(part.getScale());
oldScales.put(part, oldScale);
part.setScale(1.0f, 1.0f);
newScales.put(part, new Vector2f(part.getScale()));
}
// 记录多选操作历史
recordMultiPartOperation("SCALE",
new HashMap<>(oldScales),
new HashMap<>(newScales));
renderPanel.repaint();
});
}
});
}
/**
* 记录多部件操作历史
*/
private void recordMultiPartOperation(String operationType, Map<ModelPart, Object> oldValues, Map<ModelPart, Object> newValues) {
if (operationHistory != null && !selectedParts.isEmpty()) {
List<Object> params = new ArrayList<>();
params.add(new ArrayList<>(selectedParts));
params.add(oldValues);
params.add(newValues);
operationHistory.recordOperation("MULTI_" + operationType, params.toArray());
}
}
/**
* 批量应用变换到所有选中部件
*/
private void applyTransformToAllParts(float posX, float posY, float rotationDegrees,
float scaleX, float scaleY, float pivotX, float pivotY) {
// 记录变换前的状态
Map<ModelPart, Object> oldStates = new HashMap<>();
Map<ModelPart, Object> newStates = new HashMap<>();
for (ModelPart part : selectedParts) {
// 记录旧状态
Object[] oldState = new Object[]{
new Vector2f(part.getPosition()),
part.getRotation(),
new Vector2f(part.getScale()),
new Vector2f(part.getPivot())
};
oldStates.put(part, oldState);
// 应用变换
part.setPosition(posX, posY);
part.setRotation((float) Math.toRadians(rotationDegrees));
part.setScale(scaleX, scaleY);
part.setPivot(pivotX, pivotY);
// 记录新状态
Object[] newState = new Object[]{
new Vector2f(part.getPosition()),
part.getRotation(),
new Vector2f(part.getScale()),
new Vector2f(part.getPivot())
};
newStates.put(part, newState);
}
// 记录批量操作历史
recordMultiPartOperation("BATCH_TRANSFORM", oldStates, newStates);
}
/**
* 事件监听器实现 - 当任何选中部件的属性变化时更新UI
*/
@Override
public void trigger(String eventName, Object source) {
if (!(source instanceof ModelPart) || !selectedParts.contains(source)) return;
SwingUtilities.invokeLater(() -> {
// 如果是多选只更新UI但不记录历史避免循环触发
if (selectedParts.size() > 1) {
updatingUI = true;
updateUIForMultiSelection();
updatingUI = false;
} else if (selectedParts.size() == 1) {
updatingUI = true;
try {
ModelPart part = (ModelPart) source;
switch (eventName) {
case "position":
Vector2f position = part.getPosition();
positionXField.setText(String.format("%.2f", position.x));
positionYField.setText(String.format("%.2f", position.y));
break;
case "rotation":
float currentRotation = (float) Math.toDegrees(part.getRotation());
currentRotation = normalizeAngle(currentRotation);
rotationField.setText(String.format("%.2f", currentRotation));
break;
case "scale":
Vector2f scale = part.getScale();
scaleXField.setText(String.format("%.2f", scale.x));
scaleYField.setText(String.format("%.2f", scale.y));
break;
case "pivot":
Vector2f pivot = part.getPivot();
pivotXField.setText(String.format("%.2f", pivot.x));
pivotYField.setText(String.format("%.2f", pivot.y));
break;
}
} catch (Exception ex) {
ex.printStackTrace();
}
updatingUI = false;
}
});
}
/**
* 调度变换更新(延迟处理)
*/
private void scheduleTransformUpdate() {
if (updatingUI || selectedParts.isEmpty()) return;
transformTimer.stop();
transformTimer.start();
}
/**
* 将角度标准化到0-360度范围内
*/
private float normalizeAngle(float degrees) {
degrees = degrees % 360;
if (degrees < 0) {
degrees += 360;
}
return degrees;
}
/**
* 应用所有变换更改(支持多选)
*/
private void applyTransformChanges() {
if (updatingUI || selectedParts.isEmpty()) return;
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
float posX = Float.parseFloat(positionXField.getText());
float posY = Float.parseFloat(positionYField.getText());
float rotationDegrees = Float.parseFloat(rotationField.getText());
rotationDegrees = normalizeAngle(rotationDegrees);
float scaleX = Float.parseFloat(scaleXField.getText());
float scaleY = Float.parseFloat(scaleYField.getText());
float pivotX = Float.parseFloat(pivotXField.getText());
float pivotY = Float.parseFloat(pivotYField.getText());
// 批量应用到所有选中部件
applyTransformToAllParts(posX, posY, rotationDegrees, scaleX, scaleY, pivotX, pivotY);
renderPanel.repaint();
} catch (NumberFormatException ex) {
// 输入无效时恢复之前的值
SwingUtilities.invokeLater(this::updateUIFromSelectedParts);
}
});
}
/**
* 从选中的部件更新UI支持多选
*/
private void updateUIFromSelectedParts() {
if (selectedParts.isEmpty()) return;
updatingUI = true;
try {
if (selectedParts.size() == 1) {
// 单选:显示具体值
ModelPart part = selectedParts.get(0);
updateUIFromSinglePart(part);
isMultiSelection = false;
} else {
// 多选:显示特殊标识或平均值
updateUIForMultiSelection();
isMultiSelection = true;
}
} catch (Exception ex) {
ex.printStackTrace();
}
updatingUI = false;
}
/**
* 从单个部件更新UI
*/
private void updateUIFromSinglePart(ModelPart part) {
// 更新位置
Vector2f position = part.getPosition();
positionXField.setText(String.format("%.2f", position.x));
positionYField.setText(String.format("%.2f", position.y));
// 更新旋转
float currentRotation = (float) Math.toDegrees(part.getRotation());
currentRotation = normalizeAngle(currentRotation);
rotationField.setText(String.format("%.2f", currentRotation));
// 更新缩放
Vector2f scale = part.getScale();
scaleXField.setText(String.format("%.2f", scale.x));
scaleYField.setText(String.format("%.2f", scale.y));
// 更新中心点
Vector2f pivot = part.getPivot();
pivotXField.setText(String.format("%.2f", pivot.x));
pivotYField.setText(String.format("%.2f", pivot.y));
}
/**
* 多选时的UI显示
*/
private void updateUIForMultiSelection() {
// 多选时显示特殊值或平均值
positionXField.setText("[多选]");
positionYField.setText("[多选]");
rotationField.setText("[多选]");
scaleXField.setText("[多选]");
scaleYField.setText("[多选]");
pivotXField.setText("[多选]");
pivotYField.setText("[多选]");
// 或者计算平均值(可选)
// calculateAndDisplayAverageValues();
}
/**
* 设置选中的部件(支持多选)
*/
public void setSelectedParts(List<ModelPart> parts) {
// 移除旧部件的事件监听
for (ModelPart oldPart : selectedParts) {
oldPart.removeEvent(this);
}
this.selectedParts.clear();
if (parts != null) {
this.selectedParts.addAll(parts);
// 添加新部件的事件监听
for (ModelPart newPart : selectedParts) {
newPart.addEvent(this);
}
}
updateUIState();
}
/**
* 添加选中部件
*/
public void addSelectedPart(ModelPart part) {
if (part != null && !selectedParts.contains(part)) {
selectedParts.add(part);
part.addEvent(this);
updateUIState();
}
}
/**
* 移除选中部件
*/
public void removeSelectedPart(ModelPart part) {
if (part != null && selectedParts.contains(part)) {
selectedParts.remove(part);
part.removeEvent(this);
updateUIState();
}
}
/**
* 清空选中部件
*/
public void clearSelectedParts() {
for (ModelPart part : selectedParts) {
part.removeEvent(this);
}
selectedParts.clear();
updateUIState();
}
/**
* 获取当前选中部件
*/
public ModelPart getSelectedPart() {
return selectedParts.isEmpty() ? null : selectedParts.get(0);
}
/**
* 获取所有选中部件
*/
public List<ModelPart> getSelectedParts() {
return new ArrayList<>(selectedParts);
}
/**
* 获取当前选中部件数量
*/
public int getSelectedPartsCount() {
return selectedParts.size();
}
/**
* 检查是否是多选状态
*/
public boolean isMultiSelection() {
return isMultiSelection;
}
private void updateUIState() {
updatingUI = true;
if (!selectedParts.isEmpty()) {
updateUIFromSelectedParts();
setControlsEnabled(true);
} else {
// 清空所有字段
positionXField.setText("0.00");
positionYField.setText("0.00");
rotationField.setText("0.00");
scaleXField.setText("1.00");
scaleYField.setText("1.00");
pivotXField.setText("0.00");
pivotYField.setText("0.00");
setControlsEnabled(false);
isMultiSelection = false;
}
updatingUI = false;
}
private void setControlsEnabled(boolean enabled) {
positionXField.setEnabled(enabled);
positionYField.setEnabled(enabled);
rotationField.setEnabled(enabled);
scaleXField.setEnabled(enabled);
scaleYField.setEnabled(enabled);
pivotXField.setEnabled(enabled);
pivotYField.setEnabled(enabled);
flipXButton.setEnabled(enabled);
flipYButton.setEnabled(enabled);
rotate90CWButton.setEnabled(enabled);
rotate90CCWButton.setEnabled(enabled);
resetScaleButton.setEnabled(enabled);
}
@Override
public void removeNotify() {
super.removeNotify();
// 清理定时器资源和事件监听
if (transformTimer != null) {
transformTimer.stop();
}
// 移除所有部件的事件监听
for (ModelPart part : selectedParts) {
part.removeEvent(this);
}
}
}

View File

@@ -0,0 +1,99 @@
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.systems.Camera;
import org.joml.Vector2f;
public class CameraManagement {
private final ModelRenderPanel modelRenderPanel;
private final GLContextManager glContextManager;
private final WorldManagement worldManagement;
private volatile int lastCameraDragX, lastCameraDragY;
private final Vector2f rotationCenter = new Vector2f();
public static final float ZOOM_STEP = 1.15f; // 每格滚轮的指数因子(>1 放大)
public static final float ZOOM_MIN = 0.1f;
public static final float ZOOM_MAX = 8.0f;
public static final float ROTATION_HANDLE_DISTANCE = 30.0f;
public CameraManagement(ModelRenderPanel modelRenderPanel, GLContextManager glContextManager, WorldManagement worldManagement){
this.modelRenderPanel = modelRenderPanel;
this.glContextManager = glContextManager;
this.worldManagement = worldManagement;
}
public void resizingApplications(int screenX, int screenY, int notches, boolean fine){
glContextManager.executeInGLContext(() -> {
Camera camera = ModelRender.getCamera();
float oldZoom = camera.getZoom();
float[] worldPosBefore = worldManagement.screenToModelCoordinates(screenX, screenY);
if (worldPosBefore == null) return;
double step = fine ? Math.pow(ZOOM_STEP, 0.25) : ZOOM_STEP;
float newZoom = oldZoom;
if (notches > 0) { // 缩小
newZoom /= (float) Math.pow(step, notches);
} else { // 放大
newZoom *= (float) Math.pow(step, -notches);
}
newZoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, newZoom));
if (Math.abs(newZoom - oldZoom) < 1e-6f) {
return;
}
camera.setZoom(newZoom);
float[] worldPosAfter = worldManagement.screenToModelCoordinates(screenX, screenY);
if (worldPosAfter == null) {
camera.setZoom(oldZoom);
return;
}
float panX = worldPosBefore[0] - worldPosAfter[0];
float panY = worldPosBefore[1] - worldPosAfter[1];
camera.move(panX, panY);
glContextManager.setDisplayScale(newZoom);
glContextManager.setTargetScale(newZoom);
});
}
/**
* 计算当前缩放因子(模型单位与屏幕像素的比例)
*/
public float calculateScaleFactor() {
int panelWidth = modelRenderPanel.getWidth();
int panelHeight = modelRenderPanel.getHeight();
if (panelWidth <= 0 || panelHeight <= 0 || glContextManager.getHeight() <= 0 || glContextManager.getHeight() <= 0) {
return 1.0f;
}
// 计算面板与离屏缓冲区的比例
float scaleX = (float) panelWidth / glContextManager.getWidth();
float scaleY = (float) panelHeight / glContextManager.getHeight();
// 基本面板缩放(保持与现有逻辑一致)
float base = Math.min(scaleX, scaleY);
// 乘以平滑的 displayScale使视觉上缩放与检测区域一致
return base * glContextManager.displayScale;
}
public int getLastCameraDragX() {
return lastCameraDragX;
}
public int getLastCameraDragY() {
return lastCameraDragY;
}
public void setLastCameraDragX(int lastCameraDragX) {
this.lastCameraDragX = lastCameraDragX;
}
public void setLastCameraDragY(int lastCameraDragY) {
this.lastCameraDragY = lastCameraDragY;
}
public Vector2f getRotationCenter() {
return rotationCenter;
}
}

View File

@@ -0,0 +1,593 @@
package com.chuangzhou.vivid2D.render.awt.manager;
import com.chuangzhou.vivid2D.render.ModelRender;
import com.chuangzhou.vivid2D.render.model.Model2D;
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11;
import org.lwjgl.system.MemoryUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
public class GLContextManager {
private static final Logger logger = LoggerFactory.getLogger(GLContextManager.class);
private long windowId;
private volatile boolean running = true;
private Thread renderThread;
// 改为可变的宽高以支持动态重建离屏上下文缓冲
private volatile int width;
private volatile int height;
private BufferedImage currentFrame;
private volatile boolean contextInitialized = false;
private final CompletableFuture<Void> contextReady = new CompletableFuture<>();
private final String modelPath;
private final AtomicReference<Model2D> modelRef = new AtomicReference<>();
private BufferedImage lastFrame = null;
private ByteBuffer pixelBuffer = null;
private int[] pixelInts = null;
private int[] argbInts = null;
public volatile float displayScale = 1.0f; // 当前可视缩放(用于检测阈值/角点等)
public volatile float targetScale = 1.0f; // 目标缩放(鼠标滚轮/程序改变时设置)
// 任务队列,用于在 GL 上下文线程执行代码
private final BlockingQueue<Runnable> glTaskQueue = new LinkedBlockingQueue<>();
private final ExecutorService taskExecutor = Executors.newSingleThreadExecutor();
private volatile boolean cameraDragging = false;
private static final float ZOOM_SMOOTHING = 0.18f; // 0..1, 越大收敛越快(建议 0.12-0.25
private RepaintCallback repaintCallback;
public GLContextManager(String modelPath, int width, int height) {
this.modelPath = modelPath;
this.width = width;
this.height = height;
}
public GLContextManager(Model2D model, int width, int height) {
this.modelPath = null;
this.width = width;
this.height = height;
this.modelRef.set(model);
}
public int getHeight() {
return height;
}
public int getWidth() {
return width;
}
/**
* 创建离屏 OpenGL 上下文
*/
private void createOffscreenContext() throws Exception {
// 设置窗口提示
GLFW.glfwDefaultWindowHints();
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3);
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3);
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE);
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GL11.GL_TRUE);
GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, 4);
// 创建离屏窗口(像素尺寸以当前 width/height 为准)
windowId = GLFW.glfwCreateWindow(width, height, "Offscreen Render", MemoryUtil.NULL, MemoryUtil.NULL);
if (windowId == MemoryUtil.NULL) {
throw new Exception("无法创建离屏 OpenGL 上下文");
}
// 设置为当前上下文并初始化
GLFW.glfwMakeContextCurrent(windowId);
GL.createCapabilities();
logger.info("OpenGL context created successfully");
// 然后初始化 RenderSystem
RenderSystem.beginInitialization();
RenderSystem.initRenderThread();
// 使用 RenderSystem 设置视口
RenderSystem.viewport(0, 0, width, height);
// 分配像素读取缓冲
int pixelCount = Math.max(1, width * height);
pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4);
pixelBuffer.order(ByteOrder.nativeOrder());
pixelInts = new int[pixelCount];
argbInts = new int[pixelCount];
// 初始化 ModelRender
ModelRender.initialize();
RenderSystem.finishInitialization();
// 在正确的上下文中加载模型(可能会耗时)
loadModelInContext();
// 标记上下文已初始化并完成通知(只 complete 一次)
contextInitialized = true;
contextReady.complete(null);
logger.info("Offscreen context initialization completed");
}
public void setRepaintCallback(RepaintCallback callback) {
this.repaintCallback = callback;
}
/**
* 在 OpenGL 上下文中加载模型
*/
private void loadModelInContext() {
try {
if (modelPath != null) {
Model2D model = Model2D.loadFromFile(modelPath);
modelRef.set(model);
logger.info("模型加载成功: {}", modelPath);
}
} catch (Exception e) {
logger.error("模型加载失败: {}", e.getMessage(), e);
e.printStackTrace();
}
}
/**
* 启动渲染线程
*/
public void startRendering() {
// 初始化 GLFW
if (!GLFW.glfwInit()) {
throw new RuntimeException("无法初始化 GLFW");
}
renderThread = new Thread(() -> {
try {
createOffscreenContext();
// 等待上下文就绪后再开始渲染循环contextReady 由 createOffscreenContext 完成)
contextReady.get();
// 确保当前线程一直持有该 GL 上下文(避免在每个任务/帧中重复 makeCurrent
GLFW.glfwMakeContextCurrent(windowId);
final long targetNs = 1_000_000_000L / 60L; // 60 FPS
while (running && !GLFW.glfwWindowShouldClose(windowId)) {
long start = System.nanoTime();
processGLTasks();
displayScale += (targetScale - displayScale) * ZOOM_SMOOTHING;
renderFrame();
long elapsed = System.nanoTime() - start;
long sleepNs = targetNs - elapsed;
if (sleepNs > 0) {
LockSupport.parkNanos(sleepNs);
}
}
} catch (Exception e) {
logger.error("渲染线程异常", e);
} finally {
cleanup();
}
});
renderThread.setDaemon(true);
renderThread.setName("GL-Render-Thread");
renderThread.start();
}
/**
* 渲染单帧并读取到 BufferedImage
*/
private void renderFrame() {
if (!contextInitialized || windowId == 0) return;
// 确保在当前上下文中
GLFW.glfwMakeContextCurrent(windowId);
Model2D currentModel = modelRef.get();
if (currentModel != null) {
try {
// 使用 RenderSystem 清除缓冲区
RenderSystem.setClearColor(0.18f, 0.18f, 0.25f, 1f);
RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT);
// 渲染模型
ModelRender.render(1.0f / 60f, currentModel);
// 读取像素数据到 BufferedImage
readPixelsToImage();
} catch (Exception e) {
System.err.println("渲染错误: " + e.getMessage());
renderErrorFrame(e.getMessage());
}
} else {
// 没有模型时显示默认背景
renderDefaultBackground();
}
// 在 Swing EDT 中更新显示
if (repaintCallback != null) {
repaintCallback.repaint();
}
}
/**
* 渲染默认背景
*/
private void renderDefaultBackground() {
RenderSystem.setClearColor(0.1f, 0.1f, 0.15f, 1f);
RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT);
readPixelsToImage();
}
/**
* 渲染错误帧
*/
private void renderErrorFrame(String errorMessage) {
GL11.glClearColor(0.3f, 0.1f, 0.1f, 1f);
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
readPixelsToImage();
BufferedImage errorImage = new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = errorImage.createGraphics();
g2d.setColor(Color.DARK_GRAY);
g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight());
g2d.setColor(Color.RED);
g2d.drawString("渲染错误: " + errorMessage, 10, 20);
g2d.dispose();
currentFrame = errorImage;
}
/**
* 读取 OpenGL 像素数据到 BufferedImage
*/
private void readPixelsToImage() {
try {
final int w = Math.max(1, this.width);
final int h = Math.max(1, this.height);
final int pixelCount = w * h;
// 确保缓冲区大小匹配(可能在 resize 后需要重建)
if (pixelBuffer == null || pixelInts == null || pixelInts.length != pixelCount) {
if (pixelBuffer != null) {
try {
MemoryUtil.memFree(pixelBuffer);
} catch (Throwable ignored) {
}
}
pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4);
pixelBuffer.order(ByteOrder.nativeOrder());
pixelInts = new int[pixelCount];
argbInts = new int[pixelCount];
}
pixelBuffer.clear();
// 从 GPU 读取 RGBA 字节到本地缓冲 - 使用 RenderSystem
RenderSystem.readPixels(0, 0, w, h, RenderSystem.GL_RGBA, RenderSystem.GL_UNSIGNED_BYTE, pixelBuffer);
// 以 int 批量读取(依赖本机字节序),然后转换为带 alpha 的 ARGB 并垂直翻转
IntBuffer ib = pixelBuffer.asIntBuffer();
ib.get(pixelInts, 0, pixelCount);
// 转换并翻转RGBA -> ARGB
for (int y = 0; y < h; y++) {
int srcRow = (h - y - 1) * w;
int dstRow = y * w;
for (int x = 0; x < w; x++) {
int rgba = pixelInts[srcRow + x];
// 提取字节(考虑 native order按 RGBA 存放)
int r = (rgba >> 0) & 0xFF;
int g = (rgba >> 8) & 0xFF;
int b = (rgba >> 16) & 0xFF;
int a = (rgba >> 24) & 0xFF;
// 组合为 ARGB (BufferedImage 使用 ARGB)
argbInts[dstRow + x] = (a << 24) | (r << 16) | (g << 8) | b;
}
}
// 使用一次 setRGB 写入 BufferedImage比逐像素 setRGB 快)
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
image.setRGB(0, 0, w, h, argbInts, 0, w);
currentFrame = image;
lastFrame = image;
} catch (Exception e) {
logger.error("读取像素数据错误", e);
// 创建错误图像(保持原逻辑)
BufferedImage errorImage = new BufferedImage(Math.max(1, this.width), Math.max(1, this.height), BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = errorImage.createGraphics();
g2d.setColor(Color.BLACK);
g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight());
g2d.setColor(Color.RED);
g2d.drawString("像素读取失败", 10, 20);
g2d.dispose();
currentFrame = errorImage;
}
}
/**
* 处理 GL 上下文任务队列
*/
private void processGLTasks() {
Runnable task;
while ((task = glTaskQueue.poll()) != null) {
try {
// 在渲染线程中执行,渲染线程已将上下文设为 current
task.run();
} catch (Exception e) {
logger.error("执行 GL 任务时出错", e);
}
}
}
/**
* 重新设置面板大小
* <p>
* 说明:当 Swing 面板被放大时,需要同时调整离屏 GLFW 窗口像素大小、GL 视口以及重分配像素读取缓冲,
* 否则将把较小分辨率的图像拉伸到更大面板上导致模糊。
*/
public void resize(int newWidth, int newHeight) {
executeInGLContext(() -> {
if (contextInitialized && windowId != 0) {
this.width = Math.max(1, newWidth);
this.height = Math.max(1, newHeight);
GLFW.glfwMakeContextCurrent(windowId);
GLFW.glfwSetWindowSize(windowId, this.width, this.height);
RenderSystem.viewport(0, 0, this.width, this.height);
ModelRender.setViewport(this.width, this.height);
try {
if (pixelBuffer != null) {
MemoryUtil.memFree(pixelBuffer);
pixelBuffer = null;
}
} catch (Throwable ignored) {}
int pixelCount = Math.max(1, this.width * this.height);
pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4);
pixelBuffer.order(ByteOrder.nativeOrder());
pixelInts = new int[pixelCount];
argbInts = new int[pixelCount];
currentFrame = null;
} else {
this.width = Math.max(1, newWidth);
this.height = Math.max(1, newHeight);
}
});
}
/**
* 等待渲染上下文准备就绪
*/
public CompletableFuture<Void> waitForContext() {
return contextReady;
}
/**
* 检查渲染上下文是否已初始化
* @return true 表示已初始化false 表示未初始化
*/
public boolean isContextInitialized() {
return contextInitialized;
}
/**
* 检查是否正在运行
*/
public boolean isRunning() {
return running && contextInitialized;
}
/**
* 清理资源
*/
public void dispose() {
running = false;
cameraDragging = false;
// 停止任务执行器
taskExecutor.shutdown();
if (renderThread != null) {
try {
renderThread.join(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
cleanup();
}
private void cleanup() {
// 清理 ModelRender
try {
if (ModelRender.isInitialized()) {
ModelRender.cleanup();
logger.info("ModelRender 已清理");
}
} catch (Exception e) {
logger.error("清理 ModelRender 时出错: {}", e.getMessage());
}
if (windowId != 0) {
try {
GLFW.glfwDestroyWindow(windowId);
} catch (Throwable ignored) {
}
windowId = 0;
}
// 释放像素缓冲
try {
if (pixelBuffer != null) {
MemoryUtil.memFree(pixelBuffer);
pixelBuffer = null;
}
} catch (Throwable t) {
logger.warn("释放 pixelBuffer 时出错: {}", t.getMessage());
}
// 终止 GLFW注意如果应用中还有其他 GLFW 窗口,这里会影响它们)
try {
GLFW.glfwTerminate();
} catch (Throwable ignored) {
}
logger.info("OpenGL 资源已清理");
}
/**
* 在 GL 上下文线程上异步执行任务
*
* @param task 要在 GL 上下文线程中执行的任务
* @return CompletableFuture 用于获取任务执行结果
*/
public CompletableFuture<Void> executeInGLContext(Runnable task) {
CompletableFuture<Void> future = new CompletableFuture<>();
if (!running) {
future.completeExceptionally(new IllegalStateException("渲染线程已停止"));
return future;
}
// 等待上下文就绪后再提交任务
contextReady.thenRun(() -> {
try {
// 使用 put 保证任务不会被丢弃,如果队列已满会阻塞调用者直到可入队
glTaskQueue.put(() -> {
try {
task.run();
future.complete(null);
} catch (Exception e) {
future.completeExceptionally(e);
}
});
} catch (Exception e) {
future.completeExceptionally(e);
}
});
return future;
}
/**
* 在 GL 上下文线程上异步执行任务并返回结果
*
* @param task 要在 GL 上下文线程中执行的有返回值的任务
* @return CompletableFuture 用于获取任务执行结果
*/
public <T> CompletableFuture<T> executeInGLContext(Callable<T> task) {
CompletableFuture<T> future = new CompletableFuture<>();
if (!running) {
future.completeExceptionally(new IllegalStateException("渲染线程已停止"));
return future;
}
contextReady.thenRun(() -> {
try {
boolean offered = glTaskQueue.offer(() -> {
try {
T result = task.call();
future.complete(result);
} catch (Exception e) {
future.completeExceptionally(e);
}
}, 5, TimeUnit.SECONDS);
if (!offered) {
future.completeExceptionally(new TimeoutException("任务队列已满无法在5秒内添加任务"));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
future.completeExceptionally(new IllegalStateException("任务提交被中断", e));
} catch (Exception e) {
future.completeExceptionally(e);
}
});
return future;
}
/**
* 同步在 GL 上下文线程上执行任务(会阻塞当前线程直到任务完成)
*
* @param task 要在 GL 上下文线程中执行的任务
* @throws Exception 如果任务执行出错
*/
public void executeInGLContextSync(Runnable task) throws Exception {
if (!running) {
throw new IllegalStateException("渲染线程已停止");
}
CompletableFuture<Void> future = executeInGLContext(task);
future.get(10, TimeUnit.SECONDS); // 设置超时时间
}
/**
* 同步在 GL 上下文线程上执行任务并返回结果(会阻塞当前线程直到任务完成)
*
* @param task 要在 GL 上下文线程中执行的有返回值的任务
* @return 任务执行结果
* @throws Exception 如果任务执行出错或超时
*/
public <T> T executeInGLContextSync(Callable<T> task) throws Exception {
if (!running) {
throw new IllegalStateException("渲染线程已停止");
}
CompletableFuture<T> future = executeInGLContext(task);
return future.get(10, TimeUnit.SECONDS); // 设置超时时间
}
public void setDisplayScale(float scale) {
this.displayScale = scale;
}
public void setTargetScale(float scale) {
this.targetScale = scale;
}
public float getDisplayScale() {
return displayScale;
}
public float getTargetScale() {
return targetScale;
}
public interface RepaintCallback {
void repaint();
}
/**
* 获取当前帧
* @return 当前帧
*/
public BufferedImage getCurrentFrame() {
return currentFrame;
}
/**
* 获取上一帧
* @return 上一帧
*/
public BufferedImage getLastFrame() {
return lastFrame;
}
public boolean isCameraDragging() {
return cameraDragging;
}
public void setCameraDragging(boolean cameraDragging) {
this.cameraDragging = cameraDragging;
}
}

View File

@@ -0,0 +1,423 @@
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;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.HashMap;
import java.util.Map;
public class KeyboardManager {
private static final Logger logger = LoggerFactory.getLogger(KeyboardManager.class);
private final ModelRenderPanel panel;
private volatile boolean shiftPressed = false;
private volatile boolean ctrlPressed = false;
// 存储自定义快捷键
private final Map<String, KeyStroke> customShortcuts = new HashMap<>();
private final Map<String, AbstractAction> customActions = new HashMap<>();
public KeyboardManager(ModelRenderPanel panel){
this.panel = panel;
}
/**
* 初始化键盘快捷键
*/
public void initKeyboardShortcuts() {
// 获取输入映射和动作映射
InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = panel.getActionMap();
// 撤回快捷键Ctrl+Z
registerShortcut("undo", KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK),
new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
panel.getStatusRecordManagement().undo();
}
});
// 重做快捷键Ctrl+Y 或 Ctrl+Shift+Z
registerShortcut("redo", KeyStroke.getKeyStroke(KeyEvent.VK_Y, KeyEvent.CTRL_DOWN_MASK),
new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
panel.getStatusRecordManagement().redo();
}
});
registerShortcut("redo2", KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK),
new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
panel.getStatusRecordManagement().redo();
}
});
// 清除历史记录Ctrl+Shift+Delete
registerShortcut("clearHistory", KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK),
new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
panel.getStatusRecordManagement().clearHistory();
}
});
// 摄像机重置快捷键Ctrl+R
registerShortcut("resetCamera", KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK),
new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
panel.resetCamera();
logger.info("重置摄像机");
}
});
// 摄像机启用/禁用快捷键Ctrl+E
registerShortcut("toggleCamera", KeyStroke.getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK),
new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
Camera camera = ModelRender.getCamera();
boolean newState = !camera.isEnabled();
camera.setEnabled(newState);
logger.info("{}摄像机", newState ? "启用" : "禁用");
}
});
// 注册工具快捷键
registerToolShortcuts();
// 设置键盘监听器
setupKeyListeners();
}
/**
* 注册工具快捷键
*/
private void registerToolShortcuts() {
// 木偶变形工具快捷键Ctrl+P
registerShortcut("puppetTool", KeyStroke.getKeyStroke(KeyEvent.VK_P, KeyEvent.CTRL_DOWN_MASK),
new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
panel.switchTool("木偶变形工具");
logger.info("切换到木偶变形工具");
}
});
// 顶点变形工具快捷键Ctrl+T
registerShortcut("vertexTool", KeyStroke.getKeyStroke(KeyEvent.VK_T, KeyEvent.CTRL_DOWN_MASK),
new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
panel.switchTool("顶点变形工具");
logger.info("切换到顶点变形工具");
}
});
// 选择工具快捷键Ctrl+S
registerShortcut("selectionTool", KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_DOWN_MASK),
new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
panel.switchTool("选择工具");
logger.info("切换到选择工具");
}
});
}
/**
* 注册自定义快捷键
* @param actionName 动作名称
* @param keyStroke 按键组合
* @param action 对应的动作
*/
public void registerShortcut(String actionName, KeyStroke keyStroke, AbstractAction action) {
InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = panel.getActionMap();
// 如果已存在相同的快捷键,先移除
if (customShortcuts.containsKey(actionName)) {
KeyStroke oldKeyStroke = customShortcuts.get(actionName);
inputMap.remove(oldKeyStroke);
}
// 注册新的快捷键
inputMap.put(keyStroke, actionName);
actionMap.put(actionName, action);
// 保存到自定义快捷键映射
customShortcuts.put(actionName, keyStroke);
customActions.put(actionName, action);
logger.debug("注册快捷键: {} -> {}", keyStroke, actionName);
}
/**
* 注销自定义快捷键
* @param actionName 动作名称
*/
public void unregisterShortcut(String actionName) {
InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = panel.getActionMap();
if (customShortcuts.containsKey(actionName)) {
KeyStroke keyStroke = customShortcuts.get(actionName);
inputMap.remove(keyStroke);
actionMap.remove(actionName);
customShortcuts.remove(actionName);
customActions.remove(actionName);
logger.debug("注销快捷键: {}", actionName);
}
}
/**
* 注册工具快捷键
* @param toolName 工具名称
* @param keyStroke 按键组合
*/
public void registerToolShortcut(String toolName, KeyStroke keyStroke) {
String actionName = "tool_" + toolName;
registerShortcut(actionName, keyStroke, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
panel.switchTool(toolName);
logger.info("切换到工具: {}", toolName);
}
});
}
/**
* 注册工具循环切换快捷键
* @param keyStroke 按键组合
*/
public void registerToolCycleShortcut(KeyStroke keyStroke) {
registerShortcut("cycleTools", keyStroke, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
panel.getToolManagement().switchToPreviousTool();
Tool currentTool = panel.getCurrentTool();
if (currentTool != null) {
logger.info("切换到上一个工具: {}", currentTool.getToolName());
}
}
});
}
/**
* 设置键盘监听器
*/
private void setupKeyListeners() {
panel.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
handleKeyPressed(e);
}
@Override
public void keyReleased(KeyEvent e) {
handleKeyReleased(e);
}
});
}
/**
* 处理按键按下
*/
private void handleKeyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
// 更新修饰键状态
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开启顶点渲染");
}
}
}
// 处理功能快捷键
if (ctrlPressed) {
switch (keyCode) {
case KeyEvent.VK_A:
// Ctrl+A 全选
e.consume();
panel.selectAllMeshes();
logger.debug("全选所有网格");
break;
case KeyEvent.VK_D:
// Ctrl+D 取消选择
e.consume();
panel.clearSelectedMeshes();
logger.debug("取消所有选择");
break;
case KeyEvent.VK_1:
// Ctrl+1 切换到第一个工具
e.consume();
switchToToolByIndex(0);
break;
case KeyEvent.VK_2:
// Ctrl+2 切换到第二个工具
e.consume();
switchToToolByIndex(1);
break;
case KeyEvent.VK_3:
// Ctrl+3 切换到第三个工具
e.consume();
switchToToolByIndex(2);
break;
}
}
// 单独按键处理
switch (keyCode) {
case KeyEvent.VK_ESCAPE:
// ESC 键取消所有选择或退出工具
e.consume();
if (panel.getToolManagement().hasActiveTool() &&
!panel.getToolManagement().getCurrentTool().getToolName().equals("选择工具")) {
// 如果有激活的工具且不是选择工具,切换到选择工具
panel.switchTool("选择工具");
logger.info("按ESC键切换到选择工具");
} else {
// 否则取消所有选择
panel.clearSelectedMeshes();
logger.info("按ESC键取消所有选择");
}
break;
case KeyEvent.VK_SPACE:
// 空格键临时切换到手型工具(用于移动视图)
if (!e.isConsumed()) {
// 这里可以添加空格键拖拽视图的功能
// 需要与鼠标管理中键拖拽功能配合
}
break;
}
}
/**
* 处理按键释放
*/
private void handleKeyReleased(KeyEvent e) {
int keyCode = e.getKeyCode();
// 更新修饰键状态
if (keyCode == KeyEvent.VK_SHIFT) {
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关闭顶点渲染");
}
}
}
}
/**
* 根据索引切换到工具
*/
private void switchToToolByIndex(int index) {
java.util.List<Tool> tools = panel.getToolManagement().getRegisteredTools();
if (index >= 0 && index < tools.size()) {
Tool tool = tools.get(index);
panel.switchTool(tool.getToolName());
logger.info("切换到工具: {}", tool.getToolName());
}
}
/**
* 获取所有注册的快捷键信息
*/
public Map<String, String> getShortcutInfo() {
Map<String, String> info = new HashMap<>();
for (Map.Entry<String, KeyStroke> entry : customShortcuts.entrySet()) {
info.put(entry.getKey(), entry.getValue().toString());
}
return info;
}
/**
* 重新加载所有快捷键
*/
public void reloadShortcuts() {
// 清除所有自定义快捷键
InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = panel.getActionMap();
for (String actionName : customShortcuts.keySet()) {
KeyStroke keyStroke = customShortcuts.get(actionName);
inputMap.remove(keyStroke);
actionMap.remove(actionName);
}
customShortcuts.clear();
customActions.clear();
// 重新初始化
initKeyboardShortcuts();
logger.info("重新加载所有快捷键");
}
/**
* 获取Shift键状态
*/
public boolean getIsShiftPressed(){
return shiftPressed;
}
/**
* 获取Ctrl键状态
*/
public boolean getIsCtrlPressed(){
return ctrlPressed;
}
/**
* 清理资源
*/
public void dispose() {
// 移除所有键盘监听器
for (KeyListener listener : panel.getKeyListeners()) {
panel.removeKeyListener(listener);
}
// 清除所有快捷键
InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = panel.getActionMap();
for (String actionName : customShortcuts.keySet()) {
KeyStroke keyStroke = customShortcuts.get(actionName);
inputMap.remove(keyStroke);
actionMap.remove(actionName);
}
customShortcuts.clear();
customActions.clear();
logger.info("键盘管理器已清理");
}
}

View File

@@ -0,0 +1,70 @@
package com.chuangzhou.vivid2D.render.awt.manager;
import com.chuangzhou.vivid2D.render.model.Model2D;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import org.joml.Vector2f;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class LayerOperationManager {
private final Model2D model;
public LayerOperationManager(Model2D model) {
this.model = model;
}
public void addLayer(String name) {
model.createPart(name);
model.markNeedsUpdate();
}
public void removeLayer(ModelPart part) {
if (part == null) return;
List<ModelPart> parts = model.getParts();
if (parts != null) parts.remove(part);
Map<String, ModelPart> partMap = model.getPartMap();
if (partMap != null) partMap.remove(part.getName());
model.markNeedsUpdate();
}
public void moveLayer(List<ModelPart> visualOrder) {
List<ModelPart> newModelParts = new ArrayList<>(visualOrder.size());
for (int i = visualOrder.size() - 1; i >= 0; i--) {
newModelParts.add(visualOrder.get(i));
}
replaceModelPartsList(newModelParts);
model.markNeedsUpdate();
}
public void setLayerOpacity(ModelPart part, float opacity) {
part.setOpacity(opacity);
model.markNeedsUpdate();
}
public void setLayerVisibility(ModelPart part, boolean visible) {
part.setVisible(visible);
model.markNeedsUpdate();
}
private void replaceModelPartsList(List<ModelPart> newParts) {
if (model == null) return;
try {
java.lang.reflect.Field partsField = model.getClass().getDeclaredField("parts");
partsField.setAccessible(true);
Object old = partsField.get(model);
if (old instanceof List) {
((List) old).clear();
((List) old).addAll(newParts);
} else {
partsField.set(model, newParts);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,92 @@
package com.chuangzhou.vivid2D.render.awt.manager;
import com.chuangzhou.vivid2D.render.ModelRender;
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
import java.awt.*;
import java.awt.event.*;
public class MouseManagement {
private final ModelRenderPanel modelRenderPanel;
private final GLContextManager glContextManager;
private final CameraManagement cameraManagement;
private final KeyboardManager keyboardManager;
public MouseManagement(ModelRenderPanel modelRenderPanel,
GLContextManager glContextManager,
CameraManagement cameraManagement,
KeyboardManager keyboardManager){
this.modelRenderPanel = modelRenderPanel;
this.glContextManager = glContextManager;
this.cameraManagement = cameraManagement;
this.keyboardManager = keyboardManager;
}
/**
* 添加鼠标事件监听器
*/
public void addMouseListeners() {
modelRenderPanel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
modelRenderPanel.handleMouseClick(e);
}
@Override
public void mousePressed(MouseEvent e) {
modelRenderPanel.handleMousePressed(e);
}
@Override
public void mouseReleased(MouseEvent e) {
modelRenderPanel.handleMouseReleased(e);
}
@Override
public void mouseExited(MouseEvent e) {
modelRenderPanel.setCursor(Cursor.getDefaultCursor());
}
});
modelRenderPanel.addMouseWheelListener(new MouseWheelListener() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
if (!glContextManager.isContextInitialized()) return;
final int screenX = e.getX();
final int screenY = e.getY();
final int notches = e.getWheelRotation();
final boolean fine = e.isShiftDown();
cameraManagement.resizingApplications(screenX, screenY, notches, fine);
}
});
modelRenderPanel.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
modelRenderPanel.handleMouseMove(e);
}
@Override
public void mouseDragged(MouseEvent e) {
modelRenderPanel.handleMouseDragged(e);
}
});
modelRenderPanel.addMouseWheelListener(e -> {
int notches = e.getWheelRotation();
boolean fine = (e.isShiftDown() || keyboardManager.getIsShiftPressed()); // 支持 Shift 更精细控制
double step = fine ? Math.pow(CameraManagement.ZOOM_STEP, 0.25) : CameraManagement.ZOOM_STEP;
if (notches > 0) {
// 滚轮下:缩小
glContextManager.targetScale *= Math.pow(1.0 / step, notches);
} else if (notches < 0) {
// 滚轮上:放大
glContextManager.targetScale *= Math.pow(step, -notches);
}
glContextManager.targetScale = Math.max(CameraManagement.ZOOM_MIN, Math.min(CameraManagement.ZOOM_MAX, glContextManager.targetScale));
});
modelRenderPanel.setFocusable(true);
modelRenderPanel.requestFocusInWindow();
}
}

View File

@@ -0,0 +1,164 @@
package com.chuangzhou.vivid2D.render.awt.manager;
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal;
import com.chuangzhou.vivid2D.render.model.ModelPart;
import org.joml.Vector2f;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class StatusRecordManagement {
private final ModelRenderPanel panel;
private final OperationHistoryGlobal operationHistory;
public StatusRecordManagement(ModelRenderPanel panel, OperationHistoryGlobal operationHistory){
this.operationHistory = operationHistory;
this.panel = panel;
}
/**
* 重做操作
*/
public void redo() {
if (operationHistory != null && operationHistory.canRedo()) {
panel.getGlContextManager().executeInGLContext(() -> {
boolean success = operationHistory.redo();
if (success) {
panel.repaint();
System.out.println("重做: " + operationHistory.getRedoDescription());
}
});
} else {
System.out.println("没有可重做的操作");
}
}
/**
* 记录位置变化操作
*/
private void recordPositionChange(ModelPart part, Vector2f oldPosition, Vector2f newPosition) {
if (operationHistory != null && part != null) {
operationHistory.recordOperation("SET_POSITION", part, oldPosition, newPosition);
}
}
/**
* 记录缩放变化操作
*/
private void recordScaleChange(ModelPart part, Vector2f oldScale, Vector2f newScale) {
if (operationHistory != null && part != null) {
operationHistory.recordOperation("SET_SCALE", part, oldScale, newScale);
}
}
/**
* 记录旋转变化操作
*/
private void recordRotationChange(ModelPart part, float oldRotation, float newRotation) {
if (operationHistory != null && part != null) {
operationHistory.recordOperation("SET_ROTATION", part, oldRotation, newRotation);
}
}
/**
* 记录中心点变化操作
*/
private void recordPivotChange(ModelPart part, Vector2f oldPivot, Vector2f newPivot) {
if (operationHistory != null && part != null) {
operationHistory.recordOperation("SET_PIVOT", part, oldPivot, newPivot);
}
}
/**
* 记录拖拽结束操作
*/
public void recordDragEnd(List<ModelPart> parts, Map<ModelPart, Vector2f> startPositions) {
if (operationHistory != null && parts != null && !parts.isEmpty()) {
List<Object> params = new ArrayList<>();
params.add(parts);
params.add(startPositions);
// 添加当前位置
for (ModelPart part : parts) {
params.add(part.getPosition());
}
operationHistory.recordOperation("DRAG_PART_END", params.toArray());
}
}
/**
* 记录调整大小结束操作
*/
public void recordResizeEnd(List<ModelPart> parts, Map<ModelPart, Vector2f> startScales) {
if (operationHistory != null && parts != null && !parts.isEmpty()) {
List<Object> params = new ArrayList<>();
params.add(parts);
params.add(startScales);
// 添加当前缩放
for (ModelPart part : parts) {
params.add(part.getScale());
}
operationHistory.recordOperation("RESIZE_PART_END", params.toArray());
}
}
/**
* 记录旋转结束操作
*/
public void recordRotateEnd(List<ModelPart> parts, Map<ModelPart, Float> startRotations) {
if (operationHistory != null && parts != null && !parts.isEmpty()) {
List<Object> params = new ArrayList<>();
params.add(parts);
params.add(startRotations);
// 添加当前旋转
for (ModelPart part : parts) {
params.add(part.getRotation());
}
operationHistory.recordOperation("ROTATE_PART_END", params.toArray());
}
}
/**
* 记录移动中心点结束操作
*/
public void recordMovePivotEnd(List<ModelPart> parts, Map<ModelPart, Vector2f> startPivots) {
if (operationHistory != null && parts != null && !parts.isEmpty()) {
List<Object> params = new ArrayList<>();
params.add(parts);
params.add(startPivots);
// 添加当前中心点
for (ModelPart part : parts) {
params.add(part.getPivot());
}
operationHistory.recordOperation("MOVE_PIVOT_END", params.toArray());
}
}
/**
* 撤回操作
*/
public void undo() {
if (operationHistory != null && operationHistory.canUndo()) {
panel.getGlContextManager().executeInGLContext(() -> {
boolean success = operationHistory.undo();
if (success) {
panel.repaint();
System.out.println("撤回: " + operationHistory.getUndoDescription());
}
});
} else {
System.out.println("没有可撤回的操作");
}
}
/**
* 清除操作历史
*/
public void clearHistory() {
if (operationHistory != null) {
operationHistory.clearHistory();
System.out.println("操作历史已清除");
}
}
}

View File

@@ -0,0 +1,244 @@
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.util.Texture;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ThumbnailManager {
private static final int THUMBNAIL_WIDTH = 48;
private static final int THUMBNAIL_HEIGHT = 48;
private final Map<ModelPart, BufferedImage> thumbnailCache = new HashMap<>();
private ModelRenderPanel renderPanel;
public ThumbnailManager(ModelRenderPanel renderPanel) {
this.renderPanel = renderPanel;
}
public BufferedImage getThumbnail(ModelPart part) {
return thumbnailCache.get(part);
}
public void generateThumbnail(ModelPart part) {
if (renderPanel == null) return;
try {
BufferedImage thumbnail = renderPanel.getGlContextManager()
.executeInGLContext(() -> renderPartThumbnail(part))
.get();
if (thumbnail != null) {
thumbnailCache.put(part, thumbnail);
}
} catch (Exception e) {
thumbnailCache.put(part, createDefaultThumbnail());
}
}
public void removeThumbnail(ModelPart part) {
thumbnailCache.remove(part);
}
public void clearCache() {
thumbnailCache.clear();
}
/**
* 渲染单个部件的缩略图
*/
private BufferedImage renderPartThumbnail(ModelPart part) {
if (renderPanel == null) return createDefaultThumbnail();
try {
return createThumbnailForPart(part);
} catch (Exception e) {
e.printStackTrace();
return createDefaultThumbnail();
}
}
private BufferedImage createThumbnailForPart(ModelPart part) {
BufferedImage thumbnail = new BufferedImage(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = thumbnail.createGraphics();
// 设置抗锯齿
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
// 绘制背景
g2d.setColor(new Color(40, 40, 40));
g2d.fillRect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
try {
// 尝试获取部件的纹理
Texture texture = null;
List<Mesh2D> meshes = part.getMeshes();
if (meshes != null && !meshes.isEmpty()) {
for (Mesh2D mesh : meshes) {
texture = mesh.getTexture();
if (texture != null) break;
}
}
if (texture != null && !texture.isDisposed()) {
// 获取纹理的 BufferedImage
BufferedImage textureImage = textureToBufferedImage(texture);
if (textureImage != null) {
// 计算缩放比例以保持宽高比
int imgWidth = textureImage.getWidth();
int imgHeight = textureImage.getHeight();
if (imgWidth > 0 && imgHeight > 0) {
float scale = Math.min(
(float)(THUMBNAIL_WIDTH - 8) / imgWidth,
(float)(THUMBNAIL_HEIGHT - 8) / imgHeight
);
int scaledWidth = (int)(imgWidth * scale);
int scaledHeight = (int)(imgHeight * scale);
int x = (THUMBNAIL_WIDTH - scaledWidth) / 2;
int y = (THUMBNAIL_HEIGHT - scaledHeight) / 2;
// 绘制纹理图片
g2d.drawImage(textureImage, x, y, scaledWidth, scaledHeight, null);
// 绘制边框
g2d.setColor(Color.WHITE);
g2d.drawRect(x, y, scaledWidth - 1, scaledHeight - 1);
}
}
}
} catch (Exception e) {
System.err.println("生成缩略图失败: " + part.getName() + " - " + e.getMessage());
}
// 如果部件不可见,绘制红色斜线覆盖
if (!part.isVisible()) {
g2d.setColor(new Color(255, 0, 0, 128)); // 半透明红色
g2d.setStroke(new BasicStroke(3));
g2d.drawLine(2, 2, THUMBNAIL_WIDTH - 2, THUMBNAIL_HEIGHT - 2);
g2d.drawLine(THUMBNAIL_WIDTH - 2, 2, 2, THUMBNAIL_HEIGHT - 2);
}
g2d.dispose();
return thumbnail;
}
/**
* 将Texture转换为BufferedImage
*/
private BufferedImage textureToBufferedImage(Texture texture) {
try {
// 确保纹理有像素数据缓存
texture.ensurePixelDataCached();
if (!texture.hasPixelData()) {
System.err.println("纹理没有像素数据: " + texture.getName());
return null;
}
byte[] pixelData = texture.getPixelData();
if (pixelData == null || pixelData.length == 0) {
return null;
}
int width = texture.getWidth();
int height = texture.getHeight();
Texture.TextureFormat format = texture.getFormat();
int components = format.getComponents();
// 创建BufferedImage
BufferedImage image;
switch (components) {
case 1: // 单通道
image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
break;
case 3: // RGB
image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
break;
case 4: // RGBA
image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
break;
default:
System.err.println("不支持的纹理格式组件数量: " + components);
return null;
}
// 将像素数据复制到BufferedImage同时翻转垂直方向
if (components == 4) {
// RGBA格式 - 垂直翻转
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int srcY = height - 1 - y; // 翻转Y坐标
for (int x = 0; x < width; x++) {
int srcIndex = (srcY * width + x) * 4;
int dstIndex = y * width + x;
int r = pixelData[srcIndex] & 0xFF;
int g = pixelData[srcIndex + 1] & 0xFF;
int b = pixelData[srcIndex + 2] & 0xFF;
int a = pixelData[srcIndex + 3] & 0xFF;
pixels[dstIndex] = (a << 24) | (r << 16) | (g << 8) | b;
}
}
image.setRGB(0, 0, width, height, pixels, 0, width);
} else if (components == 3) {
// RGB格式 - 垂直翻转
for (int y = 0; y < height; y++) {
int srcY = height - 1 - y; // 翻转Y坐标
for (int x = 0; x < width; x++) {
int srcIndex = (srcY * width + x) * 3;
int r = pixelData[srcIndex] & 0xFF;
int g = pixelData[srcIndex + 1] & 0xFF;
int b = pixelData[srcIndex + 2] & 0xFF;
int rgb = (r << 16) | (g << 8) | b;
image.setRGB(x, y, rgb);
}
}
} else if (components == 1) {
// 单通道格式 - 垂直翻转
for (int y = 0; y < height; y++) {
int srcY = height - 1 - y; // 翻转Y坐标
for (int x = 0; x < width; x++) {
int srcIndex = srcY * width + x;
int gray = pixelData[srcIndex] & 0xFF;
int rgb = (gray << 16) | (gray << 8) | gray;
image.setRGB(x, y, rgb);
}
}
}
return image;
} catch (Exception e) {
System.err.println("转换纹理到BufferedImage失败: " + texture.getName() + " - " + e.getMessage());
e.printStackTrace();
return null;
}
}
private BufferedImage createDefaultThumbnail() {
BufferedImage thumbnail = new BufferedImage(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = thumbnail.createGraphics();
g2d.setColor(new Color(60, 60, 60));
g2d.fillRect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
g2d.setColor(Color.GRAY);
g2d.drawRect(2, 2, THUMBNAIL_WIDTH - 5, THUMBNAIL_HEIGHT - 5);
g2d.setColor(Color.WHITE);
g2d.drawString("?", THUMBNAIL_WIDTH/2 - 4, THUMBNAIL_HEIGHT/2 + 4);
g2d.dispose();
return thumbnail;
}
}

View File

@@ -0,0 +1,329 @@
package com.chuangzhou.vivid2D.render.awt.manager;
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
import com.chuangzhou.vivid2D.render.awt.tools.SelectionTool;
import com.chuangzhou.vivid2D.render.awt.tools.Tool;
import com.chuangzhou.vivid2D.render.model.util.manager.RanderToolsManager;
import com.chuangzhou.vivid2D.render.model.util.tools.RanderTools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* 工具管理器
* 负责注册、管理和切换各种编辑工具
*/
public class ToolManagement {
private static final Logger logger = LoggerFactory.getLogger(ToolManagement.class);
private final ModelRenderPanel renderPanel;
private final Map<String, Tool> registeredTools;
private final RanderToolsManager randerToolsManager;
private Tool currentTool = null;
private Tool previousTool = null;
// 默认工具(选择工具)
private final Tool defaultTool;
public ToolManagement(ModelRenderPanel renderPanel, RanderToolsManager randerToolsManager) {
this.renderPanel = renderPanel;
this.registeredTools = new ConcurrentHashMap<>();
this.randerToolsManager = randerToolsManager;
// 创建默认选择工具
this.defaultTool = new SelectionTool(renderPanel);
registerTool(defaultTool);
// 设置默认工具为当前工具
switchTool(defaultTool.getToolName());
}
// ================== 工具注册管理 ==================
/**
* 注册工具
*/
public void registerTool(Tool tool, RanderTools randerTools) {
if (tool == null) {
logger.warn("尝试注册空工具");
return;
}
String toolName = tool.getToolName();
if (registeredTools.containsKey(toolName)) {
logger.warn("工具已存在: {}", toolName);
return;
}
registeredTools.put(toolName, tool);
randerToolsManager.bindToolWithRanderTools(tool, randerTools);
tool.setAssociatedRanderTools(randerTools);
logger.info("注册工具: {}", toolName);
}
/**
* 注册工具
*/
public void registerTool(Tool tool) {
if (tool == null) {
logger.warn("尝试注册空工具");
return;
}
String toolName = tool.getToolName();
if (registeredTools.containsKey(toolName)) {
logger.warn("工具已存在: {}", toolName);
return;
}
registeredTools.put(toolName, tool);
logger.info("注册工具: {}", toolName);
}
/**
* 注销工具
*/
public void unregisterTool(String toolName) {
Tool tool = registeredTools.get(toolName);
if (tool == null) {
logger.warn("工具不存在: {}", toolName);
return;
}
// 如果要注销的工具是当前工具,先停用它
if (currentTool == tool) {
switchToDefaultTool();
}
tool.dispose();
registeredTools.remove(toolName);
logger.info("注销工具: {}", toolName);
}
/**
* 获取所有注册的工具
*/
public List<Tool> getRegisteredTools() {
return new ArrayList<>(registeredTools.values());
}
/**
* 根据名称获取工具
*/
public Tool getTool(String toolName) {
return registeredTools.get(toolName);
}
// ================== 工具切换管理 ==================
/**
* 切换到指定工具
*/
public boolean switchTool(String toolName) {
Tool targetTool = registeredTools.get(toolName);
if (targetTool == null) {
logger.warn("工具不存在: {}", toolName);
return false;
}
return switchTool(targetTool);
}
/**
* 切换到指定工具实例
*/
public boolean switchTool(Tool tool) {
if (tool == null) {
logger.warn("尝试切换到空工具");
return false;
}
// 检查工具是否可用
if (!tool.isAvailable()) {
logger.warn("工具不可用: {}", tool.getToolName());
return false;
}
// 如果已经是当前工具,直接返回
if (currentTool == tool) {
return true;
}
// 停用当前工具
if (currentTool != null) {
currentTool.deactivate();
previousTool = currentTool;
}
// 激活新工具
currentTool = tool;
currentTool.activate();
// 更新光标
updateCursor();
logger.info("切换到工具: {}", currentTool.getToolName());
return true;
}
/**
* 切换到默认工具
*/
public void switchToDefaultTool() {
switchTool(defaultTool);
}
/**
* 切换到上一个工具
*/
public void switchToPreviousTool() {
if (previousTool != null && previousTool.isAvailable()) {
switchTool(previousTool);
} else {
switchToDefaultTool();
}
}
/**
* 获取当前工具
*/
public Tool getCurrentTool() {
return currentTool;
}
/**
* 获取上一个工具
*/
public Tool getPreviousTool() {
return previousTool;
}
/**
* 获取默认工具
*/
public Tool getDefaultTool() {
return defaultTool;
}
// ================== 事件转发 ==================
/**
* 处理鼠标按下事件
*/
public void handleMousePressed(MouseEvent e, float modelX, float modelY) {
if (currentTool != null) {
currentTool.onMousePressed(e, modelX, modelY);
}
}
/**
* 处理鼠标释放事件
*/
public void handleMouseReleased(MouseEvent e, float modelX, float modelY) {
if (currentTool != null) {
currentTool.onMouseReleased(e, modelX, modelY);
}
}
/**
* 处理鼠标拖拽事件
*/
public void handleMouseDragged(MouseEvent e, float modelX, float modelY) {
if (currentTool != null) {
currentTool.onMouseDragged(e, modelX, modelY);
}
}
/**
* 处理鼠标移动事件
*/
public void handleMouseMoved(MouseEvent e, float modelX, float modelY) {
if (currentTool != null) {
currentTool.onMouseMoved(e, modelX, modelY);
}
}
/**
* 处理鼠标点击事件
*/
public void handleMouseClicked(MouseEvent e, float modelX, float modelY) {
if (currentTool != null) {
currentTool.onMouseClicked(e, modelX, modelY);
}
}
/**
* 处理鼠标双击事件
*/
public void handleMouseDoubleClicked(MouseEvent e, float modelX, float modelY) {
if (currentTool != null) {
currentTool.onMouseDoubleClicked(e, modelX, modelY);
}
}
// ================== 工具状态管理 ==================
/**
* 更新光标
*/
private void updateCursor() {
if (currentTool != null) {
Cursor cursor = currentTool.getToolCursor();
if (cursor != null) {
renderPanel.setCursor(cursor);
}
}
}
/**
* 检查是否有工具处于激活状态
*/
public boolean hasActiveTool() {
return currentTool != null && currentTool.isActive();
}
/**
* 停用所有工具
*/
public void deactivateAllTools() {
for (Tool tool : registeredTools.values()) {
if (tool.isActive()) {
tool.deactivate();
}
}
currentTool = null;
renderPanel.setCursor(Cursor.getDefaultCursor());
}
/**
* 清理所有工具资源
*/
public void dispose() {
deactivateAllTools();
for (Tool tool : registeredTools.values()) {
tool.dispose();
}
registeredTools.clear();
logger.info("工具管理器已清理");
}
/**
* 获取工具统计信息
*/
public String getToolStatistics() {
int activeCount = 0;
for (Tool tool : registeredTools.values()) {
if (tool.isActive()) {
activeCount++;
}
}
return String.format("工具统计: 注册%d个, 激活%d个, 当前工具: %s",
registeredTools.size(), activeCount,
currentTool != null ? currentTool.getToolName() : "");
}
}

View File

@@ -0,0 +1,31 @@
package com.chuangzhou.vivid2D.render.awt.manager;
import com.chuangzhou.vivid2D.render.ModelRender;
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
import org.joml.Vector2f;
public class WorldManagement {
private final ModelRenderPanel modelRenderPanel;
private final GLContextManager glContextManager;
public WorldManagement(ModelRenderPanel modelRenderPanel, GLContextManager glContextManager) {
this.modelRenderPanel = modelRenderPanel;
this.glContextManager = glContextManager;
}
/**
* 将屏幕坐标转换为模型坐标
*/
public float[] screenToModelCoordinates(int screenX, int screenY) {
if (!glContextManager.isContextInitialized() || glContextManager.getWidth() <= 0 || glContextManager.getHeight() <= 0) return null;
float glX = (float) screenX * glContextManager.getWidth() / modelRenderPanel.getWidth();
float glY = (float) screenY * glContextManager.getHeight() / modelRenderPanel.getHeight();
float ndcX = (2.0f * glX) / glContextManager.getWidth() - 1.0f;
float ndcY = 1.0f - (2.0f * glY) / glContextManager.getHeight();
Vector2f camOffset = ModelRender.getCameraOffset();
float zoom = ModelRender.getCamera().getZoom();
float modelX = (ndcX * glContextManager.getWidth() / (2.0f * zoom)) + camOffset.x;
float modelY = (ndcY * glContextManager.getHeight() / (-2.0f * zoom)) + camOffset.y;
return new float[]{modelX, modelY};
}
}

View File

@@ -0,0 +1,400 @@
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);
// 判断是否按住Ctrl键决定是否创建顶点
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();
}
}

View File

@@ -0,0 +1,297 @@
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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,147 @@
package com.chuangzhou.vivid2D.render.awt.tools;
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
import com.chuangzhou.vivid2D.render.model.util.tools.RanderTools;
import java.awt.*;
import java.awt.event.MouseEvent;
/**
* 工具抽象基类
* 所有编辑工具都应继承此类
*/
public abstract class Tool {
protected ModelRenderPanel renderPanel;
protected String toolName;
protected String toolDescription;
protected boolean isActive = false;
// 关联的渲染工具对象
protected RanderTools associatedRanderTools;
public Tool(ModelRenderPanel renderPanel, String toolName, String toolDescription) {
this.renderPanel = renderPanel;
this.toolName = toolName;
this.toolDescription = toolDescription;
}
// ================== 生命周期方法 ==================
/**
* 激活工具
*/
public abstract void activate();
/**
* 停用工具
*/
public abstract void deactivate();
/**
* 工具是否处于激活状态
*/
public boolean isActive() {
return isActive;
}
// ================== 事件处理方法 ==================
/**
* 处理鼠标按下事件
*/
public abstract void onMousePressed(MouseEvent e, float modelX, float modelY);
/**
* 处理鼠标释放事件
*/
public abstract void onMouseReleased(MouseEvent e, float modelX, float modelY);
/**
* 处理鼠标拖拽事件
*/
public abstract void onMouseDragged(MouseEvent e, float modelX, float modelY);
/**
* 处理鼠标移动事件
*/
public abstract void onMouseMoved(MouseEvent e, float modelX, float modelY);
/**
* 处理鼠标点击事件
*/
public abstract void onMouseClicked(MouseEvent e, float modelX, float modelY);
/**
* 处理鼠标双击事件
*/
public abstract void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY);
// ================== 工具状态方法 ==================
/**
* 获取工具名称
*/
public String getToolName() {
return toolName;
}
/**
* 获取工具描述
*/
public String getToolDescription() {
return toolDescription;
}
/**
* 获取工具光标
*/
public abstract Cursor getToolCursor();
/**
* 工具是否可用
*/
public boolean isAvailable() {
return true;
}
/**
* 清理工具资源
*/
public void dispose() {
// 子类可重写此方法清理资源
if (associatedRanderTools != null) {
associatedRanderTools = null;
}
}
// ================== 新增方法与RanderToolsManager集成 ==================
/**
* 设置关联的渲染工具
* @param randerTools 渲染工具对象
*/
public void setAssociatedRanderTools(RanderTools randerTools) {
this.associatedRanderTools = randerTools;
}
/**
* 获取关联的渲染工具
* @return 关联的渲染工具对象可能为null
*/
public RanderTools getAssociatedRanderTools() {
return associatedRanderTools;
}
/**
* 检查是否有关联的渲染工具
* @return true如果有关联的渲染工具
*/
public boolean hasAssociatedRanderTools() {
return associatedRanderTools != null;
}
@Override
public String toString() {
return toolName;
}
}

Some files were not shown because too many files have changed in this diff Show More