Build native card inspector and secure state; hide manual analysis and unify automatic follow (validation in progress)
这个提交包含在:
1 父节点
5793e3de49
当前提交
f4c0d608a4
20 个文件已更改
+1492
-236
没有与搜索条件相匹配的文件
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
基于本仓库的 FxPlug4 初始工程制作的人像美化滤镜,支持 Motion 和 Final Cut Pro。源码使用 Objective-C、Vision、Core Image 与 Metal。
|
基于本仓库的 FxPlug4 初始工程制作的人像美化滤镜,支持 Motion 和 Final Cut Pro。源码使用 Objective-C、Vision、Core Image 与 Metal。
|
||||||
|
|
||||||
功能包括磨皮、肤色、细节、牙齿美白、眼睛提亮、九类脸型调整、画面选脸、三目标分析跟踪扩展和蒙版预览。原版 MSE18V2 的静态分析依据、近似实现与尚未对齐的部分见 [分析说明](docs/analysis/REA-分析与复刻说明.md)。本版尚未完成原版逐像素与复杂运动跟踪对照。
|
功能包括磨皮、肤色、细节、牙齿美白、眼睛提亮、九类脸型调整、自动人脸跟随和多人处理。原版 MSE18V2 的静态分析依据、近似实现与尚未对齐的部分见 [分析说明](docs/analysis/REA-分析与复刻说明.md)。本版尚未完成原版逐像素与复杂运动跟踪对照。
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
正式安装路径为 `/Library/Plug-Ins/FxPlug/beatygril.app`。Motion 滤镜分类为「大鱼」,滤镜名「大鱼人像美化」。Final Cut Pro 在「效果 → 大鱼 → 大鱼人像美化」中使用配套 Motion 效果,模板目录必须同时保留 `.moef` 与 `Drop Zone.tiff`。美化会随当前帧的人脸自动更新,不需要先点分析;默认脸型跟随主要人脸,开启多人高速模式后对多脸变形。可按需调中频磨皮、肤色与脸型;「只处理选中人脸」用于画面重选目标。三个目标的向前/向后分析属于可选扩展,数据可随工程保存。
|
正式安装路径为 `/Library/Plug-Ins/FxPlug/beatygril.app`。Motion 滤镜分类为「大鱼」,滤镜名「大鱼人像美化」。Final Cut Pro 在「效果 → 大鱼 → 大鱼人像美化」中使用配套 Motion 效果,模板目录必须同时保留 `.moef` 与 `Drop Zone.tiff`。美化会随当前帧的人脸自动更新,不需要点击分析。界面以磨皮、美白、瘦脸三张卡片呈现,默认跟随主要人脸;多人模式对多脸变形。单项眼睛可暂时停用并保留数值,右侧箭头恢复默认;磨皮半径、饱和度、锐化和整体混合等收在「更多设置」。旧手动分析与调试参数仅保留隐藏兼容,实际输出统一使用实时自动跟随。
|
||||||
|
|
||||||
## 开发和恢复
|
## 开发和恢复
|
||||||
|
|
||||||
@@ -36,3 +36,9 @@ xcodebuild -project beatygril/beatygril.xcodeproj -scheme 'Wrapper Application'
|
|||||||
旧版分析记录无法区分是否包含额外翻转,新版保留原字符串但暂用实时跟随;重新分析即可写入新坐标标记并恢复离线跟踪。最终Motion/FCP画面由用户自行测试。
|
旧版分析记录无法区分是否包含额外翻转,新版保留原字符串但暂用实时跟随;重新分析即可写入新坐标标记并恢复离线跟踪。最终Motion/FCP画面由用户自行测试。
|
||||||
|
|
||||||
1.0.6(8)已完成正式签名、Apple公证、票据、安装和模板同步。904项完整回归及独立远程恢复核验通过;最终宿主视觉由用户自测。交付回执在`docs/verification/v1.0.6/`,恢复标签为`v1.0.6`。
|
1.0.6(8)已完成正式签名、Apple公证、票据、安装和模板同步。904项完整回归及独立远程恢复核验通过;最终宿主视觉由用户自测。交付回执在`docs/verification/v1.0.6/`,恢复标签为`v1.0.6`。
|
||||||
|
|
||||||
|
## 1.1.0 原生卡片面板
|
||||||
|
|
||||||
|
使用AppKit自绘卡片、滑块和开关,原生数值输入、肤色圆点、单项/分组启停及重置。FCP仅发布自定义面板210和两项版权信息,撤掉平铺参数与手动分析板块。新面板使用独立NSSecureCoding状态对象完整保存设置,支持复制、撤销数据和数值插值;未编辑时保持旧文档参数值。旧分析数据不参与实时渲染。
|
||||||
|
|
||||||
|
`./scripts/test-inspector.sh`验证参数路由、保存恢复、事务、绕过/重置和离屏布局;`python3 scripts/inspector-template.py audit Templates/大鱼人像美化/大鱼人像美化.moef`验证模板自定义数据和发布入口。离屏截图和模拟宿主检查不能替代Motion/FCP的实际嵌入、键盘/VoiceOver和工程重开验收,仍由用户自测。当前阶段尚未替换已安装1.0.6。
|
||||||
+206
-51
@@ -1,9 +1,28 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"pluginUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
|
"pluginUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
|
||||||
"filterObject": "510000005",
|
"filterObject": "510000005",
|
||||||
"visiblePublished": 46,
|
"visiblePublished": 1,
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": 210,
|
||||||
|
"name": "",
|
||||||
|
"channel": "./210",
|
||||||
|
"kind": "custom-archive",
|
||||||
|
"class": "PGInspectorState",
|
||||||
|
"published": true,
|
||||||
|
"hasEdits": false,
|
||||||
|
"schema": 1,
|
||||||
|
"templateFlags": "12901679104",
|
||||||
|
"panelCards": [
|
||||||
|
"磨皮",
|
||||||
|
"美白",
|
||||||
|
"瘦脸"
|
||||||
|
],
|
||||||
|
"secondarySection": "更多",
|
||||||
|
"valueStorage": "Own secure keyed archive; Custom Data inside BlindDataObject, DataIsLegacy=false",
|
||||||
|
"nativeLoadingValidation": "pending user"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": 70,
|
"id": 70,
|
||||||
"name": "启用磨皮",
|
"name": "启用磨皮",
|
||||||
@@ -12,7 +31,9 @@
|
|||||||
"kind": "bool",
|
"kind": "bool",
|
||||||
"default": 1,
|
"default": 1,
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
@@ -22,7 +43,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 1,
|
"default": 1,
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 12,
|
"id": 12,
|
||||||
@@ -32,7 +55,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0.5,
|
"value": 0.5,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
@@ -42,7 +67,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 8,
|
"default": 8,
|
||||||
"value": 8,
|
"value": 8,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 30,
|
"id": 30,
|
||||||
@@ -52,7 +79,9 @@
|
|||||||
"kind": "bool",
|
"kind": "bool",
|
||||||
"default": 1,
|
"default": 1,
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 71,
|
"id": 71,
|
||||||
@@ -62,7 +91,9 @@
|
|||||||
"kind": "bool",
|
"kind": "bool",
|
||||||
"default": 1,
|
"default": 1,
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 42,
|
"id": 42,
|
||||||
@@ -72,7 +103,9 @@
|
|||||||
"kind": "popup",
|
"kind": "popup",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12901744656"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
@@ -82,7 +115,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 14,
|
"id": 14,
|
||||||
@@ -92,7 +127,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 15,
|
"id": 15,
|
||||||
@@ -102,7 +139,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 41,
|
"id": 41,
|
||||||
@@ -112,7 +151,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 31,
|
"id": 31,
|
||||||
@@ -122,7 +163,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 32,
|
"id": 32,
|
||||||
@@ -132,7 +175,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 73,
|
"id": 73,
|
||||||
@@ -142,7 +187,9 @@
|
|||||||
"kind": "bool",
|
"kind": "bool",
|
||||||
"default": 1,
|
"default": 1,
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 17,
|
"id": 17,
|
||||||
@@ -152,7 +199,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
@@ -162,7 +211,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 72,
|
"id": 72,
|
||||||
@@ -172,7 +223,9 @@
|
|||||||
"kind": "bool",
|
"kind": "bool",
|
||||||
"default": 1,
|
"default": 1,
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 18,
|
"id": 18,
|
||||||
@@ -182,7 +235,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 33,
|
"id": 33,
|
||||||
@@ -192,7 +247,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 22,
|
"id": 22,
|
||||||
@@ -202,7 +259,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 23,
|
"id": 23,
|
||||||
@@ -212,7 +271,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 24,
|
"id": 24,
|
||||||
@@ -222,7 +283,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 25,
|
"id": 25,
|
||||||
@@ -232,7 +295,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 26,
|
"id": 26,
|
||||||
@@ -242,7 +307,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 27,
|
"id": 27,
|
||||||
@@ -252,7 +319,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 28,
|
"id": 28,
|
||||||
@@ -262,7 +331,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 29,
|
"id": 29,
|
||||||
@@ -272,7 +343,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 20,
|
"id": 20,
|
||||||
@@ -282,7 +355,9 @@
|
|||||||
"kind": "bool",
|
"kind": "bool",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 21,
|
"id": 21,
|
||||||
@@ -292,7 +367,9 @@
|
|||||||
"kind": "point",
|
"kind": "point",
|
||||||
"default": 0.5,
|
"default": 0.5,
|
||||||
"value": 0.5,
|
"value": 0.5,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905943072"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 39,
|
"id": 39,
|
||||||
@@ -302,7 +379,9 @@
|
|||||||
"kind": "bool",
|
"kind": "bool",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 40,
|
"id": 40,
|
||||||
@@ -312,7 +391,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 1.8,
|
"default": 1.8,
|
||||||
"value": 1.8,
|
"value": 1.8,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 45,
|
"id": 45,
|
||||||
@@ -322,7 +403,9 @@
|
|||||||
"kind": "bool",
|
"kind": "bool",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 46,
|
"id": 46,
|
||||||
@@ -332,7 +415,9 @@
|
|||||||
"kind": "popup",
|
"kind": "popup",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12901744656"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 50,
|
"id": 50,
|
||||||
@@ -342,7 +427,9 @@
|
|||||||
"kind": "button",
|
"kind": "button",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 51,
|
"id": 51,
|
||||||
@@ -352,7 +439,9 @@
|
|||||||
"kind": "button",
|
"kind": "button",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 52,
|
"id": 52,
|
||||||
@@ -362,7 +451,9 @@
|
|||||||
"kind": "button",
|
"kind": "button",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 53,
|
"id": 53,
|
||||||
@@ -372,7 +463,9 @@
|
|||||||
"kind": "status",
|
"kind": "status",
|
||||||
"default": "选择画面中的人脸后开始分析",
|
"default": "选择画面中的人脸后开始分析",
|
||||||
"value": "选择画面中的人脸后开始分析",
|
"value": "选择画面中的人脸后开始分析",
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 19,
|
"id": 19,
|
||||||
@@ -382,7 +475,9 @@
|
|||||||
"kind": "bool",
|
"kind": "bool",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 35,
|
"id": 35,
|
||||||
@@ -392,7 +487,9 @@
|
|||||||
"kind": "bool",
|
"kind": "bool",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
@@ -402,7 +499,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 148,
|
"default": 148,
|
||||||
"value": 148,
|
"value": 148,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 8,
|
"id": 8,
|
||||||
@@ -412,7 +511,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0.6,
|
"default": 0.6,
|
||||||
"value": 0.6,
|
"value": 0.6,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 9,
|
"id": 9,
|
||||||
@@ -422,7 +523,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 0.4,
|
"default": 0.4,
|
||||||
"value": 0.4,
|
"value": 0.4,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 10,
|
"id": 10,
|
||||||
@@ -432,7 +535,9 @@
|
|||||||
"kind": "bool",
|
"kind": "bool",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 43,
|
"id": 43,
|
||||||
@@ -442,7 +547,9 @@
|
|||||||
"kind": "bool",
|
"kind": "bool",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 44,
|
"id": 44,
|
||||||
@@ -452,7 +559,9 @@
|
|||||||
"kind": "popup",
|
"kind": "popup",
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12901744656"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
@@ -462,7 +571,9 @@
|
|||||||
"kind": "float",
|
"kind": "float",
|
||||||
"default": 1,
|
"default": 1,
|
||||||
"value": 1,
|
"value": 1,
|
||||||
"published": true
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 60,
|
"id": 60,
|
||||||
@@ -470,7 +581,9 @@
|
|||||||
"channel": "./60",
|
"channel": "./60",
|
||||||
"kind": "hidden-string",
|
"kind": "hidden-string",
|
||||||
"value": "",
|
"value": "",
|
||||||
"published": false
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 61,
|
"id": 61,
|
||||||
@@ -478,7 +591,9 @@
|
|||||||
"channel": "./61",
|
"channel": "./61",
|
||||||
"kind": "hidden-string",
|
"kind": "hidden-string",
|
||||||
"value": "",
|
"value": "",
|
||||||
"published": false
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 62,
|
"id": 62,
|
||||||
@@ -486,7 +601,9 @@
|
|||||||
"channel": "./62",
|
"channel": "./62",
|
||||||
"kind": "hidden-string",
|
"kind": "hidden-string",
|
||||||
"value": "",
|
"value": "",
|
||||||
"published": false
|
"published": false,
|
||||||
|
"hiddenCompatibility": true,
|
||||||
|
"templateFlags": "12905938976"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"effectSource": {
|
"effectSource": {
|
||||||
@@ -498,5 +615,43 @@
|
|||||||
"dropZone": 1,
|
"dropZone": 1,
|
||||||
"path": "Drop Zone.tiff",
|
"path": "Drop Zone.tiff",
|
||||||
"resourceBundled": true
|
"resourceBundled": true
|
||||||
|
},
|
||||||
|
"informationalPublished": 2,
|
||||||
|
"totalPublished": 3,
|
||||||
|
"legacyDefinitionCount": 49,
|
||||||
|
"legacyGroupsPreserved": [
|
||||||
|
100,
|
||||||
|
101,
|
||||||
|
102,
|
||||||
|
103,
|
||||||
|
104,
|
||||||
|
105
|
||||||
|
],
|
||||||
|
"nativeHiddenPolicy": "All legacy groups and parameters remain registered with SDK kFxParameterFlag_HIDDEN. Known scalar/point/string hidden XML flags are copied from observed native structures; group/popup/button private flags are preserved.",
|
||||||
|
"publishSettings": [
|
||||||
|
{
|
||||||
|
"object": "510000011",
|
||||||
|
"channel": "./2/100",
|
||||||
|
"name": "FCP 版本作者→"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "510000012",
|
||||||
|
"channel": "./2/100",
|
||||||
|
"name": "更多模板浏览器访问→"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "510000005",
|
||||||
|
"channel": "./210",
|
||||||
|
"name": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaults": {
|
||||||
|
"legacyMidtoneDefault": 0,
|
||||||
|
"legacyMidtoneValue": 0.5,
|
||||||
|
"customStateHasEdits": false,
|
||||||
|
"initialEffectiveMidtone": 0.5,
|
||||||
|
"publishOSC": 0,
|
||||||
|
"manualAnalysisVisible": false,
|
||||||
|
"trackingMode": "automatic primary realtime follow"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+26
-20
@@ -1,37 +1,43 @@
|
|||||||
# 大鱼人像美化 FCP 效果模板
|
# 大鱼人像美化 FCP 效果模板
|
||||||
|
|
||||||
版本:1.0.6。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。模板必须与已安装的大鱼人像美化 FxPlug APP 配套使用。
|
版本:1.1.0。滤镜 UUID:`FB14B0E6-59B2-4836-916F-CD0C559E9241`。需配套同版大鱼人像美化 FxPlug APP。
|
||||||
|
|
||||||
## 默认数值
|
## 三卡片与自动跟随
|
||||||
|
|
||||||
模板首次应用时,“中频磨皮”为 **50%(0.5)**;直接在 Motion 添加裸 FxPlug 滤镜时为 **0%(0)**。模板保存了 `default=0 / value=0.5`,保留裸插件的重置基准;其他功能参数与插件默认一致。整体混合为100%,脸型、肤色提亮、锐化、牙齿和眼睛提亮初始为0,跟踪初始关闭。此模板不预置人脸身份或跟踪记录。
|
FCP 发布列表仅保留作者、网站两个空绑定展示项,以及一个空名称的自定义面板入口 `./210`,共3项。面板内提供“磨皮、美白、瘦脸”三张卡片和“更多”附加设置。主体人脸由当前画面自动选择并实时跟随,无需手动前向/后向分析。多人模式、预览框、防跳脸与各项调节由插件自定义面板承载。
|
||||||
|
|
||||||
## 参数与输入
|
原有46个平铺功能入口已撤下。旧49个参数定义及六组路径仍保留供旧项目兼容,插件以 SDK 隐藏标记注册;旧手动分析按钮、跟踪目标/状态、选脸点和目标数据不再发布为界面入口。隐藏的目标数据60/61/62不删除。发布 OSC 初始为0;旧OSC类注册由插件保留。
|
||||||
|
|
||||||
- 保留磨皮、肤色、细节、脸型、人脸选择与跟踪、蒙版与输出六个原生 FxPlug 参数组,发布46项可见功能参数,包括前向/后向分析及清除按钮。
|
## 默认与旧项目
|
||||||
- 目标1–3的隐藏分析数据以参数60/61/62保留在滤镜节点中,不发布成用户控件;分析完成后由插件写入宿主参数并随工程保存。
|
|
||||||
- 选脸位置发布为原生二维参数,并启用滤镜的“发布 OSC”宿主通道。跟踪按钮、OSC与工程关闭重开后的数据恢复须由实际 FCP 验证。
|
|
||||||
- 画面只含一个标准 Effect Source(Type=3、Drop Zone=1),其 Source Media 指向 clip `510000006`,clip 位于 footage `510000007`。`pathURL=Drop Zone.tiff` 为模板同目录内的相对引用,标准1200×1200占位图已随模板附带,没有外部安装目录依赖、示例人像、旧滤镜或新动画。FCP时间线片段的实际替换已在独立验收项目中确认正常出图。
|
|
||||||
- 作者与网站为两个原生空绑定展示菜单,不改变渲染。
|
|
||||||
|
|
||||||
建议模板安装位置:`~/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/`。APP正式安装与模板安装、Motion/FCP原生验收由主任务处理。
|
模板首次应用时,旧原生“中频磨皮”仍保存 `default=0 / value=0.5`,即初始局部磨皮50%;直接添加裸插件时为0%。自定义 `PGInspectorState` 初始 `hasEdits=NO`,因此读取并保留旧原生参数,不会用面板归档中的默认0覆盖模板0.5或旧项目数值。面板经过编辑后由自定义状态持久保存调节值。此模板不预置任何人脸身份或跟踪记录。
|
||||||
|
|
||||||
## 结构来源与静态验证
|
## 自有状态归档
|
||||||
|
|
||||||
Apple项目、群组、Effect Source和滤镜节点骨架来源于用户自有“大鱼去黑.moef”;仅保留这些原生结构,已替换滤镜UUID、名称、版本和全部功能参数。FxPlug组/字符串/点参数的原生结构交叉参照自有Number与Zoom and Pan样本;作者/网站空绑定来自ae-to-motion技能的原生版权样本。
|
参数210使用本插件生成的 `PGInspectorState` 安全键控归档:内部 `Custom Data` 对象、外部 `BlindDataObject` 与 `DataIsLegacy=false`,再按已核对的Motion文本编码和长度封装。保存 `schema=1 / edited=false`,不使用原版私有数据、类名、品牌资源或空 `<dataValue>` 猜测初始化。默认和当前归档相同,可完整解码并往返核验。
|
||||||
|
|
||||||
`PARAMETERS.json`逐项记录参数ID、组、发布通道及初始值;`VALIDATION.json`记录本次静态核验。48个发布路径(46功能+2信息)均能反查实际对象和通道,实体ID无冲突,三个隐藏数据通道未发布;必要占位媒体以相对路径随包附带,保存选择状态为0个内容对象且焦点为项目。技能`motion_selection.py check`与`audit_motion.py --require-no-selection`通过。
|
该编码依据原生文件静态结构及独立Foundation归档检查;1.1.0自定义面板在Motion/FCP中的实际创建、显示、交互、撤销和关闭重开恢复仍待用户验收,不能以XML可解析或归档解码成功代替宿主加载通过。
|
||||||
|
|
||||||
结构核验之外,已用实际素材在FCP和Motion完成加载、画面及提亮回归测试。详细范围及尚未覆盖的边界见 [验证记录](../../docs/验证记录.md)。
|
## 输入、媒体与版权
|
||||||
|
|
||||||
## 前版输入链错误与本次修复
|
输入链沿用已修复的标准 Effect Source(Type=3、Drop Zone=1):Source Media→clip `510000006`→footage `510000007`→相对文件 `Drop Zone.tiff`。该占位图随模板附带,420128字节,SHA256 `a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e`。没有外部素材、示例人像或新动画。
|
||||||
|
|
||||||
前版生成稿(SHA256 `3b7984e11a92487834ab67121fd3814bf13c22b77a70851298efa84206f91478`)将效果源的 Source Media 设为0,并删除了原生clip/footage。用户在FCP应用后实测整幅黑屏。这是模板装配错误;保留Type=3本身并不能证明FCP输入替换链完整。此前XML解析、发布路径和无缺媒体检查通过,也没有覆盖真实FCP画面输入,不能当作该模板已经可用的证据。
|
作者与网站仍是同一个Rig下的两个无受控参数菜单:“FCP 版本作者→/大鱼先森Media”和“更多模板浏览器访问→/dayu.media”。仅展示信息,不改变渲染。
|
||||||
|
|
||||||
黑屏修复恢复标准效果源clip/footage和Source Media关联,并随包加入标准`Drop Zone.tiff`,保留49个插件参数、48个发布入口与Type=3/Drop Zone=1。1.0.4同时包含独立的提亮蒙版与选脸坐标修复。该占位图从Apple Motion的Ozone.framework标准资源复制,实际运行只引用本模板内的相对副本。图像SHA256为 `a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e`,420128字节。
|
建议安装位置:`~/Movies/Motion Templates.localized/Effects.localized/大鱼/大鱼人像美化/`。本轮不安装模板,不操作Motion/FCP,由主任务完成正式交付,用户自测原生面板。
|
||||||
|
|
||||||
本次已经静态核验Source Media→clip→相对文件链存在、文件字节一致、48项发布仍解析、隐藏数据未发布和保存选择状态;修复后已在FCP独立验收项目新增实例,确认输入替换、实际人像和提亮画面正常;Mix=0逐字节直通由宿主适配器IOSurface回归测试覆盖,未将其混称为FCP截图逐像素核验。
|
## 验证边界与历史修复
|
||||||
|
|
||||||
1.0.5将瘦脸改为76点驱动的17组局部位移,供用户在Motion/FCP自测最大瘦脸与转脸。旧跟踪记录若没有全点阵,需重新分析后才有瘦脸,或关闭“使用跟踪数据”采用当前帧自动检测。
|
`PARAMETERS.json`记录新旧参数和3条发布路径;`VALIDATION.json`分开记录结构/归档核验与当前宿主待验项。内容选择状态保持为空,效果源、clip/footage、占位资源和版权绑定结构均保留。
|
||||||
|
|
||||||
1.0.6移除Core Image/IOSurface链按宿主原点重复翻转,修正下巴三组局部位移及正负方向。旧版未标记坐标空间的分析记录保留但不再重放,自动回退实时跟随;需要离线记录时重新分析当前目标。
|
早期模板曾误删clip/footage、将Source Media设为0,用户在FCP实测黑屏;随后恢复完整标准输入链,历史版本已实际出图。该历史结果只支持输入链,不能证明新1.1.0自定义面板已加载。1.0.5加入76点17组局部瘦脸;1.0.6移除IOSurface重复翻转并修复下巴三组局部位移。旧未标记坐标空间的跟踪数据保留但不重放,自动回退实时跟随。
|
||||||
|
|
||||||
|
## 可复现的只读核验
|
||||||
|
|
||||||
|
在仓库根目录执行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 scripts/inspector-template.py audit "Templates/大鱼人像美化/大鱼人像美化.moef"
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本可提取内外归档并核验自有类、schema、初始状态、3条发布和媒体引用;`encode --inner <本插件生成的Custom Data归档> --text-output <输出文件>` 可重新包装文本值,不启动宿主。其结果明确保留宿主加载待验状态。
|
||||||
+114
-12
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.6",
|
"version": "1.1.0",
|
||||||
"sha256": "30ca8edf36801264c5de2c753c40b720aba8052633620713a12401098fcd2409",
|
"sha256": "e1c0cff1dd3541ed97cab2cb39a76ad4f28dc659b3be7991cc7ac92dd8ea090b",
|
||||||
"filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
|
"filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
|
||||||
"publishedFunctionalControls": 46,
|
"publishedFunctionalControls": 1,
|
||||||
"informationalControls": 2,
|
"informationalControls": 2,
|
||||||
"hiddenTrackingSlots": 3,
|
"hiddenTrackingSlots": 3,
|
||||||
"publishedPathsResolved": 48,
|
"publishedPathsResolved": 3,
|
||||||
"sourceType": 3,
|
"sourceType": 3,
|
||||||
"sourceMedia": "510000006",
|
"sourceMedia": "510000006",
|
||||||
"clip": "510000006",
|
"clip": "510000006",
|
||||||
@@ -23,14 +23,6 @@
|
|||||||
"problem": "Previous generated template removed the standard source clip and could not establish the native effect-source input chain. XML/reference checks did not validate FCP timeline replacement.",
|
"problem": "Previous generated template removed the standard source clip and could not establish the native effect-source input chain. XML/reference checks did not validate FCP timeline replacement.",
|
||||||
"change": "Restored the user-owned reference template clip/footage skeleton with new unique IDs; restored the Source Media link; bundled standard Drop Zone.tiff; filter, publications and Type=3/Drop Zone=1 preserved."
|
"change": "Restored the user-owned reference template clip/footage skeleton with new unique IDs; restored the Source Media link; bundled standard Drop Zone.tiff; filter, publications and Type=3/Drop Zone=1 preserved."
|
||||||
},
|
},
|
||||||
"hostValidation": {
|
|
||||||
"date": "2026-10-21",
|
|
||||||
"FCP": "Restored input-chain template shows supplied real video; effect parameters and forward analysis responded in independent QA project.",
|
|
||||||
"Motion": "1.0.4 installed formal app loaded the real-video QA project and rendered brightness/geometry with a primary box at different playhead positions; offline tracking disabled.",
|
|
||||||
"scope": "1.0.6 coordinate/chin repair native visual QA is reserved for user self-testing; no Motion/FCP UI operations. Historical host tests remain scoped to their versions.",
|
|
||||||
"version105": "Formal 1.0.5 (7) signed/notarized/stapled/installed and PluginKit formal path verified. User closed hosts before replacement; no Motion/FCP UI operated. Native visual testing pending user.",
|
|
||||||
"version106": "Formal 1.0.6 (8) universal Release signed/notarized/stapled/installed, identity/Gatekeeper/formal PluginKit path verified. User closed hosts; companion moef and TIFF synchronized and byte-verified. Generated outer APPs and notarization ZIP removed. No Motion/FCP UI operations; native visual QA pending user."
|
|
||||||
},
|
|
||||||
"slimValidation": {
|
"slimValidation": {
|
||||||
"offlineFullChecks": 604,
|
"offlineFullChecks": 604,
|
||||||
"slimChecks": 224,
|
"slimChecks": 224,
|
||||||
@@ -51,5 +43,115 @@
|
|||||||
"maxByteErrorVsIndependentCIReference": 2,
|
"maxByteErrorVsIndependentCIReference": 2,
|
||||||
"maxUVOracleError": 1.09828022e-05,
|
"maxUVOracleError": 1.09828022e-05,
|
||||||
"nativeVisualQA": "pending user"
|
"nativeVisualQA": "pending user"
|
||||||
|
},
|
||||||
|
"historicalHostValidation": {
|
||||||
|
"date": "2026-10-21",
|
||||||
|
"FCP": "Restored input-chain template shows supplied real video; effect parameters and forward analysis responded in independent QA project.",
|
||||||
|
"Motion": "1.0.4 installed formal app loaded the real-video QA project and rendered brightness/geometry with a primary box at different playhead positions; offline tracking disabled.",
|
||||||
|
"scope": "1.0.6 coordinate/chin repair native visual QA is reserved for user self-testing; no Motion/FCP UI operations. Historical host tests remain scoped to their versions.",
|
||||||
|
"version105": "Formal 1.0.5 (7) signed/notarized/stapled/installed and PluginKit formal path verified. User closed hosts before replacement; no Motion/FCP UI operated. Native visual testing pending user.",
|
||||||
|
"version106": "Formal 1.0.6 (8) universal Release signed/notarized/stapled/installed, identity/Gatekeeper/formal PluginKit path verified. User closed hosts; companion moef and TIFF synchronized and byte-verified. Generated outer APPs and notarization ZIP removed. No Motion/FCP UI operations; native visual QA pending user."
|
||||||
|
},
|
||||||
|
"totalPublished": 3,
|
||||||
|
"hiddenCompatibilityParameters": 49,
|
||||||
|
"legacyGroupPathsPreserved": true,
|
||||||
|
"legacyValuesPreserved": true,
|
||||||
|
"publishOSC": 0,
|
||||||
|
"currentHostValidation": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"Motion": "pending user",
|
||||||
|
"FCP": "pending user",
|
||||||
|
"scope": "No Motion/FCP launch, quit, activation, UI operation or native save was performed. XML structure and standalone archive checks do not establish custom inspector loading."
|
||||||
|
},
|
||||||
|
"customParameter": {
|
||||||
|
"id": 210,
|
||||||
|
"channel": "./210",
|
||||||
|
"name": "",
|
||||||
|
"flags": "12901679104",
|
||||||
|
"flagsOrigin": "Observed original custom parameter native structure, without copying its data blob",
|
||||||
|
"class": "PGInspectorState",
|
||||||
|
"schema": 1,
|
||||||
|
"hasEdits": false,
|
||||||
|
"valueCount": 30,
|
||||||
|
"disabledCount": 0,
|
||||||
|
"classes": [
|
||||||
|
"NSDictionary",
|
||||||
|
"NSSet",
|
||||||
|
"PGInspectorState"
|
||||||
|
],
|
||||||
|
"innerBytes": 935,
|
||||||
|
"innerSHA256": "257540ac6079756a76f3d31dff1b9bca2b33393962057927791316965495ab60",
|
||||||
|
"outerBytes": 1127,
|
||||||
|
"outerSHA256": "95d5f1e2c35c40e9f1234b1480894bcc0ee68c0822c9bd3f7c04338062e6a26b",
|
||||||
|
"encodedCharacters": 1516,
|
||||||
|
"encodingAlphabet": "*-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
||||||
|
"envelope": "8-byte big-endian outer archive length, byte 0x2a, outer NSKeyedArchiver payload, zero padding to 3-byte boundary, custom base64 alphabet",
|
||||||
|
"outerTopKeys": [
|
||||||
|
"BlindDataObject",
|
||||||
|
"DataIsLegacy"
|
||||||
|
],
|
||||||
|
"innerTopKey": "Custom Data",
|
||||||
|
"legacyFlag": false,
|
||||||
|
"defaultEqualsCurrent": true,
|
||||||
|
"exactEncodeDecodeRoundtrip": true,
|
||||||
|
"outerSecureDecodeRoundtrip": true,
|
||||||
|
"vendorClassesOrBlobReused": false,
|
||||||
|
"emptyDataValueFallback": "not established and not relied upon",
|
||||||
|
"nativeLoadStatus": "pending user"
|
||||||
|
},
|
||||||
|
"publicationTransition": {
|
||||||
|
"previousFunctionalControls": 46,
|
||||||
|
"currentFunctionalControls": 1,
|
||||||
|
"informationControls": 2,
|
||||||
|
"manualAnalysisPublished": false,
|
||||||
|
"retainedForCompatibility": [
|
||||||
|
45,
|
||||||
|
46,
|
||||||
|
50,
|
||||||
|
51,
|
||||||
|
52,
|
||||||
|
53,
|
||||||
|
60,
|
||||||
|
61,
|
||||||
|
62
|
||||||
|
],
|
||||||
|
"oldParameterPathsUnchanged": true
|
||||||
|
},
|
||||||
|
"xmlFlagPolicy": {
|
||||||
|
"scalarBooleanString": "12905938976; complete observed hidden native flags",
|
||||||
|
"point": "12905943072; complete observed hidden native point flags",
|
||||||
|
"groupPopupButton": "existing private XML flags retained; no guessed bit patch",
|
||||||
|
"pluginHiddenRegistration": "verified in current source: kFxParameterFlag_HIDDEN for all legacy groups, numeric/point/popup/string parameters; commands additionally disabled"
|
||||||
|
},
|
||||||
|
"nonVisualAcceptance": {
|
||||||
|
"XMLAndReferences": "pass: own codec audit and ae-to-motion audit_motion.py",
|
||||||
|
"mediaClosure": "pass: Source Media/clip/footage/relative TIFF and media SHA256",
|
||||||
|
"publications": "pass: exactly two copyright menus plus empty-name ./210",
|
||||||
|
"copyrightBindings": "pass: complete Rig/widget trees unchanged from baseline, own audit resolves menus/publications",
|
||||||
|
"selection": "pass: 0 selected content objects; project focus",
|
||||||
|
"customArchiveStructure": "pass: exact inner bytes, own class/schema/values; Foundation outer secure decode",
|
||||||
|
"customHostInstantiation": "pending user"
|
||||||
|
},
|
||||||
|
"semanticPreservation": {
|
||||||
|
"inverseDiffPassed": true,
|
||||||
|
"allowedChanges": [
|
||||||
|
"description/version",
|
||||||
|
"46 native functional publications replaced by custom210",
|
||||||
|
"custom210 own state archive",
|
||||||
|
"observed scalar/point/string hidden flags",
|
||||||
|
"publish OSC value 1 to 0"
|
||||||
|
],
|
||||||
|
"otherStructureAndLegacyValuesUnchanged": true
|
||||||
|
},
|
||||||
|
"reproducibleCodec": {
|
||||||
|
"script": "../../scripts/inspector-template.py",
|
||||||
|
"commands": [
|
||||||
|
"python3 scripts/inspector-template.py audit \"Templates/大鱼人像美化/大鱼人像美化.moef\"",
|
||||||
|
"python3 scripts/inspector-template.py encode --inner build/ui-analysis/PGInspectorState-default-inner.bplist --text-output build/ui-analysis/panel-value.txt"
|
||||||
|
],
|
||||||
|
"ownRoundtrip": "pass",
|
||||||
|
"malformedOrVendorArchiveRejectionCases": 5,
|
||||||
|
"foundationOuterSecureDecode": "pass",
|
||||||
|
"innerClassDecode": "model generates own archive; structure decoded and exact generated bytes preserved; native host instantiation pending user"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+55
-91
@@ -46,7 +46,7 @@
|
|||||||
<flags>1</flags>
|
<flags>1</flags>
|
||||||
</template>
|
</template>
|
||||||
<build />
|
<build />
|
||||||
<description>大鱼人像美化 1.0.6:人像磨皮、肤色、脸型、五官保护和三目标人脸跟踪。模板中频磨皮默认为50%。</description>
|
<description>大鱼人像美化 1.1.0:磨皮、美白、瘦脸三卡片自定义面板,自动选择主体并实时跟随。模板局部磨皮默认为50%。</description>
|
||||||
<scene>
|
<scene>
|
||||||
<sceneSettings>
|
<sceneSettings>
|
||||||
<width>1920</width>
|
<width>1920</width>
|
||||||
@@ -82,52 +82,7 @@
|
|||||||
<version>2</version>
|
<version>2</version>
|
||||||
<target object="510000011" channel="./2/100" name="FCP 版本作者→" />
|
<target object="510000011" channel="./2/100" name="FCP 版本作者→" />
|
||||||
<target object="510000012" channel="./2/100" name="更多模板浏览器访问→" />
|
<target object="510000012" channel="./2/100" name="更多模板浏览器访问→" />
|
||||||
<target object="510000005" channel="./100/70" name="启用磨皮" />
|
<target object="510000005" channel="./210" name="" />
|
||||||
<target object="510000005" channel="./100/1" name="磨皮强度" />
|
|
||||||
<target object="510000005" channel="./100/12" name="中频磨皮" />
|
|
||||||
<target object="510000005" channel="./100/3" name="磨皮半径" />
|
|
||||||
<target object="510000005" channel="./100/30" name="保护五官" />
|
|
||||||
<target object="510000005" channel="./101/71" name="启用肤色" />
|
|
||||||
<target object="510000005" channel="./101/42" name="肤色风格" />
|
|
||||||
<target object="510000005" channel="./101/2" name="肤色提亮" />
|
|
||||||
<target object="510000005" channel="./101/14" name="肤色色相" />
|
|
||||||
<target object="510000005" channel="./101/15" name="肤色饱和度" />
|
|
||||||
<target object="510000005" channel="./101/41" name="均匀肤色" />
|
|
||||||
<target object="510000005" channel="./101/31" name="牙齿美白" />
|
|
||||||
<target object="510000005" channel="./101/32" name="眼睛提亮" />
|
|
||||||
<target object="510000005" channel="./102/73" name="启用细节" />
|
|
||||||
<target object="510000005" channel="./102/17" name="高频细节平滑" />
|
|
||||||
<target object="510000005" channel="./102/4" name="锐化" />
|
|
||||||
<target object="510000005" channel="./103/72" name="启用脸型" />
|
|
||||||
<target object="510000005" channel="./103/18" name="瘦脸" />
|
|
||||||
<target object="510000005" channel="./103/33" name="大眼" />
|
|
||||||
<target object="510000005" channel="./103/22" name="短脸" />
|
|
||||||
<target object="510000005" channel="./103/23" name="窄脸" />
|
|
||||||
<target object="510000005" channel="./103/24" name="下巴" />
|
|
||||||
<target object="510000005" channel="./103/25" name="V 脸" />
|
|
||||||
<target object="510000005" channel="./103/26" name="下颌" />
|
|
||||||
<target object="510000005" channel="./103/27" name="颧骨" />
|
|
||||||
<target object="510000005" channel="./103/28" name="小头" />
|
|
||||||
<target object="510000005" channel="./103/29" name="小头范围" />
|
|
||||||
<target object="510000005" channel="./104/20" name="只处理选中人脸" />
|
|
||||||
<target object="510000005" channel="./104/21" name="选脸位置" />
|
|
||||||
<target object="510000005" channel="./104/39" name="多人高速模式" />
|
|
||||||
<target object="510000005" channel="./104/40" name="防跳脸范围" />
|
|
||||||
<target object="510000005" channel="./104/45" name="使用跟踪数据" />
|
|
||||||
<target object="510000005" channel="./104/46" name="跟踪目标" />
|
|
||||||
<target object="510000005" channel="./104/50" name="向前分析" />
|
|
||||||
<target object="510000005" channel="./104/51" name="向后分析" />
|
|
||||||
<target object="510000005" channel="./104/52" name="清除当前目标" />
|
|
||||||
<target object="510000005" channel="./104/53" name="跟踪状态" />
|
|
||||||
<target object="510000005" channel="./104/19" name="显示人脸框" />
|
|
||||||
<target object="510000005" channel="./104/35" name="显示跟踪框" />
|
|
||||||
<target object="510000005" channel="./105/7" name="蒙版阈值" />
|
|
||||||
<target object="510000005" channel="./105/8" name="蒙版柔度" />
|
|
||||||
<target object="510000005" channel="./105/9" name="边缘保护" />
|
|
||||||
<target object="510000005" channel="./105/10" name="预览蒙版" />
|
|
||||||
<target object="510000005" channel="./105/43" name="反转蒙版" />
|
|
||||||
<target object="510000005" channel="./105/44" name="蒙版通道" />
|
|
||||||
<target object="510000005" channel="./105/5" name="整体混合" />
|
|
||||||
</publishSettings>
|
</publishSettings>
|
||||||
<currentFrame>0 1 1 0</currentFrame>
|
<currentFrame>0 1 1 0</currentFrame>
|
||||||
<currentObject>510000001</currentObject>
|
<currentObject>510000001</currentObject>
|
||||||
@@ -298,72 +253,81 @@
|
|||||||
<parameter name="Width" id="313" flags="8589934610" default="1" value="1920" />
|
<parameter name="Width" id="313" flags="8589934610" default="1" value="1920" />
|
||||||
<parameter name="Height" id="314" flags="8589934610" default="1" value="1080" />
|
<parameter name="Height" id="314" flags="8589934610" default="1" value="1080" />
|
||||||
</parameter>
|
</parameter>
|
||||||
<filter name="大鱼人像美化" id="510000005" factoryID="6" pluginUUID="FB14B0E6-59B2-4836-916F-CD0C559E9241" pluginVersion="1.0.6" pluginName="大鱼人像美化" pluginDynamicParams="0">
|
<filter name="大鱼人像美化" id="510000005" factoryID="6" pluginUUID="FB14B0E6-59B2-4836-916F-CD0C559E9241" pluginVersion="1.1.0" pluginName="大鱼人像美化" pluginDynamicParams="0">
|
||||||
<timing in="0 1 1 0" out="299 30 1 0" offset="0 1 1 0" />
|
<timing in="0 1 1 0" out="299 30 1 0" offset="0 1 1 0" />
|
||||||
<baseFlags>8589934608</baseFlags>
|
<baseFlags>8589934608</baseFlags>
|
||||||
|
<parameter name="" id="210" flags="12901679104">
|
||||||
|
<numberOfKeypoints>1</numberOfKeypoints>
|
||||||
|
<keypoint>
|
||||||
|
<time>0 0 0 0</time>
|
||||||
|
<value />
|
||||||
|
</keypoint>
|
||||||
|
<defaultVal>********-4QeMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl21dq7kP4ZnR1*kp*20*kE3-UQ8K0FqNL7nOKxiKGFVQaBcOLNZQZEYR4xkK0FjMadZMrFn2U*-Ve-T2*xCIohZSKJYEL7XO4ZqNL9F0*ZPErJnR4xh62FVR44**OwE8EgA3pZOKplRLZxUMK7XN4JaNqVdOahgPKtjQ53mQrFpRbRsSLdvUMG5JGFiRKlgpEoC1l*F2VAI3FNKRa3gRKJnJWFXP43nQpNZN4ZoNKFKQqBcNKpVK4FdQq3WP4JYU*8*8*UE*M*aolUN1VctK3RCImtfNLZnKYtH9axWOaJXR5Cj2-sP5-oS5m*V6WAY7GMb80Ye8mkh9WwkAH6nB1IqBnW**s*2U*K*-c*5U*W*0M*8U*i*16*BU*u*1s*EU-4*2c*HU-G*3M*KU-S*46*NU-e*4s*QU-q*5c*TU00j2-suCnguCncuCXd1CncuCXcuCXcuCncvCXcuCXdJCXe*6M*WU08*6M*WU04*6M*VU04*6s*WU04*6M*VU04*6M*VU04*6M*WU04*6c*VU04*6M*VU04*76*VU04*7F*d2-sEFl*P2*IE6l*M2*6E6-*c22ME2V*R2-cE1l*22-QE1-*e22UE5l*-20QE5-*F2-YE1V*1202E3WA**********0Azw********0AzzAnAk****0B*6********B7wTLtzKWFXP43nQqtVPKJM74BgMLBnNLBQHZB2OKBoOKxiML7tcbu*K2tHHq7eNKBooVYCUcCUU0TGT5q3VZJCIpBZR883UB7wTMW7Ll*EI2R7PbBkNKBoPr7HR43oNO88U3wE23-5GKtnQ4JXR4xmIrFVR4I*0**F*-c*7**d*16*Bk-7*2k*K*-O*6M*X*0L*7s*dE0g*9A*j*0y*A**kE11*AI*n*1I*Bw-**20*EE--U26*Ec-1*2C*F*-2U2I*FM-4*2O*Fk-5U2U*G6-7*2a*GU-8U2g*Gs-A*2m*HE-BU2s*Hc-D*3R*Jw-ME3X*KI-Nk3d*Kg-PE3j*L2-Qk3p*LQ-SE3v*Lo-Tk4-*MA-VE45*MY-Wk4B*Mw-YE4H*NI-Zk4N*Ng-bE4T*O2-ck4Z*OQ-eE4f*Oo-fk4l*PA-hE4r*PY-ik4x*Pw-kE51*QI-lk57*Qg-nE5D*R2-ok5J*RQ-s*5d*T6-yk6**Ug03*6V*WE09E6m*XA0BE6u*Y*0Ek76*Zg0LU********6-*********6g*******************7l0**6*-2*4U*Y*0Y*AU*r*2Y*HU-P*4o*Pk-l*5I*SkEa*********U2*********2*******************-0Q*</defaultVal>
|
||||||
|
<dataValue>********-4QeMb-gOLBoA11I*E61-*I4-klM75NZQbBdPqtN743mMqVdRaJmJ0FoPr-M74xWOaJXR5AG**44c3wE1otHGqJtNKF-QaBcOLNZQh660Ec9L2FVR437QolZNq3XSJwE1o7gOKtYF43oMIxWOaJXR6*0U*4X1EsDJGFiRKlgHl21dq7kP4ZnR1*kp*20*kE3-UQ8K0FqNL7nOKxiKGFVQaBcOLNZQZEYR4xkK0FjMadZMrFn2U*-Ve-T2*xCIohZSKJYEL7XO4ZqNL9F0*ZPErJnR4xh62FVR44**OwE8EgA3pZOKplRLZxUMK7XN4JaNqVdOahgPKtjQ53mQrFpRbRsSLdvUMG5JGFiRKlgpEoC1l*F2VAI3FNKRa3gRKJnJWFXP43nQpNZN4ZoNKFKQqBcNKpVK4FdQq3WP4JYU*8*8*UE*M*aolUN1VctK3RCImtfNLZnKYtH9axWOaJXR5Cj2-sP5-oS5m*V6WAY7GMb80Ye8mkh9WwkAH6nB1IqBnW**s*2U*K*-c*5U*W*0M*8U*i*16*BU*u*1s*EU-4*2c*HU-G*3M*KU-S*46*NU-e*4s*QU-q*5c*TU00j2-suCnguCncuCXd1CncuCXcuCXcuCncvCXcuCXdJCXe*6M*WU08*6M*WU04*6M*VU04*6s*WU04*6M*VU04*6M*VU04*6M*WU04*6c*VU04*6M*VU04*76*VU04*7F*d2-sEFl*P2*IE6l*M2*6E6-*c22ME2V*R2-cE1l*22-QE1-*e22UE5l*-20QE5-*F2-YE1V*1202E3WA**********0Azw********0AzzAnAk****0B*6********B7wTLtzKWFXP43nQqtVPKJM74BgMLBnNLBQHZB2OKBoOKxiML7tcbu*K2tHHq7eNKBooVYCUcCUU0TGT5q3VZJCIpBZR883UB7wTMW7Ll*EI2R7PbBkNKBoPr7HR43oNO88U3wE23-5GKtnQ4JXR4xmIrFVR4I*0**F*-c*7**d*16*Bk-7*2k*K*-O*6M*X*0L*7s*dE0g*9A*j*0y*A**kE11*AI*n*1I*Bw-**20*EE--U26*Ec-1*2C*F*-2U2I*FM-4*2O*Fk-5U2U*G6-7*2a*GU-8U2g*Gs-A*2m*HE-BU2s*Hc-D*3R*Jw-ME3X*KI-Nk3d*Kg-PE3j*L2-Qk3p*LQ-SE3v*Lo-Tk4-*MA-VE45*MY-Wk4B*Mw-YE4H*NI-Zk4N*Ng-bE4T*O2-ck4Z*OQ-eE4f*Oo-fk4l*PA-hE4r*PY-ik4x*Pw-kE51*QI-lk57*Qg-nE5D*R2-ok5J*RQ-s*5d*T6-yk6**Ug03*6V*WE09E6m*XA0BE6u*Y*0Ek76*Zg0LU********6-*********6g*******************7l0**6*-2*4U*Y*0Y*AU*r*2Y*HU-P*4o*Pk-l*5I*SkEa*********U2*********2*******************-0Q*</dataValue>
|
||||||
|
</parameter>
|
||||||
<parameter name="磨皮" id="100" flags="8589938704">
|
<parameter name="磨皮" id="100" flags="8589938704">
|
||||||
<parameter name="启用磨皮" id="70" flags="12901679120" default="1" value="1" />
|
<parameter name="启用磨皮" id="70" flags="12905938976" default="1" value="1" />
|
||||||
<parameter name="磨皮强度" id="1" flags="12901679120" default="1" value="1" />
|
<parameter name="磨皮强度" id="1" flags="12905938976" default="1" value="1" />
|
||||||
<parameter name="中频磨皮" id="12" flags="12901679120" default="0" value="0.5" />
|
<parameter name="中频磨皮" id="12" flags="12905938976" default="0" value="0.5" />
|
||||||
<parameter name="磨皮半径" id="3" flags="12901679120" default="8" value="8" />
|
<parameter name="磨皮半径" id="3" flags="12905938976" default="8" value="8" />
|
||||||
<parameter name="保护五官" id="30" flags="12901679120" default="1" value="1" />
|
<parameter name="保护五官" id="30" flags="12905938976" default="1" value="1" />
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="肤色" id="101" flags="8589938704">
|
<parameter name="肤色" id="101" flags="8589938704">
|
||||||
<parameter name="启用肤色" id="71" flags="12901679120" default="1" value="1" />
|
<parameter name="启用肤色" id="71" flags="12905938976" default="1" value="1" />
|
||||||
<parameter name="肤色风格" id="42" flags="12901744656" default="0" value="0" />
|
<parameter name="肤色风格" id="42" flags="12901744656" default="0" value="0" />
|
||||||
<parameter name="肤色提亮" id="2" flags="12901679120" default="0" value="0" />
|
<parameter name="肤色提亮" id="2" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="肤色色相" id="14" flags="12901679120" default="0" value="0" />
|
<parameter name="肤色色相" id="14" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="肤色饱和度" id="15" flags="12901679120" default="0" value="0" />
|
<parameter name="肤色饱和度" id="15" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="均匀肤色" id="41" flags="12901679120" default="0" value="0" />
|
<parameter name="均匀肤色" id="41" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="牙齿美白" id="31" flags="12901679120" default="0" value="0" />
|
<parameter name="牙齿美白" id="31" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="眼睛提亮" id="32" flags="12901679120" default="0" value="0" />
|
<parameter name="眼睛提亮" id="32" flags="12905938976" default="0" value="0" />
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="细节" id="102" flags="8589938704">
|
<parameter name="细节" id="102" flags="8589938704">
|
||||||
<parameter name="启用细节" id="73" flags="12901679120" default="1" value="1" />
|
<parameter name="启用细节" id="73" flags="12905938976" default="1" value="1" />
|
||||||
<parameter name="高频细节平滑" id="17" flags="12901679120" default="0" value="0" />
|
<parameter name="高频细节平滑" id="17" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="锐化" id="4" flags="12901679120" default="0" value="0" />
|
<parameter name="锐化" id="4" flags="12905938976" default="0" value="0" />
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="脸型" id="103" flags="8589938704">
|
<parameter name="脸型" id="103" flags="8589938704">
|
||||||
<parameter name="启用脸型" id="72" flags="12901679120" default="1" value="1" />
|
<parameter name="启用脸型" id="72" flags="12905938976" default="1" value="1" />
|
||||||
<parameter name="瘦脸" id="18" flags="12901679120" default="0" value="0" />
|
<parameter name="瘦脸" id="18" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="大眼" id="33" flags="12901679120" default="0" value="0" />
|
<parameter name="大眼" id="33" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="短脸" id="22" flags="12901679120" default="0" value="0" />
|
<parameter name="短脸" id="22" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="窄脸" id="23" flags="12901679120" default="0" value="0" />
|
<parameter name="窄脸" id="23" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="下巴" id="24" flags="12901679120" default="0" value="0" />
|
<parameter name="下巴" id="24" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="V 脸" id="25" flags="12901679120" default="0" value="0" />
|
<parameter name="V 脸" id="25" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="下颌" id="26" flags="12901679120" default="0" value="0" />
|
<parameter name="下颌" id="26" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="颧骨" id="27" flags="12901679120" default="0" value="0" />
|
<parameter name="颧骨" id="27" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="小头" id="28" flags="12901679120" default="0" value="0" />
|
<parameter name="小头" id="28" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="小头范围" id="29" flags="12901679120" default="0" value="0" />
|
<parameter name="小头范围" id="29" flags="12905938976" default="0" value="0" />
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="人脸选择与跟踪" id="104" flags="8589938704">
|
<parameter name="人脸选择与跟踪" id="104" flags="8589938704">
|
||||||
<parameter name="只处理选中人脸" id="20" flags="12901679120" default="0" value="0" />
|
<parameter name="只处理选中人脸" id="20" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="选脸位置" id="21" flags="12901683200">
|
<parameter name="选脸位置" id="21" flags="12905943072">
|
||||||
<foldFlags>15</foldFlags>
|
<foldFlags>15</foldFlags>
|
||||||
<parameter name="X" id="1" flags="12901679120" default="0.5" value="0.5" />
|
<parameter name="X" id="1" flags="12905938976" default="0.5" value="0.5" />
|
||||||
<parameter name="Y" id="2" flags="12901679120" default="0.5" value="0.5" />
|
<parameter name="Y" id="2" flags="12905938976" default="0.5" value="0.5" />
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="多人高速模式" id="39" flags="12901679120" default="0" value="0" />
|
<parameter name="多人高速模式" id="39" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="防跳脸范围" id="40" flags="12901679120" default="1.8" value="1.8" />
|
<parameter name="防跳脸范围" id="40" flags="12905938976" default="1.8" value="1.8" />
|
||||||
<parameter name="使用跟踪数据" id="45" flags="12901679120" default="0" value="0" />
|
<parameter name="使用跟踪数据" id="45" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="跟踪目标" id="46" flags="12901744656" default="0" value="0" />
|
<parameter name="跟踪目标" id="46" flags="12901744656" default="0" value="0" />
|
||||||
<parameter name="向前分析" id="50" />
|
<parameter name="向前分析" id="50" />
|
||||||
<parameter name="向后分析" id="51" />
|
<parameter name="向后分析" id="51" />
|
||||||
<parameter name="清除当前目标" id="52" />
|
<parameter name="清除当前目标" id="52" />
|
||||||
<parameter name="跟踪状态" id="53" flags="12889161760">
|
<parameter name="跟踪状态" id="53" flags="12905938976">
|
||||||
<text>选择画面中的人脸后开始分析</text>
|
<text>选择画面中的人脸后开始分析</text>
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="显示人脸框" id="19" flags="12901679120" default="0" value="0" />
|
<parameter name="显示人脸框" id="19" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="显示跟踪框" id="35" flags="12901679120" default="0" value="0" />
|
<parameter name="显示跟踪框" id="35" flags="12905938976" default="0" value="0" />
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="蒙版与输出" id="105" flags="8589938704">
|
<parameter name="蒙版与输出" id="105" flags="8589938704">
|
||||||
<parameter name="蒙版阈值" id="7" flags="12901679120" default="148" value="148" />
|
<parameter name="蒙版阈值" id="7" flags="12905938976" default="148" value="148" />
|
||||||
<parameter name="蒙版柔度" id="8" flags="12901679120" default="0.6" value="0.6" />
|
<parameter name="蒙版柔度" id="8" flags="12905938976" default="0.6" value="0.6" />
|
||||||
<parameter name="边缘保护" id="9" flags="12901679120" default="0.4" value="0.4" />
|
<parameter name="边缘保护" id="9" flags="12905938976" default="0.4" value="0.4" />
|
||||||
<parameter name="预览蒙版" id="10" flags="12901679120" default="0" value="0" />
|
<parameter name="预览蒙版" id="10" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="反转蒙版" id="43" flags="12901679120" default="0" value="0" />
|
<parameter name="反转蒙版" id="43" flags="12905938976" default="0" value="0" />
|
||||||
<parameter name="蒙版通道" id="44" flags="12901744656" default="0" value="0" />
|
<parameter name="蒙版通道" id="44" flags="12901744656" default="0" value="0" />
|
||||||
<parameter name="整体混合" id="5" flags="12901679120" default="1" value="1" />
|
<parameter name="整体混合" id="5" flags="12905938976" default="1" value="1" />
|
||||||
</parameter>
|
</parameter>
|
||||||
<parameter name="分析数据" id="60" flags="12905938976">
|
<parameter name="分析数据" id="60" flags="12905938976">
|
||||||
<text />
|
<text />
|
||||||
@@ -377,7 +341,7 @@
|
|||||||
<parameter name="混合" id="10001" flags="12884901888" default="1" value="1" />
|
<parameter name="混合" id="10001" flags="12884901888" default="1" value="1" />
|
||||||
<parameter name="Flip" id="10002" flags="12889161760" default="0" value="0" />
|
<parameter name="Flip" id="10002" flags="12889161760" default="0" value="0" />
|
||||||
<parameter name="Input Points" id="10003" flags="12889161760" default="1" value="1" />
|
<parameter name="Input Points" id="10003" flags="12889161760" default="1" value="1" />
|
||||||
<parameter name="发布 OSC" id="10005" flags="12901679104" default="0" value="1" />
|
<parameter name="发布 OSC" id="10005" flags="12901679104" default="0" value="0" />
|
||||||
</filter>
|
</filter>
|
||||||
</scenenode>
|
</scenenode>
|
||||||
<aspectRatio>1</aspectRatio>
|
<aspectRatio>1</aspectRatio>
|
||||||
|
|||||||
@@ -63,11 +63,13 @@
|
|||||||
"/Localized: Plugin/InfoPlist.strings",
|
"/Localized: Plugin/InfoPlist.strings",
|
||||||
"/Localized: Plugin/Localizable.strings",
|
"/Localized: Plugin/Localizable.strings",
|
||||||
Plugin/beatygrilPlugIn.m,
|
Plugin/beatygrilPlugIn.m,
|
||||||
|
Plugin/Info.plist,
|
||||||
|
Plugin/main.m,
|
||||||
Plugin/PGBeautyRenderer.m,
|
Plugin/PGBeautyRenderer.m,
|
||||||
Plugin/PGFaceProcessor.m,
|
Plugin/PGFaceProcessor.m,
|
||||||
Plugin/PGFaceSelectOSC.m,
|
Plugin/PGFaceSelectOSC.m,
|
||||||
Plugin/Info.plist,
|
Plugin/PGInspectorState.m,
|
||||||
Plugin/main.m,
|
Plugin/PGInspectorView.m,
|
||||||
"Wrapper Application/Info.plist",
|
"Wrapper Application/Info.plist",
|
||||||
);
|
);
|
||||||
target = 6945FC8A3088B7220013E108 /* Wrapper Application */;
|
target = 6945FC8A3088B7220013E108 /* Wrapper Application */;
|
||||||
@@ -306,11 +308,11 @@
|
|||||||
6945FCCD3088B7220013E108 /* Debug configuration for PBXNativeTarget "XPC Service" */ = {
|
6945FCCD3088B7220013E108 /* Debug configuration for PBXNativeTarget "XPC Service" */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Manual;
|
|
||||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
|
||||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||||
INFOPLIST_FILE = beatygril/Plugin/Info.plist;
|
INFOPLIST_FILE = beatygril/Plugin/Info.plist;
|
||||||
@@ -329,11 +331,11 @@
|
|||||||
6945FCCE3088B7220013E108 /* Release configuration for PBXNativeTarget "XPC Service" */ = {
|
6945FCCE3088B7220013E108 /* Release configuration for PBXNativeTarget "XPC Service" */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Manual;
|
|
||||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
|
||||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||||
INFOPLIST_FILE = beatygril/Plugin/Info.plist;
|
INFOPLIST_FILE = beatygril/Plugin/Info.plist;
|
||||||
@@ -353,11 +355,11 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements";
|
||||||
CODE_SIGN_STYLE = Manual;
|
|
||||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
|
||||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||||
INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist";
|
INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril;
|
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril;
|
||||||
@@ -370,11 +372,11 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements";
|
||||||
CODE_SIGN_STYLE = Manual;
|
|
||||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
|
||||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
DEVELOPMENT_TEAM = B3W25FX5H5;
|
DEVELOPMENT_TEAM = B3W25FX5H5;
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||||
INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist";
|
INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril;
|
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril;
|
||||||
@@ -387,9 +389,11 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk;
|
ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk;
|
||||||
ARCHS = "arm64 x86_64";
|
|
||||||
ONLY_ACTIVE_ARCH = NO;
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ARCHS = (
|
||||||
|
arm64,
|
||||||
|
x86_64,
|
||||||
|
);
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
@@ -448,6 +452,7 @@
|
|||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_VERSION = 5;
|
SWIFT_VERSION = 5;
|
||||||
};
|
};
|
||||||
@@ -457,9 +462,11 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk;
|
ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk;
|
||||||
ARCHS = "arm64 x86_64";
|
|
||||||
ONLY_ACTIVE_ARCH = NO;
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ARCHS = (
|
||||||
|
arm64,
|
||||||
|
x86_64,
|
||||||
|
);
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
@@ -512,6 +519,7 @@
|
|||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_VERSION = 5;
|
SWIFT_VERSION = 5;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XPC!</string>
|
<string>XPC!</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.6</string>
|
<string>1.1.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>8</string>
|
<string>9</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string></string>
|
<string></string>
|
||||||
<key>PlugInKit</key>
|
<key>PlugInKit</key>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<key>uuid</key>
|
<key>uuid</key>
|
||||||
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
|
<string>FB14B0E6-59B2-4836-916F-CD0C559E9241</string>
|
||||||
<key>version</key>
|
<key>version</key>
|
||||||
<string>1.0.6</string>
|
<string>1.1.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>className</key>
|
<key>className</key>
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
<key>uuid</key>
|
<key>uuid</key>
|
||||||
<string>E0E432C8-3D71-4ABA-B006-E7F519E06C87</string>
|
<string>E0E432C8-3D71-4ABA-B006-E7F519E06C87</string>
|
||||||
<key>version</key>
|
<key>version</key>
|
||||||
<string>1.0.6</string>
|
<string>1.1.0</string>
|
||||||
<key>protocolNames</key>
|
<key>protocolNames</key>
|
||||||
<array>
|
<array>
|
||||||
<string>FxOnScreenControl</string>
|
<string>FxOnScreenControl</string>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <FxPlug/FxPlugSDK.h>
|
||||||
|
#import "PGBeautyTypes.h"
|
||||||
|
#import "PGParameterIDs.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// The descriptor list is shared by UI, persistence and render settings.
|
||||||
|
/// Keys: id, name, section (smooth/color/shape/more), kind
|
||||||
|
/// (slider/toggle/preset/master), min, max, default, format, bypass.
|
||||||
|
FOUNDATION_EXPORT NSArray<NSDictionary *> *PGInspectorControls(void);
|
||||||
|
FOUNDATION_EXPORT NSDictionary * _Nullable PGInspectorDescriptor(UInt32 identifier);
|
||||||
|
|
||||||
|
@interface PGInspectorState : NSObject <NSSecureCoding, NSCopying, FxCustomParameterInterpolation_v2>
|
||||||
|
@property (nonatomic, copy, readonly) NSDictionary<NSNumber *, NSNumber *> *values;
|
||||||
|
@property (nonatomic, copy, readonly) NSSet<NSNumber *> *disabledParameters;
|
||||||
|
/// An untouched panel must not replace parameters from an existing document.
|
||||||
|
@property (nonatomic, readonly) BOOL hasEdits;
|
||||||
|
+ (instancetype)stateWithValues:(NSDictionary *)values disabledParameters:(NSSet *)disabled hasEdits:(BOOL)edited;
|
||||||
|
- (double)valueForParameter:(UInt32)identifier;
|
||||||
|
- (BOOL)isParameterEnabled:(UInt32)identifier;
|
||||||
|
@end
|
||||||
|
|
||||||
|
FOUNDATION_EXPORT PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings settings, PGInspectorState * _Nullable state);
|
||||||
|
|
||||||
|
/// All arbitrary-time FxPlug parameter access is bracketed by host actions.
|
||||||
|
/// The published custom object owns the full UI state, so FCP save/undo does
|
||||||
|
/// not depend on unpublished parameters being mirrored by the template.
|
||||||
|
@interface PGInspectorController : NSObject
|
||||||
|
@property (nonatomic, strong, readonly) PGInspectorState *state;
|
||||||
|
@property (nonatomic, readonly) BOOL available;
|
||||||
|
@property (nonatomic, copy, readonly) NSString *statusMessage;
|
||||||
|
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager;
|
||||||
|
- (void)refresh;
|
||||||
|
- (void)beginInteraction;
|
||||||
|
- (void)endInteraction;
|
||||||
|
- (BOOL)setValue:(double)value forParameter:(UInt32)identifier;
|
||||||
|
- (BOOL)toggleEnabledForParameter:(UInt32)identifier;
|
||||||
|
- (BOOL)resetParameter:(UInt32)identifier;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
#import "PGInspectorState.h"
|
||||||
|
#import <math.h>
|
||||||
|
#import <stddef.h>
|
||||||
|
|
||||||
|
NSArray<NSDictionary *> *PGInspectorControls(void) {
|
||||||
|
static NSArray *controls; static dispatch_once_t once;
|
||||||
|
dispatch_once(&once, ^{
|
||||||
|
PGBeautySettings defaults=PGBeautyDefaults();
|
||||||
|
NSMutableArray *items=[NSMutableArray new];
|
||||||
|
#define ITEM(ID,NAME,FIELD,SECTION,KIND,FORMAT,MIN,MAX,BYPASS,STORAGE) \
|
||||||
|
[items addObject:@{@"id":@(ID),@"name":@NAME,@"section":@SECTION,@"kind":@KIND,@"format":@FORMAT,@"min":@(MIN),@"max":@(MAX),@"default":@(defaults.FIELD),@"bypass":@(BYPASS),@"offset":@(offsetof(PGBeautySettings,FIELD)),@"storage":@STORAGE}]
|
||||||
|
ITEM(PGSmoothingEnabled,"磨皮",featureProtection,"smooth","master","integer",0,1,NO,"master");
|
||||||
|
ITEM(PGMidtone,"局部磨皮",midtoneDetail,"smooth","slider","percent",0,1,YES,"float");
|
||||||
|
ITEM(PGHighPass,"全局磨皮",highPassDetail,"smooth","slider","percent",0,1,YES,"float");
|
||||||
|
ITEM(PGFeatureProtect,"五官保护",featureProtection,"smooth","toggle","integer",0,1,NO,"uint");
|
||||||
|
ITEM(PGColorEnabled,"美白",featureProtection,"color","master","integer",0,1,NO,"master");
|
||||||
|
ITEM(PGTonePreset,"肤色",skinTonePreset,"color","preset","integer",0,6,NO,"uint");
|
||||||
|
NSMutableDictionary *preset=[items.lastObject mutableCopy];
|
||||||
|
preset[@"options"]=@[@"原色",@"自然白",@"冷白",@"暖白",@"红润白",@"小麦",@"古铜"];items[items.count-1]=preset;
|
||||||
|
ITEM(PGBrightness,"肤色亮度",skinBrightness,"color","slider","percent",0,1,YES,"float");
|
||||||
|
ITEM(PGTeeth,"白牙",teethWhitening,"color","slider","percent",0,1,YES,"float");
|
||||||
|
ITEM(PGHue,"皮肤色相",skinHue,"color","slider","degrees",-30,30,YES,"float");
|
||||||
|
ITEM(PGShapeEnabled,"瘦脸",featureProtection,"shape","master","integer",0,1,NO,"master");
|
||||||
|
ITEM(PGTrackBoxes,"人脸框预览",showTrackBox,"shape","toggle","integer",0,1,NO,"uint");
|
||||||
|
ITEM(PGFastMulti,"多人模式",fastMultiFace,"shape","toggle","integer",0,1,NO,"uint");
|
||||||
|
ITEM(PGJumpGuard,"防跳脸范围",faceJumpGuardRange,"shape","slider","percent",1,4,NO,"float");
|
||||||
|
ITEM(PGThin,"瘦脸",faceThin,"shape","slider","signed",0,1,YES,"float");
|
||||||
|
ITEM(PGBigEye,"大眼",faceBigEye,"shape","slider","signed",0,1,YES,"float");
|
||||||
|
ITEM(PGShort,"短脸",faceShort,"shape","slider","signed",-1,1,YES,"float");
|
||||||
|
ITEM(PGNarrow,"窄脸",faceNarrow,"shape","slider","signed",-1,1,YES,"float");
|
||||||
|
ITEM(PGChin,"下巴",faceChin,"shape","slider","signed",-1,1,YES,"float");
|
||||||
|
ITEM(PGVFace,"V脸",faceV,"shape","slider","signed",0,1,YES,"float");
|
||||||
|
ITEM(PGJaw,"下颌骨",faceJaw,"shape","slider","signed",0,1,YES,"float");
|
||||||
|
ITEM(PGCheekbone,"颧骨",faceCheekbone,"shape","slider","signed",0,1,YES,"float");
|
||||||
|
ITEM(PGSmallHead,"小头",faceSmallHead,"shape","slider","signed",0,1,YES,"float");
|
||||||
|
ITEM(PGSmallHeadRange,"小头范围",faceSmallHeadRange,"shape","slider","percent",0,1,NO,"float");
|
||||||
|
ITEM(PGAmount,"磨皮强度",amount,"more","slider","percent",0,1,YES,"float");
|
||||||
|
ITEM(PGRadius,"磨皮半径",radius,"more","slider","radius",1,30,NO,"float");
|
||||||
|
ITEM(PGSaturation,"肤色饱和度",skinSaturation,"more","slider","percent",-100,100,YES,"float");
|
||||||
|
ITEM(PGEvenTone,"均匀肤色",skinEvenTone,"more","slider","percent",0,1,YES,"float");
|
||||||
|
ITEM(PGEyeBright,"眼睛提亮",eyeBrightening,"more","slider","percent",0,1,YES,"float");
|
||||||
|
ITEM(PGSharpness,"锐化",sharpness,"more","slider","percent",0,1,YES,"float");
|
||||||
|
ITEM(PGMix,"整体混合",mix,"more","slider","percent",0,1,NO,"float");
|
||||||
|
#undef ITEM
|
||||||
|
controls=[items copy];
|
||||||
|
}); return controls;
|
||||||
|
}
|
||||||
|
NSDictionary *PGInspectorDescriptor(UInt32 identifier) {
|
||||||
|
for(NSDictionary *d in PGInspectorControls()) if([d[@"id"] unsignedIntValue]==identifier) return d;
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
static double PGControlValue(id raw,NSDictionary *d) {
|
||||||
|
double value=[raw isKindOfClass:NSNumber.class]?[raw doubleValue]:[d[@"default"] doubleValue];
|
||||||
|
if(!isfinite(value)) value=[d[@"default"] doubleValue];
|
||||||
|
value=fmin([d[@"max"] doubleValue],fmax([d[@"min"] doubleValue],value));
|
||||||
|
if(![d[@"storage"] isEqual:@"float"]) value=round(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
@implementation PGInspectorState
|
||||||
|
+ (BOOL)supportsSecureCoding {return YES;}
|
||||||
|
+ (instancetype)stateWithValues:(NSDictionary *)values disabledParameters:(NSSet *)disabled hasEdits:(BOOL)edited {
|
||||||
|
PGInspectorState *state=[self new]; NSMutableDictionary *safe=[NSMutableDictionary new];NSMutableSet *bypassed=[NSMutableSet new];
|
||||||
|
for(NSDictionary *d in PGInspectorControls()) {
|
||||||
|
NSNumber *key=d[@"id"];safe[key]=@(PGControlValue(values[key],d));
|
||||||
|
if([d[@"bypass"] boolValue] && [disabled containsObject:key]) [bypassed addObject:key];
|
||||||
|
}
|
||||||
|
state->_values=[safe copy];state->_disabledParameters=[bypassed copy];state->_hasEdits=edited;return state;
|
||||||
|
}
|
||||||
|
- (instancetype)init {if((self=[super init])) {_values=@{};_disabledParameters=[NSSet set];}return self;}
|
||||||
|
- (instancetype)initWithCoder:(NSCoder *)coder {
|
||||||
|
NSInteger version=[coder decodeIntegerForKey:@"schema"];
|
||||||
|
if(version!=1) return nil;
|
||||||
|
NSSet *classes=[NSSet setWithObjects:NSDictionary.class,NSNumber.class,nil];
|
||||||
|
NSDictionary *values=[coder decodeObjectOfClasses:classes forKey:@"values"];
|
||||||
|
NSSet *disabled=[coder decodeObjectOfClasses:[NSSet setWithObjects:NSSet.class,NSNumber.class,nil] forKey:@"disabled"];
|
||||||
|
return [PGInspectorState stateWithValues:values?:@{} disabledParameters:disabled?:[NSSet set] hasEdits:[coder decodeBoolForKey:@"edited"]];
|
||||||
|
}
|
||||||
|
- (void)encodeWithCoder:(NSCoder *)coder {
|
||||||
|
[coder encodeInteger:1 forKey:@"schema"];[coder encodeObject:_values forKey:@"values"];
|
||||||
|
[coder encodeObject:_disabledParameters forKey:@"disabled"];[coder encodeBool:_hasEdits forKey:@"edited"];
|
||||||
|
}
|
||||||
|
- (id)copyWithZone:(NSZone *)zone {return [PGInspectorState stateWithValues:_values disabledParameters:_disabledParameters hasEdits:_hasEdits];}
|
||||||
|
- (double)valueForParameter:(UInt32)identifier {NSDictionary *d=PGInspectorDescriptor(identifier);return d?PGControlValue(_values[@(identifier)],d):0;}
|
||||||
|
- (BOOL)isParameterEnabled:(UInt32)identifier {return ![_disabledParameters containsObject:@(identifier)];}
|
||||||
|
- (BOOL)isEqual:(id)object {
|
||||||
|
if(object==self)return YES;
|
||||||
|
if(![object isKindOfClass:PGInspectorState.class])return NO;
|
||||||
|
PGInspectorState *other=object;return _hasEdits==other.hasEdits && [_values isEqual:other.values] && [_disabledParameters isEqual:other.disabledParameters];
|
||||||
|
}
|
||||||
|
- (NSUInteger)hash {return _values.hash ^ _disabledParameters.hash ^ _hasEdits;}
|
||||||
|
- (NSObject<NSSecureCoding,NSCopying> *)interpolateBetween:(NSObject<NSSecureCoding,NSCopying> *)rightValue withWeight:(float)weight {
|
||||||
|
if(![rightValue isKindOfClass:PGInspectorState.class])return [self copy];
|
||||||
|
PGInspectorState *right=(PGInspectorState *)rightValue;double t=isfinite(weight)?fmin(1,fmax(0,weight)):0;
|
||||||
|
if(t==0)return [self copy];if(t==1)return [right copy];
|
||||||
|
if(!_hasEdits || !right.hasEdits)return [self copy];
|
||||||
|
NSMutableDictionary *values=[NSMutableDictionary new];
|
||||||
|
for(NSDictionary *d in PGInspectorControls()) {
|
||||||
|
UInt32 key=[d[@"id"] unsignedIntValue];double a=[self valueForParameter:key],b=[right valueForParameter:key];
|
||||||
|
// Menus, switches and enable masks hold the preceding key until the
|
||||||
|
// next key. Numeric slider values interpolate within their valid range.
|
||||||
|
values[@(key)]=[d[@"storage"] isEqual:@"float"]?@(a+(b-a)*t):@(a);
|
||||||
|
}
|
||||||
|
return [PGInspectorState stateWithValues:values disabledParameters:_disabledParameters hasEdits:_hasEdits||right.hasEdits];
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorState *state) {
|
||||||
|
if(!state.hasEdits)return s;
|
||||||
|
for(NSDictionary *d in PGInspectorControls()) {
|
||||||
|
NSString *storage=d[@"storage"];if([storage isEqual:@"master"])continue;
|
||||||
|
UInt32 key=[d[@"id"] unsignedIntValue];double value=[state valueForParameter:key];
|
||||||
|
if(![state isParameterEnabled:key]) value=0;
|
||||||
|
size_t offset=[d[@"offset"] unsignedIntegerValue];
|
||||||
|
if([storage isEqual:@"float"]) *(float *)((uint8_t *)&s+offset)=value;
|
||||||
|
else *(uint32_t *)((uint8_t *)&s+offset)=(uint32_t)value;
|
||||||
|
}
|
||||||
|
if(![state valueForParameter:PGSmoothingEnabled])s.amount=s.midtoneDetail=s.highPassDetail=0;
|
||||||
|
if(![state valueForParameter:PGColorEnabled]) {s.skinBrightness=s.skinHue=s.skinSaturation=s.skinEvenTone=s.teethWhitening=s.eyeBrightening=0;s.skinTonePreset=0;}
|
||||||
|
if(![state valueForParameter:PGShapeEnabled])s.faceThin=s.faceBigEye=s.faceShort=s.faceNarrow=s.faceChin=s.faceV=s.faceJaw=s.faceCheekbone=s.faceSmallHead=0;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@interface PGInspectorController ()
|
||||||
|
@property (nonatomic,strong,readwrite) PGInspectorState *state;
|
||||||
|
@property (nonatomic,readwrite) BOOL available;
|
||||||
|
@property (nonatomic,copy,readwrite) NSString *statusMessage;
|
||||||
|
@property (nonatomic,strong) id<PROAPIAccessing> manager;
|
||||||
|
@property (nonatomic,strong) id<FxCustomParameterActionAPI_v4> action;
|
||||||
|
@property (nonatomic) BOOL interacting;
|
||||||
|
@property (nonatomic) NSTimeInterval pendingUntil;
|
||||||
|
@end
|
||||||
|
@implementation PGInspectorController
|
||||||
|
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager {
|
||||||
|
if((self=[super init])){_manager=manager;_state=[PGInspectorState stateWithValues:@{} disabledParameters:[NSSet set] hasEdits:NO];_statusMessage=@"";}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
- (BOOL)start {
|
||||||
|
if(_interacting)return YES;
|
||||||
|
_action=[_manager apiForProtocol:@protocol(FxCustomParameterActionAPI_v4)];
|
||||||
|
id retrieval=[_manager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||||
|
id setting=[_manager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||||
|
_available=_action && retrieval && setting;
|
||||||
|
if(!_available){_statusMessage=@"等待宿主连接";return NO;}
|
||||||
|
[_action startAction:self];_interacting=YES;return YES;
|
||||||
|
}
|
||||||
|
- (void)finish {if(_interacting){_interacting=NO;[_action endAction:self];_action=nil;}}
|
||||||
|
- (void)readAtTime:(CMTime)time {
|
||||||
|
id<FxParameterRetrievalAPI_v6> api=[_manager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||||
|
NSObject *object=nil;[api getCustomParameterValue:&object fromParameter:PGInspectorPanel atTime:time];
|
||||||
|
if([object isKindOfClass:PGInspectorState.class] && [(PGInspectorState *)object hasEdits]) {
|
||||||
|
PGInspectorState *host=(PGInspectorState *)object;
|
||||||
|
if([host isEqual:_state])_pendingUntil=0;
|
||||||
|
if(!_pendingUntil || NSDate.timeIntervalSinceReferenceDate>=_pendingUntil)_state=[host copy];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(_pendingUntil && NSDate.timeIntervalSinceReferenceDate<_pendingUntil)return;
|
||||||
|
NSMutableDictionary *values=[NSMutableDictionary new];
|
||||||
|
for(NSDictionary *d in PGInspectorControls()) {
|
||||||
|
UInt32 key=[d[@"id"] unsignedIntValue];double value=[d[@"default"] doubleValue];
|
||||||
|
if([d[@"kind"] isEqual:@"preset"]) {int menu=(int)value;if([api getIntValue:&menu fromParameter:key atTime:time])value=menu;}
|
||||||
|
else if([d[@"kind"] isEqual:@"toggle"] || [d[@"kind"] isEqual:@"master"]) {BOOL on=value!=0;if([api getBoolValue:&on fromParameter:key atTime:time])value=on;}
|
||||||
|
else [api getFloatValue:&value fromParameter:key atTime:time];
|
||||||
|
values[@(key)]=@(value);
|
||||||
|
}
|
||||||
|
_state=[PGInspectorState stateWithValues:values disabledParameters:[NSSet set] hasEdits:NO];
|
||||||
|
}
|
||||||
|
- (void)refresh {
|
||||||
|
if(_interacting)return;
|
||||||
|
if(![self start])return;
|
||||||
|
@try {[self readAtTime:[_action currentTime]];_statusMessage=@"";} @finally {[self finish];}
|
||||||
|
}
|
||||||
|
- (void)beginInteraction {
|
||||||
|
if(_interacting)return;
|
||||||
|
if([self start])[self readAtTime:[_action currentTime]];
|
||||||
|
}
|
||||||
|
- (void)endInteraction {[self finish];}
|
||||||
|
- (BOOL)commit:(PGInspectorState *)next {
|
||||||
|
if([next isEqual:_state] && _state.hasEdits)return YES;
|
||||||
|
id<FxParameterSettingAPI_v5> api=[_manager apiForProtocol:@protocol(FxParameterSettingAPI_v5)];
|
||||||
|
CMTime time=[_action currentTime];
|
||||||
|
// The published object is authoritative. Only mirror after its write was
|
||||||
|
// accepted; a rejected write must never leave half-applied hidden values.
|
||||||
|
if(![api setCustomParameterValue:next toParameter:PGInspectorPanel atTime:time]){_statusMessage=@"参数未能保存,请重试";return NO;}
|
||||||
|
for(NSDictionary *d in PGInspectorControls()) {
|
||||||
|
UInt32 key=[d[@"id"] unsignedIntValue];double value=[next valueForParameter:key];
|
||||||
|
if(![next isParameterEnabled:key])value=0;
|
||||||
|
double previous=[_state isParameterEnabled:key]?[_state valueForParameter:key]:0;
|
||||||
|
if(previous==value)continue;
|
||||||
|
if([d[@"kind"] isEqual:@"preset"]) [api setIntValue:(int)value toParameter:key atTime:time];
|
||||||
|
else if([d[@"kind"] isEqual:@"toggle"] || [d[@"kind"] isEqual:@"master"]) [api setBoolValue:value!=0 toParameter:key atTime:time];
|
||||||
|
else [api setFloatValue:value toParameter:key atTime:time];
|
||||||
|
}
|
||||||
|
_state=next;_pendingUntil=NSDate.timeIntervalSinceReferenceDate+.35;_statusMessage=@"";return YES;
|
||||||
|
}
|
||||||
|
- (BOOL)setValue:(double)value forParameter:(UInt32)identifier {
|
||||||
|
NSDictionary *d=PGInspectorDescriptor(identifier);if(!d || !isfinite(value))return NO;
|
||||||
|
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
|
||||||
|
BOOL ok=NO;@try {
|
||||||
|
NSMutableDictionary *values=[_state.values mutableCopy];values[@(identifier)]=@(PGControlValue(@(value),d));
|
||||||
|
ok=[self commit:[PGInspectorState stateWithValues:values disabledParameters:_state.disabledParameters hasEdits:YES]];
|
||||||
|
} @finally {if(owned)[self finish];}return ok;
|
||||||
|
}
|
||||||
|
- (BOOL)toggleEnabledForParameter:(UInt32)identifier {
|
||||||
|
NSDictionary *d=PGInspectorDescriptor(identifier);if(!d)return NO;
|
||||||
|
if([d[@"kind"] isEqual:@"master"] || [d[@"kind"] isEqual:@"toggle"])return [self setValue:![self.state valueForParameter:identifier] forParameter:identifier];
|
||||||
|
if(![d[@"bypass"] boolValue])return NO;
|
||||||
|
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
|
||||||
|
BOOL ok=NO;@try {
|
||||||
|
NSMutableSet *disabled=[_state.disabledParameters mutableCopy];NSNumber *key=@(identifier);
|
||||||
|
if([disabled containsObject:key])[disabled removeObject:key];else [disabled addObject:key];
|
||||||
|
ok=[self commit:[PGInspectorState stateWithValues:_state.values disabledParameters:disabled hasEdits:YES]];
|
||||||
|
} @finally {if(owned)[self finish];}return ok;
|
||||||
|
}
|
||||||
|
- (BOOL)resetParameter:(UInt32)identifier {
|
||||||
|
NSDictionary *d=PGInspectorDescriptor(identifier);if(!d)return NO;
|
||||||
|
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
|
||||||
|
BOOL ok=NO;@try {
|
||||||
|
NSMutableDictionary *values=[_state.values mutableCopy];values[@(identifier)]=d[@"default"];
|
||||||
|
NSMutableSet *disabled=[_state.disabledParameters mutableCopy];[disabled removeObject:@(identifier)];
|
||||||
|
ok=[self commit:[PGInspectorState stateWithValues:values disabledParameters:disabled hasEdits:YES]];
|
||||||
|
} @finally {if(owned)[self finish];}return ok;
|
||||||
|
}
|
||||||
|
- (void)dealloc {if(_interacting)[_action endAction:self];}
|
||||||
|
@end
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
#import "PGInspectorState.h"
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
/// Native inspector controls. Host access belongs exclusively to the controller.
|
||||||
|
@interface PGInspectorView : NSView
|
||||||
|
@property (nonatomic, strong, readonly) PGInspectorController *controller;
|
||||||
|
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager;
|
||||||
|
/// Also permits a deterministic controller in an offscreen preview or test.
|
||||||
|
- (instancetype)initWithController:(PGInspectorController *)controller;
|
||||||
|
- (void)refreshFromHost;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
#import "PGInspectorView.h"
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
static NSColor *PGUIColor(CGFloat r, CGFloat g, CGFloat b, CGFloat a) {
|
||||||
|
return [NSColor colorWithSRGBRed:r green:g blue:b alpha:a];
|
||||||
|
}
|
||||||
|
static NSColor *PGUIText(void) { return PGUIColor(.88,.91,.95,1); }
|
||||||
|
static NSColor *PGUIMuted(void) { return PGUIColor(.60,.65,.73,1); }
|
||||||
|
static NSTextField *PGUILabel(NSString *text, CGFloat size, NSFontWeight weight) {
|
||||||
|
NSTextField *label=[NSTextField labelWithString:text ?: @""];
|
||||||
|
label.font=[NSFont systemFontOfSize:size weight:weight]; label.textColor=PGUIText();
|
||||||
|
label.lineBreakMode=NSLineBreakByTruncatingTail; return label;
|
||||||
|
}
|
||||||
|
static CGFloat PGValueScale(NSDictionary *descriptor) {
|
||||||
|
return [descriptor[@"format"] isEqual:@"percent"] && [descriptor[@"max"] doubleValue]<=10 ? 100 : 1;
|
||||||
|
}
|
||||||
|
static NSString *PGFormattedValue(NSDictionary *descriptor, double value) {
|
||||||
|
NSString *format=descriptor[@"format"];
|
||||||
|
if ([format isEqual:@"percent"]) return [NSString stringWithFormat:@"%.0f%%",value*PGValueScale(descriptor)];
|
||||||
|
if ([format isEqual:@"degrees"]) return [NSString stringWithFormat:@"%+.0f°",value];
|
||||||
|
if ([format isEqual:@"signed"]) return [NSString stringWithFormat:@"%.2f",value];
|
||||||
|
if ([format isEqual:@"integer"]) return [NSString stringWithFormat:@"%.0f",value];
|
||||||
|
return [NSString stringWithFormat:@"%.1f",value];
|
||||||
|
}
|
||||||
|
|
||||||
|
@interface PGInspectorGlyphButton : NSButton
|
||||||
|
@property (nonatomic, copy) NSString *glyph;
|
||||||
|
@property (nonatomic, strong) NSColor *accent;
|
||||||
|
@property (nonatomic, strong) NSColor *swatch;
|
||||||
|
@end
|
||||||
|
@implementation PGInspectorGlyphButton
|
||||||
|
- (BOOL)isFlipped { return YES; }
|
||||||
|
- (void)drawRect:(NSRect)dirtyRect {
|
||||||
|
(void)dirtyRect; NSRect b=NSInsetRect(self.bounds,1,1);
|
||||||
|
CGFloat opacity=self.enabled?1:.35;
|
||||||
|
NSColor *accent=[self.accent colorWithAlphaComponent:opacity];
|
||||||
|
BOOL on=self.state==NSControlStateValueOn;
|
||||||
|
if ([self.glyph isEqual:@"swatch"]) {
|
||||||
|
NSRect circle=NSInsetRect(b,3,3);
|
||||||
|
[[(self.swatch ?: PGUIColor(.12,.19,.23,1)) colorWithAlphaComponent:opacity] setFill]; [[NSBezierPath bezierPathWithOvalInRect:circle] fill];
|
||||||
|
[[PGUIText() colorWithAlphaComponent:.25*opacity] setStroke];
|
||||||
|
NSBezierPath *ring=[NSBezierPath bezierPathWithOvalInRect:circle];ring.lineWidth=1;[ring stroke];
|
||||||
|
if (!self.swatch) {
|
||||||
|
[PGUIMuted() setStroke];NSBezierPath *slash=[NSBezierPath bezierPath];
|
||||||
|
[slash moveToPoint:NSMakePoint(NSMinX(circle)+4,NSMinY(circle)+4)];[slash lineToPoint:NSMakePoint(NSMaxX(circle)-4,NSMaxY(circle)-4)];slash.lineWidth=1.5;[slash stroke];
|
||||||
|
}
|
||||||
|
if(on) { [accent setStroke];NSBezierPath *selection=[NSBezierPath bezierPathWithOvalInRect:NSInsetRect(b,.5,.5)];selection.lineWidth=1.5;[selection stroke]; }
|
||||||
|
} else if ([self.glyph isEqual:@"switch"]) {
|
||||||
|
NSRect pill=NSInsetRect(b,1,3);
|
||||||
|
[(on?accent:PGUIColor(.20,.25,.34,opacity)) setFill];[[NSBezierPath bezierPathWithRoundedRect:pill xRadius:NSHeight(pill)/2 yRadius:NSHeight(pill)/2]fill];
|
||||||
|
CGFloat diameter=NSHeight(pill)-4;CGFloat x=on?NSMaxX(pill)-diameter-2:NSMinX(pill)+2;
|
||||||
|
[[NSColor colorWithWhite:.98 alpha:opacity]setFill];[[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(x,NSMinY(pill)+2,diameter,diameter)]fill];
|
||||||
|
} else {
|
||||||
|
NSBezierPath *back=[NSBezierPath bezierPathWithRoundedRect:b xRadius:7 yRadius:7];
|
||||||
|
[[self.accent colorWithAlphaComponent:([self.glyph isEqual:@"eye"]?.055:.13)*opacity]setFill];[back fill];
|
||||||
|
if ([self.glyph isEqual:@"eye"]) {
|
||||||
|
[[self.accent colorWithAlphaComponent:.35*opacity]setStroke];back.lineWidth=.7;[back stroke];
|
||||||
|
NSPoint c=NSMakePoint(NSMidX(b),NSMidY(b));
|
||||||
|
NSBezierPath *eye=[NSBezierPath bezierPath];[eye moveToPoint:NSMakePoint(c.x-6,c.y)];
|
||||||
|
[eye curveToPoint:NSMakePoint(c.x+6,c.y) controlPoint1:NSMakePoint(c.x-3,c.y-5.5) controlPoint2:NSMakePoint(c.x+3,c.y-5.5)];
|
||||||
|
[eye curveToPoint:NSMakePoint(c.x-6,c.y) controlPoint1:NSMakePoint(c.x+3,c.y+5.5) controlPoint2:NSMakePoint(c.x-3,c.y+5.5)];eye.lineWidth=1.3;[accent setStroke];[eye stroke];
|
||||||
|
if(on){[accent setFill];[[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(c.x-1.8,c.y-1.8,3.6,3.6)]fill];}
|
||||||
|
else{NSBezierPath *slash=[NSBezierPath bezierPath];[slash moveToPoint:NSMakePoint(c.x-6,c.y+6)];[slash lineToPoint:NSMakePoint(c.x+6,c.y-6)];slash.lineWidth=1.2;[slash stroke];}
|
||||||
|
} else {
|
||||||
|
NSString *symbol=[self.glyph isEqual:@"disclosure"]?(on?@"⌄":@"›"):@"↻";
|
||||||
|
NSFont *font=[NSFont systemFontOfSize:[self.glyph isEqual:@"disclosure"]?19:18 weight:NSFontWeightSemibold];
|
||||||
|
NSDictionary *attributes=@{NSFontAttributeName:font,NSForegroundColorAttributeName:[self.accent colorWithAlphaComponent:(on?1:.58)*opacity]};
|
||||||
|
NSSize size=[symbol sizeWithAttributes:attributes];[symbol drawAtPoint:NSMakePoint(NSMidX(b)-size.width/2,NSMidY(b)-size.height/2)withAttributes:attributes];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(self.window.firstResponder==self){[[self.accent colorWithAlphaComponent:.8]setStroke];NSBezierPath *focus=[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(self.bounds,.5,.5) xRadius:7 yRadius:7];focus.lineWidth=1;[focus stroke];}
|
||||||
|
}
|
||||||
|
- (void)setState:(NSControlStateValue)state { [super setState:state];self.needsDisplay=YES; }
|
||||||
|
- (void)setEnabled:(BOOL)enabled { [super setEnabled:enabled];self.needsDisplay=YES; }
|
||||||
|
@end
|
||||||
|
static PGInspectorGlyphButton *PGUIButton(NSString *glyph, NSColor *accent, id target, SEL action) {
|
||||||
|
PGInspectorGlyphButton *button=[[PGInspectorGlyphButton alloc]initWithFrame:NSZeroRect];
|
||||||
|
button.glyph=glyph;button.accent=accent;button.bordered=NO;button.title=@"";
|
||||||
|
[button setButtonType:([glyph isEqual:@"eye"]||[glyph isEqual:@"switch"]||[glyph isEqual:@"disclosure"]||[glyph isEqual:@"swatch"])?NSButtonTypePushOnPushOff:NSButtonTypeMomentaryPushIn];
|
||||||
|
button.target=target;button.action=action;button.focusRingType=NSFocusRingTypeExterior;
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
@interface PGInspectorSliderCell : NSSliderCell
|
||||||
|
@property (nonatomic, strong) NSColor *accent;
|
||||||
|
@end
|
||||||
|
@implementation PGInspectorSliderCell
|
||||||
|
- (void)drawBarInside:(NSRect)rect flipped:(BOOL)flipped {
|
||||||
|
(void)flipped;NSRect bar=NSMakeRect(NSMinX(rect),NSMidY(rect)-2,NSWidth(rect),4);
|
||||||
|
[PGUIColor(.08,.13,.20,self.enabled?1:.5)setFill];NSBezierPath *track=[NSBezierPath bezierPathWithRoundedRect:bar xRadius:2 yRadius:2];[track fill];
|
||||||
|
[[self.accent colorWithAlphaComponent:.18]setStroke];track.lineWidth=.7;[track stroke];
|
||||||
|
double range=self.maxValue-self.minValue;if(range<=0)return;
|
||||||
|
CGFloat from=self.minValue<0?(CGFloat)((0-self.minValue)/range):0;
|
||||||
|
CGFloat to=(CGFloat)((self.doubleValue-self.minValue)/range);
|
||||||
|
NSRect fill=NSMakeRect(NSMinX(bar)+MIN(from,to)*NSWidth(bar),NSMinY(bar),fabs(to-from)*NSWidth(bar),NSHeight(bar));
|
||||||
|
[[self.accent colorWithAlphaComponent:self.enabled?.85:.23]setFill];[[NSBezierPath bezierPathWithRoundedRect:fill xRadius:2 yRadius:2]fill];
|
||||||
|
}
|
||||||
|
- (void)drawKnob:(NSRect)knobRect {
|
||||||
|
CGFloat d=12;NSRect r=NSMakeRect(NSMidX(knobRect)-d/2,NSMidY(knobRect)-d/2,d,d);
|
||||||
|
[[self.accent colorWithAlphaComponent:self.enabled?.22:.08]setFill];[[NSBezierPath bezierPathWithOvalInRect:NSInsetRect(r,-2,-2)]fill];
|
||||||
|
[[NSColor colorWithWhite:.97 alpha:self.enabled?1:.5]setFill];NSBezierPath *knob=[NSBezierPath bezierPathWithOvalInRect:r];[knob fill];
|
||||||
|
[[self.accent colorWithAlphaComponent:self.enabled?.85:.25]setStroke];knob.lineWidth=1;[knob stroke];
|
||||||
|
}
|
||||||
|
- (CGFloat)knobThickness { return 14; }
|
||||||
|
@end
|
||||||
|
@interface PGInspectorSlider : NSSlider
|
||||||
|
@property (nonatomic, copy) void (^interactionBegan)(void);
|
||||||
|
@property (nonatomic, copy) void (^interactionEnded)(void);
|
||||||
|
@end
|
||||||
|
@implementation PGInspectorSlider
|
||||||
|
- (void)mouseDown:(NSEvent *)event {
|
||||||
|
if(!self.enabled)return;if(self.interactionBegan)self.interactionBegan();
|
||||||
|
@try{[super mouseDown:event];}@finally{if(self.interactionEnded)self.interactionEnded();}
|
||||||
|
}
|
||||||
|
- (void)keyDown:(NSEvent *)event {
|
||||||
|
if(self.interactionBegan)self.interactionBegan();
|
||||||
|
@try{[super keyDown:event];}@finally{if(self.interactionEnded)self.interactionEnded();}
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface PGInspectorNumberCell : NSTextFieldCell
|
||||||
|
@end
|
||||||
|
@implementation PGInspectorNumberCell
|
||||||
|
- (NSRect)drawingRectForBounds:(NSRect)bounds {
|
||||||
|
NSRect rect=[super drawingRectForBounds:bounds];CGFloat height=[self cellSizeForBounds:bounds].height;
|
||||||
|
if(NSHeight(rect)>height){rect.origin.y+=(NSHeight(rect)-height)*.5;rect.size.height=height;}return rect;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
@interface PGInspectorNumberField : NSTextField
|
||||||
|
@end
|
||||||
|
@implementation PGInspectorNumberField
|
||||||
|
+ (Class)cellClass { return PGInspectorNumberCell.class; }
|
||||||
|
@end
|
||||||
|
|
||||||
|
@class PGInspectorRow;
|
||||||
|
@interface PGInspectorCard : NSView
|
||||||
|
@property (nonatomic, strong) NSColor *accent;
|
||||||
|
@property (nonatomic, copy) NSString *heading;
|
||||||
|
@property (nonatomic, strong) NSTextField *titleLabel;
|
||||||
|
@property (nonatomic, strong) PGInspectorGlyphButton *master;
|
||||||
|
@property (nonatomic, copy) NSArray<PGInspectorRow *> *rows;
|
||||||
|
@property (nonatomic) BOOL expanded;
|
||||||
|
- (CGFloat)heightForWidth:(CGFloat)width;
|
||||||
|
@end
|
||||||
|
@interface PGInspectorView ()
|
||||||
|
@property (nonatomic, strong, readwrite) PGInspectorController *controller;
|
||||||
|
- (void)reloadControls;
|
||||||
|
- (void)beginControlInteraction;
|
||||||
|
- (void)endControlInteraction;
|
||||||
|
- (void)writeValue:(double)value parameter:(UInt32)identifier;
|
||||||
|
@end
|
||||||
|
@interface PGInspectorRow : NSView <NSTextFieldDelegate>
|
||||||
|
@property (nonatomic, weak) PGInspectorView *inspector;
|
||||||
|
@property (nonatomic, copy) NSDictionary *descriptor;
|
||||||
|
@property (nonatomic, strong) NSColor *accent;
|
||||||
|
@property (nonatomic, strong) NSTextField *label;
|
||||||
|
@property (nonatomic, strong) NSTextField *valueField;
|
||||||
|
@property (nonatomic, strong) NSTextField *hint;
|
||||||
|
@property (nonatomic, strong) PGInspectorSlider *slider;
|
||||||
|
@property (nonatomic, strong) PGInspectorGlyphButton *eye;
|
||||||
|
@property (nonatomic, strong) PGInspectorGlyphButton *reset;
|
||||||
|
@property (nonatomic, strong) PGInspectorGlyphButton *toggle;
|
||||||
|
@property (nonatomic, copy) NSArray<PGInspectorGlyphButton *> *swatches;
|
||||||
|
- (instancetype)initWithDescriptor:(NSDictionary *)descriptor accent:(NSColor *)accent inspector:(PGInspectorView *)inspector;
|
||||||
|
- (CGFloat)heightForWidth:(CGFloat)width;
|
||||||
|
- (void)reloadState:(PGInspectorState *)state master:(BOOL)master available:(BOOL)available;
|
||||||
|
@end
|
||||||
|
@implementation PGInspectorRow
|
||||||
|
- (BOOL)isFlipped { return YES; }
|
||||||
|
- (instancetype)initWithDescriptor:(NSDictionary *)descriptor accent:(NSColor *)accent inspector:(PGInspectorView *)inspector {
|
||||||
|
if((self=[super initWithFrame:NSZeroRect])) {
|
||||||
|
_descriptor=[descriptor copy];_accent=accent;_inspector=inspector;
|
||||||
|
self.identifier=[NSString stringWithFormat:@"pg.parameter.%@",descriptor[@"id"]];
|
||||||
|
NSString *name=descriptor[@"name"];_label=PGUILabel(name,12,NSFontWeightSemibold);[self addSubview:_label];
|
||||||
|
NSString *kind=descriptor[@"kind"];
|
||||||
|
if([descriptor[@"bypass"]boolValue]){
|
||||||
|
_eye=PGUIButton(@"eye",accent,self,@selector(toggleEnabled:));_eye.toolTip=[@"启用 / 停用 · " stringByAppendingString:name];_eye.accessibilityLabel=_eye.toolTip;[self addSubview:_eye];
|
||||||
|
}
|
||||||
|
if([kind isEqual:@"preset"]){
|
||||||
|
NSArray *colors=@[NSNull.null,PGUIColor(.95,.87,.80,1),PGUIColor(.88,.88,.95,1),PGUIColor(1,.86,.72,1),PGUIColor(.99,.78,.75,1),PGUIColor(.76,.57,.39,1),PGUIColor(.48,.32,.22,1)];
|
||||||
|
NSArray *options=descriptor[@"options"] ?: @[];NSMutableArray *buttons=[NSMutableArray new];
|
||||||
|
for(NSUInteger i=0;i<7;i++){
|
||||||
|
PGInspectorGlyphButton *button=PGUIButton(@"swatch",accent,self,@selector(selectPreset:));button.tag=i;button.swatch=i?colors[i]:nil;
|
||||||
|
button.toolTip=i<options.count?options[i]:[NSString stringWithFormat:@"%@ %lu",name,(unsigned long)i];button.accessibilityLabel=button.toolTip;
|
||||||
|
[self addSubview:button];[buttons addObject:button];
|
||||||
|
}_swatches=[buttons copy];
|
||||||
|
}else{
|
||||||
|
_reset=PGUIButton(@"reset",accent,self,@selector(resetValue:));_reset.toolTip=[@"重置 · "stringByAppendingString:name];_reset.accessibilityLabel=_reset.toolTip;[self addSubview:_reset];
|
||||||
|
if([kind isEqual:@"toggle"]){
|
||||||
|
_toggle=PGUIButton(@"switch",accent,self,@selector(toggleValue:));_toggle.accessibilityLabel=name;[self addSubview:_toggle];
|
||||||
|
_hint=PGUILabel(@"",10,NSFontWeightMedium);_hint.textColor=PGUIMuted();_hint.alignment=NSTextAlignmentRight;[self addSubview:_hint];
|
||||||
|
_label.lineBreakMode=NSLineBreakByWordWrapping;_label.maximumNumberOfLines=2;
|
||||||
|
}else{
|
||||||
|
_slider=[[PGInspectorSlider alloc]initWithFrame:NSZeroRect];PGInspectorSliderCell *cell=[PGInspectorSliderCell new];cell.accent=accent;_slider.cell=cell;
|
||||||
|
_slider.minValue=[descriptor[@"min"]doubleValue];_slider.maxValue=[descriptor[@"max"]doubleValue];_slider.continuous=YES;_slider.target=self;_slider.action=@selector(sliderChanged:);_slider.accessibilityLabel=name;
|
||||||
|
__weak PGInspectorView *weakInspector=inspector;
|
||||||
|
_slider.interactionBegan=^{[weakInspector beginControlInteraction];};_slider.interactionEnded=^{[weakInspector endControlInteraction];};[self addSubview:_slider];
|
||||||
|
_valueField=[[PGInspectorNumberField alloc]initWithFrame:NSZeroRect];_valueField.font=[NSFont monospacedDigitSystemFontOfSize:11 weight:NSFontWeightSemibold];_valueField.textColor=PGUIText();_valueField.alignment=NSTextAlignmentCenter;
|
||||||
|
_valueField.bordered=NO;_valueField.bezeled=NO;_valueField.drawsBackground=NO;_valueField.backgroundColor=PGUIColor(.14,.19,.26,1);_valueField.wantsLayer=YES;_valueField.layer.backgroundColor=_valueField.backgroundColor.CGColor;_valueField.layer.cornerRadius=7;_valueField.layer.borderWidth=.7;_valueField.layer.borderColor=[accent colorWithAlphaComponent:.24].CGColor;
|
||||||
|
_valueField.target=self;_valueField.action=@selector(numberCommitted:);_valueField.delegate=self;_valueField.accessibilityLabel=[name stringByAppendingString:@"数值"];[self addSubview:_valueField];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NSString *key=[descriptor[@"id"]stringValue];
|
||||||
|
_slider.identifier=[@"pg.value."stringByAppendingString:key];_valueField.identifier=[@"pg.number."stringByAppendingString:key];
|
||||||
|
_toggle.identifier=[@"pg.toggle."stringByAppendingString:key];_eye.identifier=[@"pg.enable."stringByAppendingString:key];_reset.identifier=[@"pg.reset."stringByAppendingString:key];
|
||||||
|
}return self;
|
||||||
|
}
|
||||||
|
- (CGFloat)heightForWidth:(CGFloat)width {
|
||||||
|
if(_swatches)return 46;
|
||||||
|
if(_toggle)return width<310 && [_descriptor[@"name"]length]>12 ? 48 : 36;
|
||||||
|
return width<310?52:36;
|
||||||
|
}
|
||||||
|
- (void)layout {
|
||||||
|
[super layout];CGFloat w=NSWidth(self.bounds),h=NSHeight(self.bounds);BOOL narrow=w<310;
|
||||||
|
CGFloat labelX=_eye?25:0;
|
||||||
|
_eye.frame=NSMakeRect(0,6,20,22);
|
||||||
|
if(_swatches){
|
||||||
|
_label.frame=NSMakeRect(labelX,14,33,20);CGFloat x=labelX+37;CGFloat d=MIN(28,MAX(12,(w-x-24)/7));CGFloat gap=MIN(8,MAX(0,(w-x-7*d)/6));
|
||||||
|
for(NSUInteger i=0;i<_swatches.count;i++)_swatches[i].frame=NSMakeRect(x+i*(d+gap),7,d,d);return;
|
||||||
|
}
|
||||||
|
_reset.frame=NSMakeRect(w-23,6,23,23);
|
||||||
|
if(_toggle){
|
||||||
|
CGFloat switchX=w-76;_toggle.frame=NSMakeRect(switchX,4+(h-36)/2,46,28);_hint.frame=NSMakeRect(switchX-39,10+(h-36)/2,34,18);
|
||||||
|
_label.frame=NSMakeRect(labelX,h>36?3:9,MAX(20,switchX-labelX-42),h>36?42:22);return;
|
||||||
|
}
|
||||||
|
CGFloat fieldW=49,fieldX=w-23-7-fieldW;_valueField.frame=NSMakeRect(fieldX,5,fieldW,25);
|
||||||
|
if(narrow){_label.frame=NSMakeRect(labelX,8,MAX(20,fieldX-labelX-6),22);_slider.frame=NSMakeRect(labelX,29,MAX(20,w-labelX-30),18);}
|
||||||
|
else{CGFloat labelW=MIN(116,MAX(71,w*.29));_label.frame=NSMakeRect(labelX,9,labelW,21);CGFloat start=labelX+labelW+5;_slider.frame=NSMakeRect(start,7,MAX(20,fieldX-start-8),22);}
|
||||||
|
}
|
||||||
|
- (void)reloadState:(PGInspectorState *)state master:(BOOL)master available:(BOOL)available {
|
||||||
|
UInt32 identifier=[_descriptor[@"id"]unsignedIntValue];double value=[state valueForParameter:identifier];BOOL enabled=[state isParameterEnabled:identifier];
|
||||||
|
_label.alphaValue=master&&enabled?1:.5;_eye.state=enabled?NSControlStateValueOn:NSControlStateValueOff;_eye.enabled=available&&master;
|
||||||
|
_reset.enabled=available;_reset.state=fabs(value-[_descriptor[@"default"]doubleValue])>.000001?NSControlStateValueOn:NSControlStateValueOff;
|
||||||
|
_slider.enabled=available&&master&&enabled;_slider.doubleValue=value;
|
||||||
|
_valueField.enabled=available&&master&&enabled;if(!_valueField.currentEditor)_valueField.stringValue=PGFormattedValue(_descriptor,value);
|
||||||
|
_toggle.enabled=available&&master&&enabled;_toggle.state=value>.5?NSControlStateValueOn:NSControlStateValueOff;
|
||||||
|
UInt32 identifierForHint=[_descriptor[@"id"]unsignedIntValue];
|
||||||
|
_hint.stringValue=identifierForHint==PGFastMulti?(value>.5?@"多人":@"单人"):identifierForHint==PGTrackBoxes?(value>.5?@"预览中":@"隐藏"):(value>.5?@"开启":@"关闭");
|
||||||
|
for(NSUInteger i=0;i<_swatches.count;i++){_swatches[i].enabled=available&&master&&enabled;_swatches[i].state=i==(NSUInteger)llround(value)?NSControlStateValueOn:NSControlStateValueOff;}
|
||||||
|
}
|
||||||
|
- (void)sliderChanged:(NSSlider *)sender { [_inspector writeValue:sender.doubleValue parameter:[_descriptor[@"id"]unsignedIntValue]]; }
|
||||||
|
- (void)toggleValue:(NSButton *)sender { [_inspector writeValue:sender.state==NSControlStateValueOn?1:0 parameter:[_descriptor[@"id"]unsignedIntValue]]; }
|
||||||
|
- (void)selectPreset:(NSButton *)sender { [_inspector writeValue:sender.tag parameter:[_descriptor[@"id"]unsignedIntValue]]; }
|
||||||
|
- (void)toggleEnabled:(id)sender { (void)sender;[_inspector.controller toggleEnabledForParameter:[_descriptor[@"id"]unsignedIntValue]];[_inspector reloadControls]; }
|
||||||
|
- (void)resetValue:(id)sender { (void)sender;[_inspector.controller resetParameter:[_descriptor[@"id"]unsignedIntValue]];[_inspector reloadControls]; }
|
||||||
|
- (void)numberCommitted:(id)sender {
|
||||||
|
(void)sender;NSString *text=[_valueField.stringValue stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||||
|
if([text hasSuffix:@"%"]||[text hasSuffix:@"°"])text=[text substringToIndex:text.length-1];
|
||||||
|
NSNumber *number=nil;
|
||||||
|
for(NSLocale *locale in @[NSLocale.currentLocale,[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]]){
|
||||||
|
NSScanner *scanner=[NSScanner scannerWithString:text];scanner.locale=locale;double parsed;
|
||||||
|
if([scanner scanDouble:&parsed]&&scanner.isAtEnd){number=@(parsed);break;}
|
||||||
|
}
|
||||||
|
if(number&&isfinite(number.doubleValue)){
|
||||||
|
double value=number.doubleValue/PGValueScale(_descriptor);value=fmin([_descriptor[@"max"]doubleValue],fmax([_descriptor[@"min"]doubleValue],value));
|
||||||
|
if(fabs(value-[_inspector.controller.state valueForParameter:[_descriptor[@"id"]unsignedIntValue]])>.0000001)[_inspector writeValue:value parameter:[_descriptor[@"id"]unsignedIntValue]];
|
||||||
|
}else NSBeep();
|
||||||
|
_valueField.stringValue=PGFormattedValue(_descriptor,[_inspector.controller.state valueForParameter:[_descriptor[@"id"]unsignedIntValue]]);
|
||||||
|
}
|
||||||
|
- (void)controlTextDidEndEditing:(NSNotification *)notification { (void)notification;[self numberCommitted:_valueField]; }
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation PGInspectorCard
|
||||||
|
- (BOOL)isFlipped { return YES; }
|
||||||
|
- (CGFloat)heightForWidth:(CGFloat)width {
|
||||||
|
if(!_expanded)return 44;CGFloat height=49;for(PGInspectorRow *row in _rows){height+=[row heightForWidth:MAX(0,width-24)];if([row.descriptor[@"id"]unsignedIntValue]==PGJumpGuard)height+=8;}return height+7;
|
||||||
|
}
|
||||||
|
- (void)layout {
|
||||||
|
[super layout];CGFloat w=NSWidth(self.bounds);_titleLabel.frame=NSMakeRect(25,13,MAX(70,w-80),22);_master.frame=NSMakeRect(w-39,10,25,25);
|
||||||
|
CGFloat y=49;for(PGInspectorRow *row in _rows){row.hidden=!_expanded;if(!_expanded)continue;CGFloat h=[row heightForWidth:w-24];row.frame=NSMakeRect(12,y,w-24,h);y+=h;if([row.descriptor[@"id"]unsignedIntValue]==PGJumpGuard)y+=8;}
|
||||||
|
}
|
||||||
|
- (void)drawRect:(NSRect)dirtyRect {
|
||||||
|
(void)dirtyRect;NSBezierPath *shape=[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(self.bounds,.5,.5) xRadius:13 yRadius:13];
|
||||||
|
[NSGraphicsContext saveGraphicsState];[shape addClip];NSGradient *gradient=[[NSGradient alloc]initWithStartingColor:[_accent blendedColorWithFraction:.87 ofColor:PGUIColor(.09,.14,.21,1)] endingColor:PGUIColor(.055,.085,.14,1)];[gradient drawInRect:self.bounds angle:90];[NSGraphicsContext restoreGraphicsState];
|
||||||
|
[[_accent colorWithAlphaComponent:.22]setStroke];shape.lineWidth=.8;[shape stroke];
|
||||||
|
NSRect pill=NSMakeRect(12,10,MAX(53,[_heading length]*13+20),25);[[_accent colorWithAlphaComponent:.10]setFill];NSBezierPath *p=[NSBezierPath bezierPathWithRoundedRect:pill xRadius:8 yRadius:8];[p fill];[[_accent colorWithAlphaComponent:.32]setStroke];p.lineWidth=.7;[p stroke];
|
||||||
|
if(_expanded){[[PGUIMuted() colorWithAlphaComponent:.20]setStroke];NSBezierPath *line=[NSBezierPath bezierPath];[line moveToPoint:NSMakePoint(12,43)];[line lineToPoint:NSMakePoint(NSWidth(self.bounds)-12,43)];line.lineWidth=.5;[line stroke];
|
||||||
|
for(PGInspectorRow *row in _rows)if([row.descriptor[@"id"]unsignedIntValue]==PGJumpGuard){CGFloat y=NSMaxY(row.frame)+4;[line removeAllPoints];[line moveToPoint:NSMakePoint(12,y)];[line lineToPoint:NSMakePoint(NSWidth(self.bounds)-12,y)];[line stroke];}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation PGInspectorView {
|
||||||
|
NSArray<PGInspectorCard *> *_cards;
|
||||||
|
PGInspectorCard *_more;
|
||||||
|
NSTextField *_brand,*_website,*_status;
|
||||||
|
NSTimer *_refreshTimer;
|
||||||
|
NSUInteger _interactionDepth;
|
||||||
|
}
|
||||||
|
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager { return [self initWithController:[[PGInspectorController alloc]initWithAPIManager:manager]]; }
|
||||||
|
- (instancetype)initWithController:(PGInspectorController *)controller {
|
||||||
|
if((self=[super initWithFrame:NSMakeRect(0,0,320,100)])){
|
||||||
|
_controller=controller;self.appearance=[NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
|
||||||
|
NSArray *sections=@[@"smooth",@"color",@"shape",@"more"],*headings=@[@"磨皮",@"美白",@"瘦脸",@"更多设置"];
|
||||||
|
NSArray *colors=@[PGUIColor(.28,.83,.96,1),PGUIColor(.34,.89,.84,1),PGUIColor(.59,.69,.98,1),PGUIColor(.54,.64,.76,1)];
|
||||||
|
NSMutableArray *cards=[NSMutableArray new];
|
||||||
|
for(NSUInteger index=0;index<sections.count;index++){
|
||||||
|
NSString *section=sections[index];PGInspectorCard *card=[[PGInspectorCard alloc]initWithFrame:NSZeroRect];card.accent=colors[index];card.heading=headings[index];card.expanded=index<3;
|
||||||
|
card.titleLabel=PGUILabel(headings[index],12,NSFontWeightSemibold);card.titleLabel.textColor=colors[index];[card addSubview:card.titleLabel];NSMutableArray *rows=[NSMutableArray new];
|
||||||
|
for(NSDictionary *descriptor in PGInspectorControls()){
|
||||||
|
if(![descriptor[@"section"]isEqual:section])continue;
|
||||||
|
if([descriptor[@"kind"]isEqual:@"master"]){card.master=PGUIButton(@"eye",colors[index],self,@selector(masterChanged:));card.master.tag=[descriptor[@"id"]integerValue];card.master.identifier=[NSString stringWithFormat:@"pg.master.%@",descriptor[@"id"]];card.master.accessibilityLabel=descriptor[@"name"];card.master.toolTip=descriptor[@"name"];[card addSubview:card.master];continue;}
|
||||||
|
PGInspectorRow *row=[[PGInspectorRow alloc]initWithDescriptor:descriptor accent:colors[index] inspector:self];[rows addObject:row];[card addSubview:row];
|
||||||
|
}
|
||||||
|
card.rows=rows;if(index==3){_more=card;card.master=PGUIButton(@"disclosure",colors[index],self,@selector(moreChanged:));card.master.identifier=@"pg.more";card.master.accessibilityLabel=@"展开更多设置";[card addSubview:card.master];}
|
||||||
|
[self addSubview:card];[cards addObject:card];
|
||||||
|
}_cards=[cards copy];
|
||||||
|
_brand=PGUILabel(@"大鱼 · 人像美化",15,NSFontWeightSemibold);_brand.alignment=NSTextAlignmentCenter;[self addSubview:_brand];
|
||||||
|
_website=PGUILabel(@"dayu.media",10,NSFontWeightMedium);_website.alignment=NSTextAlignmentCenter;_website.textColor=PGUIMuted();[self addSubview:_website];
|
||||||
|
_status=PGUILabel(@"",10,NSFontWeightRegular);_status.textColor=PGUIMuted();_status.alignment=NSTextAlignmentCenter;[self addSubview:_status];
|
||||||
|
[self reloadControls];[self setFrameSize:NSMakeSize(320,[self heightForWidth:320])];
|
||||||
|
}return self;
|
||||||
|
}
|
||||||
|
- (BOOL)isFlipped { return YES; }
|
||||||
|
- (CGFloat)heightForWidth:(CGFloat)width { CGFloat h=12;for(PGInspectorCard *card in _cards)h+=[card heightForWidth:MAX(0,width-20)]+10;return h+83; }
|
||||||
|
- (NSSize)intrinsicContentSize { return NSMakeSize(NSViewNoIntrinsicMetric,[self heightForWidth:NSWidth(self.bounds)>0?NSWidth(self.bounds):320]); }
|
||||||
|
- (NSSize)fittingSize { CGFloat w=NSWidth(self.bounds)>0?NSWidth(self.bounds):320;return NSMakeSize(w,[self heightForWidth:w]); }
|
||||||
|
- (void)setFrameSize:(NSSize)newSize { if(_cards)newSize.height=[self heightForWidth:newSize.width];[super setFrameSize:newSize];[self invalidateIntrinsicContentSize];self.needsLayout=YES; }
|
||||||
|
- (void)layout {
|
||||||
|
[super layout];CGFloat w=NSWidth(self.bounds),y=12;for(PGInspectorCard *card in _cards){CGFloat h=[card heightForWidth:w-20];card.frame=NSMakeRect(10,y,MAX(0,w-20),h);y+=h+10;}
|
||||||
|
_brand.frame=NSMakeRect(12,y+12,MAX(0,w-24),23);_website.frame=NSMakeRect(12,y+38,MAX(0,w-24),17);_status.frame=NSMakeRect(12,y+61,MAX(0,w-24),16);
|
||||||
|
}
|
||||||
|
- (void)drawRect:(NSRect)dirtyRect { [PGUIColor(.035,.060,.105,1)setFill];NSRectFill(dirtyRect); }
|
||||||
|
- (void)reloadControls {
|
||||||
|
PGInspectorState *state=_controller.state;BOOL available=_controller.available;
|
||||||
|
for(PGInspectorCard *card in _cards){BOOL master=card==_more || [state valueForParameter:(UInt32)card.master.tag]>.5;
|
||||||
|
if(card!=_more){card.master.state=master?NSControlStateValueOn:NSControlStateValueOff;card.master.enabled=available;}
|
||||||
|
for(PGInspectorRow *row in card.rows)[row reloadState:state master:master available:available];
|
||||||
|
}
|
||||||
|
_status.stringValue=_controller.statusMessage.length?_controller.statusMessage:@"自动跟随 · 无需手动分析";_status.toolTip=_status.stringValue;
|
||||||
|
}
|
||||||
|
- (BOOL)hasActiveTextEditor { for(PGInspectorCard *card in _cards)for(PGInspectorRow *row in card.rows)if(row.valueField.currentEditor)return YES;return NO; }
|
||||||
|
- (void)refreshFromHost { if(_interactionDepth || [self hasActiveTextEditor])return;[_controller refresh];[self reloadControls]; }
|
||||||
|
- (void)writeValue:(double)value parameter:(UInt32)identifier { [_controller setValue:value forParameter:identifier];[self reloadControls]; }
|
||||||
|
- (void)beginControlInteraction { if(_interactionDepth++==0)[_controller beginInteraction]; }
|
||||||
|
- (void)endControlInteraction { if(!_interactionDepth)return;if(--_interactionDepth==0){[_controller endInteraction];[self refreshFromHost];} }
|
||||||
|
- (void)masterChanged:(NSButton *)sender { [_controller setValue:sender.state==NSControlStateValueOn?1:0 forParameter:(UInt32)sender.tag];[self reloadControls]; }
|
||||||
|
- (void)moreChanged:(NSButton *)sender { _more.expanded=sender.state==NSControlStateValueOn;sender.accessibilityLabel=_more.expanded?@"收起更多设置":@"展开更多设置";[_more setNeedsDisplay:YES];[self setFrameSize:self.frame.size]; }
|
||||||
|
- (void)viewDidMoveToWindow {
|
||||||
|
[super viewDidMoveToWindow];[_refreshTimer invalidate];_refreshTimer=nil;
|
||||||
|
if(self.window){[self refreshFromHost];__weak PGInspectorView *weakSelf=self;_refreshTimer=[NSTimer timerWithTimeInterval:.65 repeats:YES block:^(NSTimer *timer){PGInspectorView *view=weakSelf;if(!view||!view.window){[timer invalidate];return;}[view refreshFromHost];}];[[NSRunLoop mainRunLoop]addTimer:_refreshTimer forMode:NSRunLoopCommonModes];}
|
||||||
|
}
|
||||||
|
- (void)dealloc { [_refreshTimer invalidate]; }
|
||||||
|
@end
|
||||||
@@ -13,5 +13,6 @@ enum {
|
|||||||
PGAnalyzeForward=50, PGAnalyzeBackward=51, PGClearTrack=52, PGStatus=53,
|
PGAnalyzeForward=50, PGAnalyzeBackward=51, PGClearTrack=52, PGStatus=53,
|
||||||
PGTrackData1=60, PGTrackData2=61, PGTrackData3=62,
|
PGTrackData1=60, PGTrackData2=61, PGTrackData3=62,
|
||||||
PGSmoothingEnabled=70, PGColorEnabled=71, PGShapeEnabled=72,
|
PGSmoothingEnabled=70, PGColorEnabled=71, PGShapeEnabled=72,
|
||||||
PGDetailEnabled=73, PGHelp=74
|
PGDetailEnabled=73, PGHelp=74,
|
||||||
|
PGInspectorPanel=210
|
||||||
};
|
};
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
#import <FxPlug/FxPlugSDK.h>
|
#import <FxPlug/FxPlugSDK.h>
|
||||||
#import "PGBeautyTypes.h"
|
#import "PGBeautyTypes.h"
|
||||||
|
|
||||||
@interface beatygrilPlugIn : NSObject <FxTileableEffect, FxAnalyzer>
|
@interface beatygrilPlugIn : NSObject <FxTileableEffect, FxAnalyzer, FxCustomParameterViewHost_v2>
|
||||||
@property (nonatomic, assign) id<PROAPIAccessing> apiManager;
|
@property (nonatomic, assign) id<PROAPIAccessing> apiManager;
|
||||||
- (PGBeautySettings)settingsAtTime:(CMTime)time;
|
- (PGBeautySettings)settingsAtTime:(CMTime)time;
|
||||||
- (void)analyzeForward;
|
- (void)analyzeForward;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
#import "PGParameterIDs.h"
|
#import "PGParameterIDs.h"
|
||||||
#import "PGBeautyRenderer.h"
|
#import "PGBeautyRenderer.h"
|
||||||
#import "PGFaceProcessor.h"
|
#import "PGFaceProcessor.h"
|
||||||
|
#import "PGInspectorState.h"
|
||||||
|
#import "PGInspectorView.h"
|
||||||
#import <Metal/Metal.h>
|
#import <Metal/Metal.h>
|
||||||
#import <IOSurface/IOSurface.h>
|
#import <IOSurface/IOSurface.h>
|
||||||
#import <stddef.h>
|
#import <stddef.h>
|
||||||
@@ -90,7 +92,6 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
|||||||
@property BOOL analyzing;
|
@property BOOL analyzing;
|
||||||
@property BOOL analysisForward;
|
@property BOOL analysisForward;
|
||||||
@property (strong) NSArray<NSString *> *loadedTracks;
|
@property (strong) NSArray<NSString *> *loadedTracks;
|
||||||
@property uint32_t compatibleTrackMask;
|
|
||||||
@property BOOL reportedRender;
|
@property BOOL reportedRender;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@@ -120,8 +121,8 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
|||||||
PGSlider spec=PGSliders[i]; if (spec.identifier!=identifier) continue;
|
PGSlider spec=PGSliders[i]; if (spec.identifier!=identifier) continue;
|
||||||
NSString *name=[NSString stringWithUTF8String:spec.name];
|
NSString *name=[NSString stringWithUTF8String:spec.name];
|
||||||
float value=*(float *)((uint8_t *)&defaults+spec.offset);
|
float value=*(float *)((uint8_t *)&defaults+spec.offset);
|
||||||
if (spec.percent) return [api addPercentSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_DEFAULT];
|
if (spec.percent) return [api addPercentSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_HIDDEN];
|
||||||
return [api addFloatSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_DEFAULT];
|
return [api addFloatSliderWithName:name parameterID:identifier defaultValue:value parameterMin:spec.min parameterMax:spec.max sliderMin:spec.min sliderMax:spec.max delta:spec.step parameterFlags:kFxParameterFlag_HIDDEN];
|
||||||
}
|
}
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
@@ -129,37 +130,47 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
|||||||
id<FxParameterCreationAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterCreationAPI_v5)];
|
id<FxParameterCreationAPI_v5> api=[_apiManager apiForProtocol:@protocol(FxParameterCreationAPI_v5)];
|
||||||
if (!api) return PGFail(error,@"无法取得 FxPlug 参数创建接口。");
|
if (!api) return PGFail(error,@"无法取得 FxPlug 参数创建接口。");
|
||||||
__block BOOL ok=YES;
|
__block BOOL ok=YES;
|
||||||
|
ok=[api addCustomParameterWithName:@"" parameterID:PGInspectorPanel
|
||||||
|
defaultValue:[PGInspectorState stateWithValues:@{} disabledParameters:[NSSet set] hasEdits:NO]
|
||||||
|
parameterFlags:kFxParameterFlag_CUSTOM_UI|kFxParameterFlag_USE_FULL_VIEW_WIDTH]&&ok;
|
||||||
void (^slider)(UInt32)=^(UInt32 identifier){ok=[self addSlider:identifier api:api]&&ok;};
|
void (^slider)(UInt32)=^(UInt32 identifier){ok=[self addSlider:identifier api:api]&&ok;};
|
||||||
void (^toggle)(NSString *,UInt32,BOOL)=^(NSString *name,UInt32 identifier,BOOL value){ok=[api addToggleButtonWithName:name parameterID:identifier defaultValue:value parameterFlags:kFxParameterFlag_DEFAULT]&&ok;};
|
void (^toggle)(NSString *,UInt32,BOOL)=^(NSString *name,UInt32 identifier,BOOL value){ok=[api addToggleButtonWithName:name parameterID:identifier defaultValue:value parameterFlags:kFxParameterFlag_HIDDEN]&&ok;};
|
||||||
void (^group)(NSString *,UInt32)=^(NSString *name,UInt32 identifier){ok=[api startParameterSubGroup:name parameterID:identifier parameterFlags:kFxParameterFlag_DEFAULT]&&ok;};
|
void (^group)(NSString *,UInt32)=^(NSString *name,UInt32 identifier){ok=[api startParameterSubGroup:name parameterID:identifier parameterFlags:kFxParameterFlag_HIDDEN]&&ok;};
|
||||||
void (^end)(void)=^{ok=[api endParameterSubGroup]&&ok;};
|
void (^end)(void)=^{ok=[api endParameterSubGroup]&&ok;};
|
||||||
group(@"磨皮",100); toggle(@"启用磨皮",PGSmoothingEnabled,YES);
|
group(@"磨皮",100); toggle(@"启用磨皮",PGSmoothingEnabled,YES);
|
||||||
slider(PGAmount); slider(PGMidtone); slider(PGRadius); toggle(@"保护五官",PGFeatureProtect,YES); end();
|
slider(PGAmount); slider(PGMidtone); slider(PGRadius); toggle(@"保护五官",PGFeatureProtect,YES); end();
|
||||||
group(@"肤色",101); toggle(@"启用肤色",PGColorEnabled,YES);
|
group(@"肤色",101); toggle(@"启用肤色",PGColorEnabled,YES);
|
||||||
ok=[api addPopupMenuWithName:@"肤色风格" parameterID:PGTonePreset defaultValue:0 menuEntries:@[@"原色",@"自然白",@"冷白",@"暖白",@"红润白",@"小麦",@"古铜"] parameterFlags:0]&&ok;
|
ok=[api addPopupMenuWithName:@"肤色风格" parameterID:PGTonePreset defaultValue:0 menuEntries:@[@"原色",@"自然白",@"冷白",@"暖白",@"红润白",@"小麦",@"古铜"] parameterFlags:kFxParameterFlag_HIDDEN]&&ok;
|
||||||
slider(PGBrightness); slider(PGHue); slider(PGSaturation); slider(PGEvenTone); slider(PGTeeth); slider(PGEyeBright); end();
|
slider(PGBrightness); slider(PGHue); slider(PGSaturation); slider(PGEvenTone); slider(PGTeeth); slider(PGEyeBright); end();
|
||||||
group(@"细节",102); toggle(@"启用细节",PGDetailEnabled,YES); slider(PGHighPass); slider(PGSharpness); end();
|
group(@"细节",102); toggle(@"启用细节",PGDetailEnabled,YES); slider(PGHighPass); slider(PGSharpness); end();
|
||||||
group(@"脸型",103); toggle(@"启用脸型",PGShapeEnabled,YES);
|
group(@"脸型",103); toggle(@"启用脸型",PGShapeEnabled,YES);
|
||||||
UInt32 shape[]={PGThin,PGBigEye,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange};
|
UInt32 shape[]={PGThin,PGBigEye,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange};
|
||||||
for(NSUInteger i=0;i<sizeof(shape)/sizeof(UInt32);i++) slider(shape[i]); end();
|
for(NSUInteger i=0;i<sizeof(shape)/sizeof(UInt32);i++) slider(shape[i]); end();
|
||||||
group(@"人脸选择与跟踪",104); toggle(@"只处理选中人脸",PGSingleFace,NO);
|
group(@"人脸选择与跟踪",104); toggle(@"只处理选中人脸",PGSingleFace,NO);
|
||||||
ok=[api addPointParameterWithName:@"选脸位置" parameterID:PGSelection defaultX:.5 defaultY:.5 parameterFlags:0]&&ok;
|
ok=[api addPointParameterWithName:@"选脸位置" parameterID:PGSelection defaultX:.5 defaultY:.5 parameterFlags:kFxParameterFlag_HIDDEN]&&ok;
|
||||||
toggle(@"多人高速模式",PGFastMulti,NO); slider(PGJumpGuard);
|
toggle(@"多人高速模式",PGFastMulti,NO); slider(PGJumpGuard);
|
||||||
toggle(@"使用跟踪数据",PGTrackEnabled,NO);
|
toggle(@"使用跟踪数据",PGTrackEnabled,NO);
|
||||||
ok=[api addPopupMenuWithName:@"跟踪目标" parameterID:PGTarget defaultValue:0 menuEntries:@[@"目标 1",@"目标 2",@"目标 3"] parameterFlags:kFxParameterFlag_NOT_ANIMATABLE]&&ok;
|
ok=[api addPopupMenuWithName:@"跟踪目标" parameterID:PGTarget defaultValue:0 menuEntries:@[@"目标 1",@"目标 2",@"目标 3"] parameterFlags:kFxParameterFlag_NOT_ANIMATABLE|kFxParameterFlag_HIDDEN]&&ok;
|
||||||
ok=[api addPushButtonWithName:@"向前分析" parameterID:PGAnalyzeForward selector:@selector(analyzeForward) parameterFlags:0]&&ok;
|
ok=[api addPushButtonWithName:@"向前分析" parameterID:PGAnalyzeForward selector:@selector(analyzeForward) parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED]&&ok;
|
||||||
ok=[api addPushButtonWithName:@"向后分析" parameterID:PGAnalyzeBackward selector:@selector(analyzeBackward) parameterFlags:0]&&ok;
|
ok=[api addPushButtonWithName:@"向后分析" parameterID:PGAnalyzeBackward selector:@selector(analyzeBackward) parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED]&&ok;
|
||||||
ok=[api addPushButtonWithName:@"清除当前目标" parameterID:PGClearTrack selector:@selector(clearTracking) parameterFlags:0]&&ok;
|
ok=[api addPushButtonWithName:@"清除当前目标" parameterID:PGClearTrack selector:@selector(clearTracking) parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED]&&ok;
|
||||||
ok=[api addStringParameterWithName:@"跟踪状态" parameterID:PGStatus defaultValue:@"选择画面中的人脸后开始分析" parameterFlags:kFxParameterFlag_NOT_ANIMATABLE|kFxParameterFlag_DISABLED]&&ok;
|
ok=[api addStringParameterWithName:@"跟踪状态" parameterID:PGStatus defaultValue:@"自动人脸跟随" parameterFlags:kFxParameterFlag_NOT_ANIMATABLE|kFxParameterFlag_DISABLED|kFxParameterFlag_HIDDEN]&&ok;
|
||||||
toggle(@"显示人脸框",PGFaceBoxes,NO); toggle(@"显示跟踪框",PGTrackBoxes,NO); end();
|
toggle(@"显示人脸框",PGFaceBoxes,NO); toggle(@"显示跟踪框",PGTrackBoxes,NO); end();
|
||||||
group(@"蒙版与输出",105); slider(PGThreshold); slider(PGSoftness); slider(PGEdgeProtect);
|
group(@"蒙版与输出",105); slider(PGThreshold); slider(PGSoftness); slider(PGEdgeProtect);
|
||||||
toggle(@"预览蒙版",PGShowMask,NO); toggle(@"反转蒙版",PGInvertMask,NO);
|
toggle(@"预览蒙版",PGShowMask,NO); toggle(@"反转蒙版",PGInvertMask,NO);
|
||||||
ok=[api addPopupMenuWithName:@"蒙版通道" parameterID:PGDebugChannel defaultValue:0 menuEntries:@[@"皮肤",@"牙齿",@"眼睛"] parameterFlags:0]&&ok;
|
ok=[api addPopupMenuWithName:@"蒙版通道" parameterID:PGDebugChannel defaultValue:0 menuEntries:@[@"皮肤",@"牙齿",@"眼睛"] parameterFlags:kFxParameterFlag_HIDDEN]&&ok;
|
||||||
slider(PGMix); end();
|
slider(PGMix); end();
|
||||||
for(UInt32 identifier=PGTrackData1;identifier<=PGTrackData3;identifier++)
|
for(UInt32 identifier=PGTrackData1;identifier<=PGTrackData3;identifier++)
|
||||||
ok=[api addStringParameterWithName:@"分析数据" parameterID:identifier defaultValue:@"" parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_NOT_ANIMATABLE]&&ok;
|
ok=[api addStringParameterWithName:@"分析数据" parameterID:identifier defaultValue:@"" parameterFlags:kFxParameterFlag_HIDDEN|kFxParameterFlag_NOT_ANIMATABLE]&&ok;
|
||||||
return ok ? YES : PGFail(error,@"宿主未能创建完整的人像美化参数。");
|
return ok ? YES : PGFail(error,@"宿主未能创建完整的人像美化参数。");
|
||||||
}
|
}
|
||||||
|
- (NSView *)createViewForParameterID:(UInt32)identifier {
|
||||||
|
return identifier==PGInspectorPanel ? [[PGInspectorView alloc] initWithAPIManager:_apiManager] : nil;
|
||||||
|
}
|
||||||
|
- (Class)classForCustomParameterID:(UInt32)identifier {return identifier==PGInspectorPanel?PGInspectorState.class:Nil;}
|
||||||
|
- (NSSet<Class> *)classesForCustomParameterID:(UInt32)identifier {
|
||||||
|
return identifier==PGInspectorPanel?[NSSet setWithObjects:PGInspectorState.class,NSDictionary.class,NSNumber.class,NSSet.class,nil]:[NSSet set];
|
||||||
|
}
|
||||||
- (PGBeautySettings)settingsAtTime:(CMTime)time {
|
- (PGBeautySettings)settingsAtTime:(CMTime)time {
|
||||||
PGBeautySettings s=PGBeautyDefaults();
|
PGBeautySettings s=PGBeautyDefaults();
|
||||||
id<FxParameterRetrievalAPI_v6> api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
id<FxParameterRetrievalAPI_v6> api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||||
@@ -192,6 +203,13 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
|||||||
enabled=YES; [api getBoolValue:&enabled fromParameter:PGShapeEnabled atTime:time];
|
enabled=YES; [api getBoolValue:&enabled fromParameter:PGShapeEnabled atTime:time];
|
||||||
if(!enabled) s.faceThin=s.faceBigEye=s.faceShort=s.faceNarrow=s.faceChin=s.faceV=s.faceJaw=s.faceCheekbone=s.faceSmallHead=0;
|
if(!enabled) s.faceThin=s.faceBigEye=s.faceShort=s.faceNarrow=s.faceChin=s.faceV=s.faceJaw=s.faceCheekbone=s.faceSmallHead=0;
|
||||||
if(s.fastMultiFace) s.singleFace=0;
|
if(s.fastMultiFace) s.singleFace=0;
|
||||||
|
NSObject *panel=nil;
|
||||||
|
[api getCustomParameterValue:&panel fromParameter:PGInspectorPanel atTime:time];
|
||||||
|
if([panel isKindOfClass:PGInspectorState.class])s=PGSettingsWithInspectorState(s,(PGInspectorState *)panel);
|
||||||
|
// The current product follows faces automatically. Old manual-analysis and
|
||||||
|
// debug values remain saved for document compatibility, but cannot silently
|
||||||
|
// override live geometry or leave an invisible mask/selection mode active.
|
||||||
|
s.trackingEnabled=0;s.singleFace=0;s.showMask=0;s.invertMask=0;s.debugChannel=0;s.showFaceBoxes=0;
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
- (NSArray<NSString *> *)trackStrings {
|
- (NSArray<NSString *> *)trackStrings {
|
||||||
@@ -207,7 +225,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
|||||||
if(!state) return PGFail(error,@"缺少渲染状态输出位置。");
|
if(!state) return PGFail(error,@"缺少渲染状态输出位置。");
|
||||||
if(![_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]) return PGFail(error,@"无法读取宿主参数。");
|
if(![_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]) return PGFail(error,@"无法读取宿主参数。");
|
||||||
PGBeautySettings s=[self settingsAtTime:time];
|
PGBeautySettings s=[self settingsAtTime:time];
|
||||||
*state=[NSPropertyListSerialization dataWithPropertyList:@{@"settings":[NSData dataWithBytes:&s length:sizeof(s)],@"tracks":[self trackStrings]} format:NSPropertyListBinaryFormat_v1_0 options:0 error:error];
|
*state=[NSPropertyListSerialization dataWithPropertyList:@{@"settings":[NSData dataWithBytes:&s length:sizeof(s)],@"tracks":@[@"",@"",@""]} format:NSPropertyListBinaryFormat_v1_0 options:0 error:error];
|
||||||
return *state!=nil;
|
return *state!=nil;
|
||||||
}
|
}
|
||||||
- (BOOL)destinationImageRect:(FxRect *)rect sourceImages:(NSArray<FxImageTile *> *)sources destinationImage:(FxImageTile *)destination pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error {
|
- (BOOL)destinationImageRect:(FxRect *)rect sourceImages:(NSArray<FxImageTile *> *)sources destinationImage:(FxImageTile *)destination pluginState:(NSData *)state atTime:(CMTime)time error:(NSError **)error {
|
||||||
@@ -237,6 +255,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
|||||||
if(![plist isKindOfClass:NSDictionary.class]||![plist[@"settings"] isKindOfClass:NSData.class]||[plist[@"settings"] length]!=sizeof(PGBeautySettings)) return PGFail(error,@"渲染状态版本无效。");
|
if(![plist isKindOfClass:NSDictionary.class]||![plist[@"settings"] isKindOfClass:NSData.class]||[plist[@"settings"] length]!=sizeof(PGBeautySettings)) return PGFail(error,@"渲染状态版本无效。");
|
||||||
PGBeautySettings s;[plist[@"settings"] getBytes:&s length:sizeof(s)];
|
PGBeautySettings s;[plist[@"settings"] getBytes:&s length:sizeof(s)];
|
||||||
if(s.version!=1) return PGFail(error,@"此版本不支持保存的渲染状态。");
|
if(s.version!=1) return PGFail(error,@"此版本不支持保存的渲染状态。");
|
||||||
|
s.trackingEnabled=0;s.singleFace=0;
|
||||||
CIImage *input=PGImageFromTile(sources.firstObject); if(!input) return PGFail(error,@"无法读取 IOSurface。");
|
CIImage *input=PGImageFromTile(sources.firstObject); if(!input) return PGFail(error,@"无法读取 IOSurface。");
|
||||||
@synchronized(self) {
|
@synchronized(self) {
|
||||||
if(!_reportedRender) {
|
if(!_reportedRender) {
|
||||||
@@ -255,21 +274,6 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
|||||||
NSArray *tracks=plist[@"tracks"];
|
NSArray *tracks=plist[@"tracks"];
|
||||||
if(![tracks isKindOfClass:NSArray.class]||tracks.count!=3) return PGFail(error,@"跟踪状态无效。");
|
if(![tracks isKindOfClass:NSArray.class]||tracks.count!=3) return PGFail(error,@"跟踪状态无效。");
|
||||||
for(id track in tracks) if(![track isKindOfClass:NSString.class]) return PGFail(error,@"跟踪记录无效。");
|
for(id track in tracks) if(![track isKindOfClass:NSString.class]) return PGFail(error,@"跟踪记录无效。");
|
||||||
if(![_loadedTracks isEqualToArray:tracks]) {
|
|
||||||
_compatibleTrackMask=0;
|
|
||||||
for(NSUInteger i=0;i<3;i++) {
|
|
||||||
NSData *data=[[NSData alloc] initWithBase64EncodedString:tracks[i] options:0];
|
|
||||||
BOOL compatible=PGCompatibleHostTrack(data);
|
|
||||||
if(compatible) _compatibleTrackMask |= 1u<<i;
|
|
||||||
[_faceProcessor setTrackingData:compatible?data:nil forTarget:i];
|
|
||||||
}
|
|
||||||
_loadedTracks=[tracks copy];
|
|
||||||
}
|
|
||||||
// Previous releases did not record which image-axis contract their
|
|
||||||
// analysis used. Never replay potentially inverted coordinates.
|
|
||||||
// Preserve the user's saved data and use realtime follow until they
|
|
||||||
// reanalyze that target under this coordinate contract.
|
|
||||||
if(s.trackingEnabled && !(_compatibleTrackMask & (1u<<MIN(s.targetIndex,2)))) s.trackingEnabled=0;
|
|
||||||
NSError *stageError=nil;
|
NSError *stageError=nil;
|
||||||
PGFaceFrame *frame=[_faceProcessor processImage:input settings:s time:time error:&stageError];
|
PGFaceFrame *frame=[_faceProcessor processImage:input settings:s time:time error:&stageError];
|
||||||
if(!frame) { os_log_error(PGRenderLog(), "face processing failed: %{public}@",stageError); if(error)*error=stageError; return NO; }
|
if(!frame) { os_log_error(PGRenderLog(), "face processing failed: %{public}@",stageError); if(error)*error=stageError; return NO; }
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.6</string>
|
<string>1.1.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>8</string>
|
<string>9</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Encode/audit our own FxPlug custom value in observed Motion XML structure.
|
||||||
|
|
||||||
|
No host is started. A successful audit verifies structure and archive contents;
|
||||||
|
it does not establish Motion/FCP custom-view loading or native interaction.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
import plistlib
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
ALPHABET = "*-0123456789" + string.ascii_uppercase + string.ascii_lowercase
|
||||||
|
STANDARD = string.ascii_uppercase + string.ascii_lowercase + string.digits + "+/"
|
||||||
|
PLUGIN_UUID = "FB14B0E6-59B2-4836-916F-CD0C559E9241"
|
||||||
|
PLACEHOLDER_SHA256 = "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e"
|
||||||
|
|
||||||
|
|
||||||
|
def require(condition: bool, message: str) -> None:
|
||||||
|
if not condition:
|
||||||
|
raise ValueError(message)
|
||||||
|
|
||||||
|
|
||||||
|
def digest(data: bytes) -> str:
|
||||||
|
return hashlib.sha256(data).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve(objects: list, value, seen: frozenset[int] = frozenset()):
|
||||||
|
"""Read Foundation collection values without instantiating archived classes."""
|
||||||
|
if isinstance(value, plistlib.UID):
|
||||||
|
index = value.data
|
||||||
|
require(0 <= index < len(objects) and index not in seen, "Invalid/cyclic archive UID")
|
||||||
|
return resolve(objects, objects[index], seen | {index})
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [resolve(objects, item, seen) for item in value]
|
||||||
|
if isinstance(value, dict):
|
||||||
|
if "NS.keys" in value:
|
||||||
|
keys, items = value["NS.keys"], value["NS.objects"]
|
||||||
|
require(len(keys) == len(items), "Mismatched dictionary keys/objects")
|
||||||
|
return {resolve(objects, k, seen): resolve(objects, v, seen) for k, v in zip(keys, items)}
|
||||||
|
if "NS.objects" in value:
|
||||||
|
return resolve(objects, value["NS.objects"], seen)
|
||||||
|
return {k: resolve(objects, v, seen) for k, v in value.items() if k != "$class"}
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_inner(inner: bytes) -> dict:
|
||||||
|
archive = plistlib.loads(inner)
|
||||||
|
require(archive.get("$archiver") == "NSKeyedArchiver", "Inner value is not a keyed archive")
|
||||||
|
require(set(archive.get("$top", {})) == {"Custom Data"}, "Inner archive requires the Custom Data key")
|
||||||
|
objects = archive["$objects"]
|
||||||
|
state = objects[archive["$top"]["Custom Data"].data]
|
||||||
|
require(objects[state["$class"].data]["$classname"] == "PGInspectorState", "Unexpected custom class")
|
||||||
|
classes = sorted({x["$classname"] for x in objects if isinstance(x, dict) and "$classname" in x})
|
||||||
|
require(set(classes) <= {"PGInspectorState", "NSObject", "NSDictionary", "NSMutableDictionary", "NSSet", "NSMutableSet", "NSNumber"}, "Unexpected archived class")
|
||||||
|
decoded = resolve(objects, state)
|
||||||
|
require(decoded.get("schema") == 1 and isinstance(decoded.get("edited"), bool), "Unexpected state schema")
|
||||||
|
values = decoded.get("values")
|
||||||
|
require(isinstance(values, dict) and values, "Missing numeric control values")
|
||||||
|
require(all(isinstance(k, int) and not isinstance(k, bool) and isinstance(v, (int, float)) and math.isfinite(v) for k, v in values.items()), "Non-numeric control values")
|
||||||
|
disabled = decoded.get("disabled")
|
||||||
|
require(isinstance(disabled, list) and all(isinstance(v, int) for v in disabled), "Invalid disabled controls")
|
||||||
|
return {"class": "PGInspectorState", "schema": decoded["schema"], "hasEdits": decoded["edited"],
|
||||||
|
"values": values, "disabled": disabled, "classes": classes,
|
||||||
|
"innerBytes": len(inner), "innerSHA256": digest(inner)}
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_inner(inner: bytes) -> bytes:
|
||||||
|
inspect_inner(inner)
|
||||||
|
# This is the observed NSKeyedArchiver wrapper, containing NSData and
|
||||||
|
# NSNumber(false), rather than a vendor custom object or copied blob.
|
||||||
|
return plistlib.dumps({"$version": 100000, "$archiver": "NSKeyedArchiver",
|
||||||
|
"$top": {"DataIsLegacy": plistlib.UID(2), "BlindDataObject": plistlib.UID(1)},
|
||||||
|
"$objects": ["$null", inner, False]}, fmt=plistlib.FMT_BINARY, sort_keys=False)
|
||||||
|
|
||||||
|
|
||||||
|
def encode_outer(outer: bytes) -> str:
|
||||||
|
data = struct.pack(">Q", len(outer)) + b"*" + outer
|
||||||
|
data += b"\0" * (-len(data) % 3)
|
||||||
|
return base64.b64encode(data).decode("ascii").translate(str.maketrans(STANDARD, ALPHABET))
|
||||||
|
|
||||||
|
|
||||||
|
def decode_text(text: str) -> tuple[bytes, bytes, dict]:
|
||||||
|
require(text and len(text) % 4 == 0 and set(text) <= set(ALPHABET), "Invalid Motion text encoding")
|
||||||
|
data = base64.b64decode(text.translate(str.maketrans(ALPHABET, STANDARD)), validate=True)
|
||||||
|
require(len(data) >= 9 and data[8] == 0x2A, "Unexpected Motion envelope")
|
||||||
|
length = int.from_bytes(data[:8], "big")
|
||||||
|
require(0 < length <= 16 * 1024 * 1024 and 0 <= len(data) - 9 - length <= 2, "Invalid payload length")
|
||||||
|
require(not any(data[9+length:]), "Nonzero envelope padding")
|
||||||
|
outer = data[9:9+length]
|
||||||
|
archive = plistlib.loads(outer)
|
||||||
|
require(archive.get("$archiver") == "NSKeyedArchiver", "Unexpected outer archive")
|
||||||
|
require(set(archive.get("$top", {})) == {"DataIsLegacy", "BlindDataObject"}, "Unexpected outer keys")
|
||||||
|
objects = archive["$objects"]
|
||||||
|
legacy = resolve(objects, archive["$top"]["DataIsLegacy"])
|
||||||
|
inner = resolve(objects, archive["$top"]["BlindDataObject"])
|
||||||
|
require(legacy is False and isinstance(inner, bytes), "Expected a nonlegacy NSData custom value")
|
||||||
|
require(encode_outer(outer) == text, "Text roundtrip mismatch")
|
||||||
|
state = inspect_inner(inner)
|
||||||
|
state.update({"outerBytes": len(outer), "outerSHA256": digest(outer), "encodedCharacters": len(text), "exactTextRoundtrip": True})
|
||||||
|
return outer, inner, state
|
||||||
|
|
||||||
|
|
||||||
|
def find_plugin(root: ET.Element) -> ET.Element:
|
||||||
|
matches = [x for x in root.iter("filter") if x.get("pluginUUID") == PLUGIN_UUID]
|
||||||
|
require(len(matches) == 1, "Expected exactly one product filter")
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
|
||||||
|
def parameter(node: ET.Element, channel: str) -> ET.Element:
|
||||||
|
require(channel.startswith("./"), "Expected a relative parameter path")
|
||||||
|
for identifier in channel[2:].split("/"):
|
||||||
|
node = node.find(f"parameter[@id='{identifier}']")
|
||||||
|
require(node is not None, f"Unresolved parameter path {channel}")
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def audit(path: Path, parameters_path: Path | None = None) -> tuple[dict, bytes, bytes]:
|
||||||
|
root = ET.parse(path).getroot()
|
||||||
|
plugin = find_plugin(root)
|
||||||
|
custom = parameter(plugin, "./210")
|
||||||
|
require(custom.get("name") == "" and custom.get("flags") == "12901679104", "Custom entry differs from observed native structure")
|
||||||
|
require(custom.findtext("numberOfKeypoints") == "1" and custom.findtext("keypoint/time") == "0 0 0 0" and custom.find("keypoint/value") is not None, "Missing native custom keypoint skeleton")
|
||||||
|
text = custom.findtext("dataValue")
|
||||||
|
require(text == custom.findtext("defaultVal"), "Initial current/default custom values differ")
|
||||||
|
outer, inner, state = decode_text(text)
|
||||||
|
require(state["hasEdits"] is False, "Initial custom value must preserve legacy native settings")
|
||||||
|
entities = [x for x in root.iter() if x.tag in {"scenenode", "layer", "group", "filter", "clip", "footage"}]
|
||||||
|
ids = [x.get("id") for x in entities]
|
||||||
|
require(None not in ids and len(ids) == len(set(ids)), "Duplicate/missing entity identity")
|
||||||
|
by_id = dict(zip(ids, entities))
|
||||||
|
targets = root.findall("./scene/publishSettings/target")
|
||||||
|
require(len(targets) == 3, "Expected exactly three publications")
|
||||||
|
require([x.get("name") for x in targets] == ["FCP 版本作者→", "更多模板浏览器访问→", ""], "Unexpected publication order/names")
|
||||||
|
require(targets[2].get("object") == plugin.get("id") and targets[2].get("channel") == "./210", "Custom publication does not target this filter")
|
||||||
|
for item in targets:
|
||||||
|
require(item.get("object") in by_id, "Unresolved publication object")
|
||||||
|
parameter(by_id[item.get("object")], item.get("channel"))
|
||||||
|
for target, title in zip(targets[:2], ["大鱼先森Media", "dayu.media"]):
|
||||||
|
entry = parameter(by_id[target.get("object")], "./2/100")
|
||||||
|
require(len(entry.findall("entry")) == 1 and title in ET.tostring(entry, encoding="unicode"), "Copyright menu differs")
|
||||||
|
require(parameter(plugin, "./100/12").get("value") == "0.5", "Template midtone default was lost")
|
||||||
|
require(parameter(plugin, "./10005").get("value") == "0", "New template OSC must be off")
|
||||||
|
for group in range(100, 106):
|
||||||
|
parameter(plugin, f"./{group}")
|
||||||
|
for identifier in (45, 46, 50, 51, 52, 53):
|
||||||
|
parameter(plugin, f"./104/{identifier}")
|
||||||
|
for identifier in (60, 61, 62):
|
||||||
|
parameter(plugin, f"./{identifier}")
|
||||||
|
metadata = parameters_path or path.with_name("PARAMETERS.json")
|
||||||
|
listed = json.loads(metadata.read_text())
|
||||||
|
for item in listed["parameters"]:
|
||||||
|
parameter(plugin, item["channel"])
|
||||||
|
require(bool(item["published"]) == (item["id"] == 210), "Legacy flat entry is still published")
|
||||||
|
clips = root.findall(".//footage/clip")
|
||||||
|
require(len(clips) == 1 and clips[0].findtext("pathURL") == "Drop Zone.tiff", "Unexpected effect source media")
|
||||||
|
require(clips[0].get("id") == "510000006" and by_id["510000007"].find("clip") is clips[0], "Broken source clip/footage hierarchy")
|
||||||
|
source = by_id["510000004"]
|
||||||
|
require(parameter(source, "./1/324/300").get("value") == "510000006", "Broken Source Media reference")
|
||||||
|
require(parameter(source, "./2/321").get("value") == "3", "Effect Source type changed")
|
||||||
|
require(parameter(source, "./2/311").get("value") == "1", "Effect Source Drop Zone changed")
|
||||||
|
media = path.parent / "Drop Zone.tiff"
|
||||||
|
require(media.is_file() and digest(media.read_bytes()) == PLACEHOLDER_SHA256, "Placeholder missing/changed")
|
||||||
|
result = {"status": "structure_verified_native_loading_pending", "templateSHA256": digest(path.read_bytes()),
|
||||||
|
"pluginVersion": plugin.get("pluginVersion"), "publications": [dict(x.attrib) for x in targets],
|
||||||
|
"parameterPathsResolved": len(listed["parameters"]), "sourceMediaClosed": True,
|
||||||
|
"mediaSHA256": PLACEHOLDER_SHA256, "customState": state,
|
||||||
|
"nativeLoading": "pending user; no host executed"}
|
||||||
|
return result, outer, inner
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
enc = sub.add_parser("encode", help="Wrap our own secure Custom Data archive and encode Motion text")
|
||||||
|
enc.add_argument("--inner", type=Path, required=True)
|
||||||
|
enc.add_argument("--text-output", type=Path, required=True)
|
||||||
|
enc.add_argument("--outer-output", type=Path)
|
||||||
|
chk = sub.add_parser("audit", help="Read-only template/archive/reference checks; native loading stays pending")
|
||||||
|
chk.add_argument("template", type=Path)
|
||||||
|
chk.add_argument("--parameters", type=Path)
|
||||||
|
chk.add_argument("--json-output", type=Path)
|
||||||
|
chk.add_argument("--inner-output", type=Path)
|
||||||
|
chk.add_argument("--outer-output", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.command == "encode":
|
||||||
|
inner = args.inner.read_bytes(); outer = wrap_inner(inner); text = encode_outer(outer)
|
||||||
|
decoded_outer, decoded_inner, result = decode_text(text)
|
||||||
|
require(decoded_inner == inner and decoded_outer == outer, "Own archive roundtrip failed")
|
||||||
|
args.text_output.write_text(text + "\n")
|
||||||
|
if args.outer_output:
|
||||||
|
args.outer_output.write_bytes(outer)
|
||||||
|
else:
|
||||||
|
result, outer, inner = audit(args.template, args.parameters)
|
||||||
|
if args.json_output:
|
||||||
|
args.json_output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
||||||
|
if args.inner_output:
|
||||||
|
args.inner_output.write_bytes(inner)
|
||||||
|
if args.outer_output:
|
||||||
|
args.outer_output.write_bytes(outer)
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except (ValueError, KeyError, IndexError, TypeError, OSError, ET.ParseError, plistlib.InvalidFileException) as error:
|
||||||
|
print(f"FAIL: {error}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
+2
-1
@@ -15,8 +15,9 @@ xcrun clang "${compiler_extra[@]}" -fobjc-arc -fmodules -Wall -Wextra -Wno-unuse
|
|||||||
-I "$plugin_dir" -F "$sdk_frameworks" -F "$runtime_frameworks" \
|
-I "$plugin_dir" -F "$sdk_frameworks" -F "$runtime_frameworks" \
|
||||||
-Wl,-rpath,"$runtime_frameworks" \
|
-Wl,-rpath,"$runtime_frameworks" \
|
||||||
"$plugin_dir/PGBeautyRenderer.m" "$plugin_dir/PGFaceProcessor.m" \
|
"$plugin_dir/PGBeautyRenderer.m" "$plugin_dir/PGFaceProcessor.m" \
|
||||||
|
"$plugin_dir/PGInspectorState.m" "$plugin_dir/PGInspectorView.m" \
|
||||||
"$plugin_dir/beatygrilPlugIn.m" tests/host_adapter_tests.m \
|
"$plugin_dir/beatygrilPlugIn.m" tests/host_adapter_tests.m \
|
||||||
-framework Foundation -framework CoreImage -framework CoreGraphics \
|
-framework Foundation -framework AppKit -framework CoreImage -framework CoreGraphics \
|
||||||
-framework CoreMedia -framework CoreVideo -framework IOSurface -framework Metal \
|
-framework CoreMedia -framework CoreVideo -framework IOSurface -framework Metal \
|
||||||
-framework Vision -framework ImageIO -framework FxPlug -framework PluginManager \
|
-framework Vision -framework ImageIO -framework FxPlug -framework PluginManager \
|
||||||
-o "$output_dir/host-tests"
|
-o "$output_dir/host-tests"
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/zsh
|
||||||
|
set -euo pipefail
|
||||||
|
cd "${0:A:h:h}"
|
||||||
|
output_dir=$(mktemp -d /tmp/paintfcp-inspector-tests.XXXXXX)
|
||||||
|
trap 'rm -rf "$output_dir"' EXIT
|
||||||
|
plugin_dir="$PWD/beatygril/beatygril/Plugin"
|
||||||
|
compiler_extra=()
|
||||||
|
if [[ "${PAINTFCP_INSPECTOR_ASAN:-0}" == 1 ]]; then
|
||||||
|
compiler_extra=(-fsanitize=address -g)
|
||||||
|
fi
|
||||||
|
xcrun clang "${compiler_extra[@]}" -fobjc-arc -fmodules -Wall -Wextra -Wno-unused-parameter \
|
||||||
|
-DCI_SILENCE_GL_DEPRECATION -mmacosx-version-min=13.0 -I "$plugin_dir" \
|
||||||
|
-F /Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks -F /Library/Developer/Frameworks \
|
||||||
|
-Wl,-rpath,/Library/Developer/Frameworks \
|
||||||
|
"$plugin_dir/PGBeautyRenderer.m" "$plugin_dir/PGFaceProcessor.m" \
|
||||||
|
"$plugin_dir/PGInspectorState.m" "$plugin_dir/PGInspectorView.m" \
|
||||||
|
"$plugin_dir/beatygrilPlugIn.m" tests/inspector_tests.m \
|
||||||
|
-framework Foundation -framework AppKit -framework CoreImage -framework CoreGraphics \
|
||||||
|
-framework CoreMedia -framework CoreVideo -framework IOSurface -framework Metal \
|
||||||
|
-framework Vision -framework ImageIO -framework FxPlug -framework PluginManager \
|
||||||
|
-o "$output_dir/inspector-tests"
|
||||||
|
"$output_dir/inspector-tests"
|
||||||
+22
-4
@@ -9,6 +9,7 @@
|
|||||||
#import "PGParameterIDs.h"
|
#import "PGParameterIDs.h"
|
||||||
#import "PGFaceProcessor.h"
|
#import "PGFaceProcessor.h"
|
||||||
#import "PGBeautyRenderer.h"
|
#import "PGBeautyRenderer.h"
|
||||||
|
#import "PGInspectorState.h"
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
// This adapter simulates host callbacks, but uses real IOSurface buffers and the
|
// This adapter simulates host callbacks, but uses real IOSurface buffers and the
|
||||||
@@ -28,6 +29,9 @@
|
|||||||
@property BOOL rejectAnalysis;
|
@property BOOL rejectAnalysis;
|
||||||
@property BOOL targetAtCurrentTime;
|
@property BOOL targetAtCurrentTime;
|
||||||
@property UInt32 rejectedWriteParameter;
|
@property UInt32 rejectedWriteParameter;
|
||||||
|
@property NSUInteger actionDepth;
|
||||||
|
@property NSUInteger actionStarts;
|
||||||
|
@property NSUInteger actionEnds;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation PGHostAPI
|
@implementation PGHostAPI
|
||||||
@@ -73,6 +77,9 @@
|
|||||||
- (BOOL)addStringParameterWithName:(NSString *)name parameterID:(UInt32)identifier defaultValue:(NSString *)value parameterFlags:(FxParameterFlags)flags {
|
- (BOOL)addStringParameterWithName:(NSString *)name parameterID:(UInt32)identifier defaultValue:(NSString *)value parameterFlags:(FxParameterFlags)flags {
|
||||||
return [self define:identifier kind:@"string" value:value flags:flags extra:@{@"name":name}];
|
return [self define:identifier kind:@"string" value:value flags:flags extra:@{@"name":name}];
|
||||||
}
|
}
|
||||||
|
- (BOOL)addCustomParameterWithName:(NSString *)name parameterID:(UInt32)identifier defaultValue:(NSObject<NSSecureCoding,NSCopying> *)value parameterFlags:(FxParameterFlags)flags {
|
||||||
|
return [self define:identifier kind:@"custom" value:[value copy] flags:flags extra:@{@"name":name}];
|
||||||
|
}
|
||||||
- (BOOL)startParameterSubGroup:(NSString *)name parameterID:(UInt32)identifier parameterFlags:(FxParameterFlags)flags {
|
- (BOOL)startParameterSubGroup:(NSString *)name parameterID:(UInt32)identifier parameterFlags:(FxParameterFlags)flags {
|
||||||
_depth++;
|
_depth++;
|
||||||
return [self define:identifier kind:@"group" value:nil flags:flags extra:@{@"name":name}];
|
return [self define:identifier kind:@"group" value:nil flags:flags extra:@{@"name":name}];
|
||||||
@@ -104,6 +111,17 @@
|
|||||||
_values[@(identifier)] = value; return YES;
|
_values[@(identifier)] = value; return YES;
|
||||||
}
|
}
|
||||||
- (BOOL)setBoolValue:(BOOL)value toParameter:(UInt32)identifier atTime:(CMTime)time { _lastSetBoolTime = time; _values[@(identifier)] = @(value); return YES; }
|
- (BOOL)setBoolValue:(BOOL)value toParameter:(UInt32)identifier atTime:(CMTime)time { _lastSetBoolTime = time; _values[@(identifier)] = @(value); return YES; }
|
||||||
|
- (BOOL)getCustomParameterValue:(NSObject **)value fromParameter:(UInt32)identifier atTime:(CMTime)time {
|
||||||
|
NSObject *object=_values[@(identifier)];if(![object isKindOfClass:PGInspectorState.class])return NO;
|
||||||
|
*value=[(PGInspectorState *)object copy];return YES;
|
||||||
|
}
|
||||||
|
- (BOOL)setCustomParameterValue:(NSObject<NSSecureCoding,NSCopying> *)value toParameter:(UInt32)identifier atTime:(CMTime)time {
|
||||||
|
if(identifier==_rejectedWriteParameter)return NO;_values[@(identifier)]=[value copy];return YES;
|
||||||
|
}
|
||||||
|
- (BOOL)setFloatValue:(double)value toParameter:(UInt32)identifier atTime:(CMTime)time {_values[@(identifier)]=@(value);return YES;}
|
||||||
|
- (BOOL)setIntValue:(int)value toParameter:(UInt32)identifier atTime:(CMTime)time {_values[@(identifier)]=@(value);return YES;}
|
||||||
|
- (void)startAction:(id)sender {_actionStarts++;_actionDepth++;}
|
||||||
|
- (void)endAction:(id)sender {_actionEnds++;if(_actionDepth)_actionDepth--;}
|
||||||
- (CMTime)currentTime { return _hostTime; }
|
- (CMTime)currentTime { return _hostTime; }
|
||||||
- (BOOL)startForwardAnalysis:(FxAnalysisLocation)location error:(NSError **)error {
|
- (BOOL)startForwardAnalysis:(FxAnalysisLocation)location error:(NSError **)error {
|
||||||
_forwardRequests++; return !_rejectAnalysis;
|
_forwardRequests++; return !_rejectAnalysis;
|
||||||
@@ -393,10 +411,10 @@ static void TrackingCoordinateTests(NSDictionary *archive, NSDictionary *hostVal
|
|||||||
trustedAPI.values = [values mutableCopy]; trustedAPI.values[@(PGTrackData3)] = [trustedTrack base64EncodedStringWithOptions:0];
|
trustedAPI.values = [values mutableCopy]; trustedAPI.values[@(PGTrackData3)] = [trustedTrack base64EncodedStringWithOptions:0];
|
||||||
Check(Render(trusted, output, @[source], Snapshot(trusted,time), time, nil, NULL), @"fresh plugin restores a marked coordinate archive");
|
Check(Render(trusted, output, @[source], Snapshot(trusted,time), time, nil, NULL), @"fresh plugin restores a marked coordinate archive");
|
||||||
NSData *trustedBytes = SurfaceBytes(output);
|
NSData *trustedBytes = SurfaceBytes(output);
|
||||||
PGBeautySettings tracked = [trusted settingsAtTime:time];
|
PGBeautySettings tracked = [trusted settingsAtTime:time]; tracked.trackingEnabled=1;
|
||||||
NSData *expectedSaved = ImageBytes(context, ProcessedReference(portrait,tracked,time,trustedTrack));
|
NSData *expectedSaved = ImageBytes(context, ProcessedReference(portrait,tracked,time,trustedTrack));
|
||||||
Check(expectedSaved.length && MaximumDifference(trustedBytes,expectedSaved) <= 1, @"marked saved geometry matches independent exact-time archive replay");
|
Check(expectedSaved.length && MaximumDifference(trustedBytes,expectedLive) <= 4 && MeanDifference(trustedBytes,expectedLive) < .005, @"automatic adapter ignores even marked saved geometry and uses the current face");
|
||||||
Check(MaximumDifference(trustedBytes,expectedLive) > 5, @"forged marked geometry is observably different, making the legacy non-replay assertion meaningful");
|
Check(MaximumDifference(expectedSaved,expectedLive) > 5, @"direct archive replay would visibly differ, making the automatic non-replay assertion meaningful");
|
||||||
|
|
||||||
// Reanalysis of an unmarked target starts a fresh compatible archive.
|
// Reanalysis of an unmarked target starts a fresh compatible archive.
|
||||||
[legacy analyzeForward];
|
[legacy analyzeForward];
|
||||||
@@ -437,7 +455,7 @@ int main(int argc, const char *argv[]) {
|
|||||||
PGHostAPI *api = [PGHostAPI new]; beatygrilPlugIn *plugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];
|
PGHostAPI *api = [PGHostAPI new]; beatygrilPlugIn *plugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];
|
||||||
NSError *error = nil;
|
NSError *error = nil;
|
||||||
Check([plugin addParametersWithError:&error] && !error, @"host creates complete parameter set");
|
Check([plugin addParametersWithError:&error] && !error, @"host creates complete parameter set");
|
||||||
UInt32 identifiers[] = {PGAmount,PGBrightness,PGRadius,PGSharpness,PGMix,PGThreshold,PGSoftness,PGEdgeProtect,PGShowMask,PGMidtone,PGHue,PGSaturation,PGHighPass,PGThin,PGFaceBoxes,PGSingleFace,PGSelection,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange,PGFeatureProtect,PGTeeth,PGEyeBright,PGBigEye,PGTrackBoxes,PGFastMulti,PGJumpGuard,PGEvenTone,PGTonePreset,PGInvertMask,PGDebugChannel,PGTrackEnabled,PGTarget,PGAnalyzeForward,PGAnalyzeBackward,PGClearTrack,PGStatus,PGTrackData1,PGTrackData2,PGTrackData3,PGSmoothingEnabled,PGColorEnabled,PGShapeEnabled,PGDetailEnabled};
|
UInt32 identifiers[] = {PGAmount,PGBrightness,PGRadius,PGSharpness,PGMix,PGThreshold,PGSoftness,PGEdgeProtect,PGShowMask,PGMidtone,PGHue,PGSaturation,PGHighPass,PGThin,PGFaceBoxes,PGSingleFace,PGSelection,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange,PGFeatureProtect,PGTeeth,PGEyeBright,PGBigEye,PGTrackBoxes,PGFastMulti,PGJumpGuard,PGEvenTone,PGTonePreset,PGInvertMask,PGDebugChannel,PGTrackEnabled,PGTarget,PGAnalyzeForward,PGAnalyzeBackward,PGClearTrack,PGStatus,PGTrackData1,PGTrackData2,PGTrackData3,PGSmoothingEnabled,PGColorEnabled,PGShapeEnabled,PGDetailEnabled,PGInspectorPanel};
|
||||||
for (NSUInteger i = 0; i < sizeof(identifiers) / sizeof(UInt32); i++) Check(api.definitions[@(identifiers[i])] != nil, [NSString stringWithFormat:@"required parameter %u exists", identifiers[i]]);
|
for (NSUInteger i = 0; i < sizeof(identifiers) / sizeof(UInt32); i++) Check(api.definitions[@(identifiers[i])] != nil, [NSString stringWithFormat:@"required parameter %u exists", identifiers[i]]);
|
||||||
Check(api.depth == 0 && !api.duplicate && api.definitions.count == sizeof(identifiers) / sizeof(UInt32) + 6, @"parameter groups close and IDs are unique");
|
Check(api.depth == 0 && !api.duplicate && api.definitions.count == sizeof(identifiers) / sizeof(UInt32) + 6, @"parameter groups close and IDs are unique");
|
||||||
Check([api.values[@(PGMidtone)] doubleValue] == 0 && [api.values[@(PGMix)] doubleValue] == 1, @"default detail and mix form identity filter");
|
Check([api.values[@(PGMidtone)] doubleValue] == 0 && [api.values[@(PGMix)] doubleValue] == 1, @"default detail and mix form identity filter");
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#define main PGExistingHostAdapterMain
|
||||||
|
#include "host_adapter_tests.m"
|
||||||
|
#undef main
|
||||||
|
#import "PGInspectorView.h"
|
||||||
|
|
||||||
|
@interface PGInspectorTestAPI : PGHostAPI
|
||||||
|
@property NSUInteger accessViolations;
|
||||||
|
@property NSUInteger customWrites;
|
||||||
|
@property NSUInteger scalarWrites;
|
||||||
|
@property (strong) NSDictionary *actionBefore;
|
||||||
|
@property (strong) NSDictionary *undoValues;
|
||||||
|
@end
|
||||||
|
@implementation PGInspectorTestAPI
|
||||||
|
- (void)guard {if(!self.actionDepth)_accessViolations++;}
|
||||||
|
- (void)startAction:(id)sender {if(!self.actionDepth)_actionBefore=[self.values copy];[super startAction:sender];}
|
||||||
|
- (void)endAction:(id)sender {if(self.actionDepth==1 && ![self.values isEqual:_actionBefore])_undoValues=_actionBefore;[super endAction:sender];}
|
||||||
|
- (CMTime)currentTime {[self guard];return [super currentTime];}
|
||||||
|
- (BOOL)getCustomParameterValue:(NSObject **)v fromParameter:(UInt32)i atTime:(CMTime)t {[self guard];return [super getCustomParameterValue:v fromParameter:i atTime:t];}
|
||||||
|
- (BOOL)getFloatValue:(double *)v fromParameter:(UInt32)i atTime:(CMTime)t {[self guard];return [super getFloatValue:v fromParameter:i atTime:t];}
|
||||||
|
- (BOOL)getBoolValue:(BOOL *)v fromParameter:(UInt32)i atTime:(CMTime)t {[self guard];return [super getBoolValue:v fromParameter:i atTime:t];}
|
||||||
|
- (BOOL)getIntValue:(int *)v fromParameter:(UInt32)i atTime:(CMTime)t {[self guard];return [super getIntValue:v fromParameter:i atTime:t];}
|
||||||
|
- (BOOL)setCustomParameterValue:(NSObject<NSSecureCoding,NSCopying> *)v toParameter:(UInt32)i atTime:(CMTime)t {[self guard];_customWrites++;return [super setCustomParameterValue:v toParameter:i atTime:t];}
|
||||||
|
- (BOOL)setFloatValue:(double)v toParameter:(UInt32)i atTime:(CMTime)t {[self guard];_scalarWrites++;return [super setFloatValue:v toParameter:i atTime:t];}
|
||||||
|
- (BOOL)setBoolValue:(BOOL)v toParameter:(UInt32)i atTime:(CMTime)t {[self guard];_scalarWrites++;return [super setBoolValue:v toParameter:i atTime:t];}
|
||||||
|
- (BOOL)setIntValue:(int)v toParameter:(UInt32)i atTime:(CMTime)t {[self guard];_scalarWrites++;return [super setIntValue:v toParameter:i atTime:t];}
|
||||||
|
@end
|
||||||
|
|
||||||
|
static NSControl *FindControl(NSView *view,Class type,NSString *label) {
|
||||||
|
if([view isKindOfClass:type] && [view.accessibilityLabel isEqual:label])return (NSControl *)view;
|
||||||
|
for(NSView *child in view.subviews){NSControl *found=FindControl(child,type,label);if(found)return found;}return nil;
|
||||||
|
}
|
||||||
|
static void Fire(NSControl *control) {[control sendAction:control.action to:control.target];}
|
||||||
|
static BOOL Near(double a,double b) {return fabs(a-b)<.00001;}
|
||||||
|
static void RegistrationTests(void) {
|
||||||
|
PGHostAPI *api=[PGHostAPI new];beatygrilPlugIn *plugin=[[beatygrilPlugIn alloc]initWithAPIManager:api];[plugin addParametersWithError:nil];
|
||||||
|
NSUInteger visible=0;for(NSNumber *key in api.definitions){NSDictionary *d=api.definitions[key];if(!([d[@"flags"]unsignedIntValue]&kFxParameterFlag_HIDDEN)){visible++;Check(key.unsignedIntValue==PGInspectorPanel,@"only the custom panel is visible in the native parameter list");}}
|
||||||
|
Check(visible==1,@"all duplicate standard controls and manual-analysis groups are hidden");
|
||||||
|
FxParameterFlags flags=[api.definitions[@(PGInspectorPanel)][@"flags"]unsignedIntValue];
|
||||||
|
Check((flags&kFxParameterFlag_CUSTOM_UI) && (flags&kFxParameterFlag_USE_FULL_VIEW_WIDTH),@"panel requests full-width custom UI");
|
||||||
|
Check([plugin classForCustomParameterID:PGInspectorPanel]==PGInspectorState.class && [[plugin classesForCustomParameterID:PGInspectorPanel]containsObject:NSDictionary.class],@"host can securely decode the published state and its nested classes");
|
||||||
|
for(NSNumber *key in @[@(PGAnalyzeForward),@(PGAnalyzeBackward),@(PGClearTrack)])Check(([api.definitions[key][@"flags"]unsignedIntValue]&(kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED))==(kFxParameterFlag_HIDDEN|kFxParameterFlag_DISABLED),@"legacy analysis commands are hidden and disabled");
|
||||||
|
Check([plugin createViewForParameterID:999]==nil,@"unknown parameter IDs do not create a second inspector");
|
||||||
|
for(NSDictionary *d in PGInspectorControls()) {
|
||||||
|
NSNumber *key=d[@"id"];Check(api.definitions[key]!=nil,@"every inspector control maps to an existing stable parameter ID");
|
||||||
|
if([d[@"kind"]isEqual:@"slider"])Check(Near([d[@"min"]doubleValue],[api.definitions[key][@"min"]doubleValue]) && Near([d[@"max"]doubleValue],[api.definitions[key][@"max"]doubleValue]),@"UI and renderer enforce the same slider range");
|
||||||
|
}
|
||||||
|
api.values[@(PGTrackEnabled)]=@YES;api.values[@(PGSingleFace)]=@YES;api.values[@(PGShowMask)]=@YES;
|
||||||
|
PGBeautySettings s=[plugin settingsAtTime:kCMTimeZero];Check(!s.trackingEnabled && !s.singleFace && !s.showMask,@"old manual analysis, selection and debug flags cannot override automatic output");
|
||||||
|
NSData *snapshot=Snapshot(plugin,kCMTimeZero);NSDictionary *plist=[NSPropertyListSerialization propertyListWithData:snapshot options:0 format:nil error:nil];
|
||||||
|
Check([plist[@"tracks"]isEqual:@[@"",@"",@""]],@"automatic render snapshots do not transfer persisted manual-analysis payloads");
|
||||||
|
}
|
||||||
|
static void ModelTests(void) {
|
||||||
|
PGInspectorState *plain=[PGInspectorState stateWithValues:@{} disabledParameters:[NSSet set] hasEdits:NO];
|
||||||
|
PGBeautySettings original=PGBeautyDefaults();original.midtoneDetail=.5;original.faceChin=-.3;
|
||||||
|
PGBeautySettings untouched=PGSettingsWithInspectorState(original,plain);
|
||||||
|
Check(memcmp(&original,&untouched,sizeof(original))==0,@"untouched panel preserves a legacy document's exact settings");
|
||||||
|
NSMutableDictionary *values=[plain.values mutableCopy];values[@(PGChin)]=@(-.6);values[@(PGBigEye)]=@.2;values[@(PGBrightness)]=@.37;values[@(PGTonePreset)]=@4;values[@(PGMidtone)]=@.7;
|
||||||
|
PGInspectorState *a=[PGInspectorState stateWithValues:values disabledParameters:[NSSet setWithObject:@(PGBrightness)] hasEdits:YES];
|
||||||
|
NSData *encoded=[NSKeyedArchiver archivedDataWithRootObject:a requiringSecureCoding:YES error:nil];
|
||||||
|
PGInspectorState *decoded=[NSKeyedUnarchiver unarchivedObjectOfClass:PGInspectorState.class fromData:encoded error:nil];
|
||||||
|
Check([decoded isEqual:a] && decoded!=a && decoded.hasEdits,@"secure host archive restores values, per-control bypass and edit state");
|
||||||
|
Check([[a copy]isEqual:a] && a.hash==[[a copy]hash],@"copy and equality are stable for host undo snapshots");
|
||||||
|
PGBeautySettings s=PGSettingsWithInspectorState(PGBeautyDefaults(),decoded);
|
||||||
|
Check(Near(s.faceChin,-.6) && Near(s.faceBigEye,.2) && s.skinBrightness==0 && s.skinTonePreset==4,@"chin, eyes and bypass route to the correct independent render fields");
|
||||||
|
NSMutableDictionary *bValues=[values mutableCopy];bValues[@(PGChin)]=@.6;bValues[@(PGTonePreset)]=@6;
|
||||||
|
PGInspectorState *b=[PGInspectorState stateWithValues:bValues disabledParameters:[NSSet set] hasEdits:YES];
|
||||||
|
PGInspectorState *mid=(PGInspectorState *)[a interpolateBetween:b withWeight:.5];
|
||||||
|
Check(Near([mid valueForParameter:PGChin],0) && [mid valueForParameter:PGTonePreset]==4 && ![mid isParameterEnabled:PGBrightness],@"numeric keyframes interpolate while menu and enable states hold the preceding key");
|
||||||
|
Check([[a interpolateBetween:b withWeight:0]isEqual:a] && [[a interpolateBetween:b withWeight:1]isEqual:b],@"keyframe endpoints restore exact independent states");
|
||||||
|
Check([[plain interpolateBetween:b withWeight:.5]isEqual:plain],@"uncommitted legacy state does not invent a numeric transition before the first panel edit");
|
||||||
|
NSDictionary *bad=@{@(PGChin):@9,@(PGTonePreset):@(-2),@(PGBrightness):@(NAN),@999:@.5};
|
||||||
|
PGInspectorState *safe=[PGInspectorState stateWithValues:bad disabledParameters:[NSSet setWithObjects:@(PGRadius),@999,nil] hasEdits:YES];
|
||||||
|
Check([safe valueForParameter:PGChin]==1 && [safe valueForParameter:PGTonePreset]==0 && [safe valueForParameter:PGBrightness]==0 && safe.values[@999]==nil && safe.disabledParameters.count==0,@"malformed controls are clamped, finite and limited to the supported schema");
|
||||||
|
NSMutableDictionary *off=[a.values mutableCopy];off[@(PGShapeEnabled)]=@0;off[@(PGColorEnabled)]=@0;off[@(PGSmoothingEnabled)]=@0;
|
||||||
|
s=PGSettingsWithInspectorState(PGBeautyDefaults(),[PGInspectorState stateWithValues:off disabledParameters:[NSSet set] hasEdits:YES]);
|
||||||
|
Check(s.faceChin==0 && s.faceBigEye==0 && s.skinBrightness==0 && s.skinTonePreset==0 && s.midtoneDetail==0 && s.highPassDetail==0,@"card eye switches bypass the complete associated effect group");
|
||||||
|
}
|
||||||
|
static void ControllerTests(void) {
|
||||||
|
PGInspectorTestAPI *api=[PGInspectorTestAPI new];beatygrilPlugIn *plugin=[[beatygrilPlugIn alloc]initWithAPIManager:api];[plugin addParametersWithError:nil];
|
||||||
|
api.values[@(PGMidtone)]=@.5;
|
||||||
|
PGInspectorController *c=[[PGInspectorController alloc]initWithAPIManager:api];[c refresh];
|
||||||
|
Check(c.available && Near([c.state valueForParameter:PGMidtone],.5) && !c.state.hasEdits,@"initial UI reads the current legacy/template values instead of replacing them");
|
||||||
|
NSUInteger starts=api.actionStarts,writes=api.customWrites;
|
||||||
|
[c beginInteraction];[c setValue:.1 forParameter:PGChin];[c setValue:.3 forParameter:PGChin];[c setValue:.5 forParameter:PGChin];[c endInteraction];
|
||||||
|
Check(api.actionStarts==starts+1 && api.actionDepth==0 && api.customWrites==writes+3,@"a full continuous drag uses one balanced host action");
|
||||||
|
Check(api.scalarWrites==3,@"continuous drags mirror only changed values rather than rewriting every hidden parameter");
|
||||||
|
PGInspectorState *saved=api.values[@(PGInspectorPanel)];Check(saved.hasEdits && Near([saved valueForParameter:PGChin],.5) && Near([saved valueForParameter:PGMidtone],.5),@"published state contains the edited control and all untouched document values");
|
||||||
|
NSUInteger before=api.customWrites;[c setValue:.5 forParameter:PGChin];Check(api.customWrites==before,@"an unchanged value does not create duplicate undo writes");
|
||||||
|
PGInspectorController *restored=[[PGInspectorController alloc]initWithAPIManager:api];[restored refresh];Check([restored.state isEqual:saved],@"fresh inspector restores the full custom parameter state");
|
||||||
|
[c setValue:.37 forParameter:PGBrightness];[c toggleEnabledForParameter:PGBrightness];
|
||||||
|
Check(Near([c.state valueForParameter:PGBrightness],.37) && ![c.state isParameterEnabled:PGBrightness],@"eye bypass preserves the prior slider value");
|
||||||
|
[c toggleEnabledForParameter:PGBrightness];Check([c.state isParameterEnabled:PGBrightness] && Near([c.state valueForParameter:PGBrightness],.37),@"reenabling restores the exact amount");
|
||||||
|
[c toggleEnabledForParameter:PGBrightness];[c resetParameter:PGBrightness];Check([c.state isParameterEnabled:PGBrightness] && [c.state valueForParameter:PGBrightness]==0,@"reset restores default and clears that item's bypass");
|
||||||
|
NSDictionary *unchanged=[api.values copy];PGInspectorState *old=[c.state copy];api.rejectedWriteParameter=PGInspectorPanel;
|
||||||
|
Check(![c setValue:.9 forParameter:PGChin] && [api.values isEqual:unchanged] && [c.state isEqual:old] && c.statusMessage.length>0,@"rejected custom writes leave the UI and all render parameters unchanged");api.rejectedWriteParameter=0;
|
||||||
|
Check(![c setValue:NAN forParameter:PGChin] && ![c setValue:.3 forParameter:999] && ![c toggleEnabledForParameter:PGRadius],@"invalid edits and unsupported bypass are safely rejected");
|
||||||
|
[c setValue:-.4 forParameter:PGChin];NSDictionary *undo=api.undoValues;
|
||||||
|
api.values=[undo mutableCopy];[[NSRunLoop currentRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.36]];[c refresh];
|
||||||
|
Check(Near([c.state valueForParameter:PGChin],[old valueForParameter:PGChin]),@"host undo refresh restores the prior committed state after pending synchronization expires");
|
||||||
|
Check(api.accessViolations==0 && api.actionDepth==0 && api.actionStarts==api.actionEnds,@"all arbitrary-time reads and writes are inside balanced FxPlug action pairs");
|
||||||
|
Check(api.forwardRequests==0 && api.backwardRequests==0,@"normal inspector edits never request manual analysis");
|
||||||
|
[api.disabledAPIs addObject:@"FxCustomParameterActionAPI_v4"];
|
||||||
|
PGInspectorController *offline=[[PGInspectorController alloc]initWithAPIManager:api];[offline refresh];
|
||||||
|
Check(!offline.available && ![offline setValue:.2 forParameter:PGChin],@"missing host actions disable editing instead of unsafe parameter access");
|
||||||
|
}
|
||||||
|
static void ViewInteractionTests(void) {
|
||||||
|
PGHostAPI *api=[PGHostAPI new];beatygrilPlugIn *plugin=[[beatygrilPlugIn alloc]initWithAPIManager:api];[plugin addParametersWithError:nil];
|
||||||
|
PGInspectorView *view=(PGInspectorView *)[plugin createViewForParameterID:PGInspectorPanel];[view refreshFromHost];
|
||||||
|
NSSlider *chin=(NSSlider *)FindControl(view,NSSlider.class,@"下巴");Check(chin!=nil && chin.enabled,@"custom view exposes an accessible native chin slider");
|
||||||
|
chin.doubleValue=-.75;Fire(chin);PGInspectorState *s=api.values[@(PGInspectorPanel)];
|
||||||
|
Check(Near([s valueForParameter:PGChin],-.75) && [s valueForParameter:PGBigEye]==0,@"actual chin slider events do not write the eye parameter");
|
||||||
|
NSTextField *number=(NSTextField *)FindControl(view,NSTextField.class,@"下巴数值");number.stringValue=@"0.40";Fire(number);s=api.values[@(PGInspectorPanel)];Check(Near([s valueForParameter:PGChin],.4),@"editable numeric field writes its signed control");
|
||||||
|
number.stringValue=@"invalid";Fire(number);s=api.values[@(PGInspectorPanel)];Check(Near([s valueForParameter:PGChin],.4),@"invalid number entry preserves the last valid value");
|
||||||
|
NSButton *preview=(NSButton *)FindControl(view,NSButton.class,@"人脸框预览");preview.state=NSControlStateValueOn;Fire(preview);s=api.values[@(PGInspectorPanel)];Check([s valueForParameter:PGTrackBoxes]==1,@"preview switch events change the automatic face box setting");
|
||||||
|
for(NSNumber *width in @[@280,@320,@380,@520]) {
|
||||||
|
[view setFrameSize:NSMakeSize(width.doubleValue,20)];[view layoutSubtreeIfNeeded];
|
||||||
|
Check(view.frame.size.height>500 && Near(view.frame.size.height,view.fittingSize.height) && Near(view.frame.size.height,view.intrinsicContentSize.height),@"width changes produce a complete consistent intrinsic/fitting height");
|
||||||
|
for(NSView *card in view.subviews)if(!card.hidden)Check(NSMaxY(card.frame)<=NSHeight(view.bounds)+1 && NSMaxX(card.frame)<=NSWidth(view.bounds)+1,@"every visible card and footer remains inside the panel at all inspector widths");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int main(void) {@autoreleasepool {
|
||||||
|
[NSApplication sharedApplication];[NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
|
||||||
|
RegistrationTests();ModelTests();ControllerTests();ViewInteractionTests();
|
||||||
|
printf("Inspector: %lu checks, %lu failures\n",(unsigned long)Checks,(unsigned long)Failures);return Failures?1:0;
|
||||||
|
}}
|
||||||
在新工单中引用
屏蔽一个用户