Build native card inspector and secure state; hide manual analysis and unify automatic follow (validation in progress)

这个提交包含在:
dayu 于 2026-10-21 22:45:37 +08:00 提交
1 父节点 5793e3de49
当前提交 f4c0d608a4
20 个文件已更改
+1492 -236

没有与搜索条件相匹配的文件

+8 -2
查看文件
@@ -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"
} }
} }
@@ -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>
+26 -18
查看文件
@@ -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;
}; };
+4 -4
查看文件
@@ -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
+2 -1
查看文件
@@ -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
}; };
+1 -1
查看文件
@@ -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;
+33 -29
查看文件
@@ -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>
+219
查看文件
@@ -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");
+125
查看文件
@@ -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;
}}