From f4c0d608a43e17ceb181979bd77c3a38a09173b3 Mon Sep 17 00:00:00 2001 From: dayu <506228475@qq.com> Date: Wed, 21 Oct 2026 22:45:37 +0800 Subject: [PATCH] Build native card inspector and secure state; hide manual analysis and unify automatic follow (validation in progress) --- README.md | 10 +- Templates/大鱼人像美化/PARAMETERS.json | 257 ++++++++++--- Templates/大鱼人像美化/README.md | 46 ++- Templates/大鱼人像美化/VALIDATION.json | 126 ++++++- Templates/大鱼人像美化/大鱼人像美化.moef | 146 +++----- beatygril/beatygril.xcodeproj/project.pbxproj | 44 ++- beatygril/beatygril/Plugin/Info.plist | 8 +- beatygril/beatygril/Plugin/PGInspectorState.h | 42 +++ beatygril/beatygril/Plugin/PGInspectorState.m | 224 ++++++++++++ beatygril/beatygril/Plugin/PGInspectorView.h | 15 + beatygril/beatygril/Plugin/PGInspectorView.m | 344 ++++++++++++++++++ beatygril/beatygril/Plugin/PGParameterIDs.h | 3 +- beatygril/beatygril/Plugin/beatygrilPlugIn.h | 2 +- beatygril/beatygril/Plugin/beatygrilPlugIn.m | 62 ++-- .../beatygril/Wrapper Application/Info.plist | 4 +- scripts/inspector-template.py | 219 +++++++++++ scripts/test-host.sh | 3 +- scripts/test-inspector.sh | 22 ++ tests/host_adapter_tests.m | 26 +- tests/inspector_tests.m | 125 +++++++ 20 files changed, 1492 insertions(+), 236 deletions(-) create mode 100644 beatygril/beatygril/Plugin/PGInspectorState.h create mode 100644 beatygril/beatygril/Plugin/PGInspectorState.m create mode 100644 beatygril/beatygril/Plugin/PGInspectorView.h create mode 100644 beatygril/beatygril/Plugin/PGInspectorView.m create mode 100644 scripts/inspector-template.py create mode 100755 scripts/test-inspector.sh create mode 100644 tests/inspector_tests.m diff --git a/README.md b/README.md index ab2c4d1..35c812c 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ 基于本仓库的 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画面由用户自行测试。 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。 diff --git a/Templates/大鱼人像美化/PARAMETERS.json b/Templates/大鱼人像美化/PARAMETERS.json index df2c01f..0b9ccd4 100644 --- a/Templates/大鱼人像美化/PARAMETERS.json +++ b/Templates/大鱼人像美化/PARAMETERS.json @@ -1,9 +1,28 @@ { - "version": "1.0.2", + "version": "1.1.0", "pluginUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241", "filterObject": "510000005", - "visiblePublished": 46, + "visiblePublished": 1, "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, "name": "启用磨皮", @@ -12,7 +31,9 @@ "kind": "bool", "default": 1, "value": 1, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 1, @@ -22,7 +43,9 @@ "kind": "float", "default": 1, "value": 1, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 12, @@ -32,7 +55,9 @@ "kind": "float", "default": 0, "value": 0.5, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 3, @@ -42,7 +67,9 @@ "kind": "float", "default": 8, "value": 8, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 30, @@ -52,7 +79,9 @@ "kind": "bool", "default": 1, "value": 1, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 71, @@ -62,7 +91,9 @@ "kind": "bool", "default": 1, "value": 1, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 42, @@ -72,7 +103,9 @@ "kind": "popup", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12901744656" }, { "id": 2, @@ -82,7 +115,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 14, @@ -92,7 +127,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 15, @@ -102,7 +139,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 41, @@ -112,7 +151,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 31, @@ -122,7 +163,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 32, @@ -132,7 +175,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 73, @@ -142,7 +187,9 @@ "kind": "bool", "default": 1, "value": 1, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 17, @@ -152,7 +199,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 4, @@ -162,7 +211,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 72, @@ -172,7 +223,9 @@ "kind": "bool", "default": 1, "value": 1, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 18, @@ -182,7 +235,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 33, @@ -192,7 +247,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 22, @@ -202,7 +259,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 23, @@ -212,7 +271,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 24, @@ -222,7 +283,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 25, @@ -232,7 +295,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 26, @@ -242,7 +307,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 27, @@ -252,7 +319,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 28, @@ -262,7 +331,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 29, @@ -272,7 +343,9 @@ "kind": "float", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 20, @@ -282,7 +355,9 @@ "kind": "bool", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 21, @@ -292,7 +367,9 @@ "kind": "point", "default": 0.5, "value": 0.5, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905943072" }, { "id": 39, @@ -302,7 +379,9 @@ "kind": "bool", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 40, @@ -312,7 +391,9 @@ "kind": "float", "default": 1.8, "value": 1.8, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 45, @@ -322,7 +403,9 @@ "kind": "bool", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 46, @@ -332,7 +415,9 @@ "kind": "popup", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12901744656" }, { "id": 50, @@ -342,7 +427,9 @@ "kind": "button", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": null }, { "id": 51, @@ -352,7 +439,9 @@ "kind": "button", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": null }, { "id": 52, @@ -362,7 +451,9 @@ "kind": "button", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": null }, { "id": 53, @@ -372,7 +463,9 @@ "kind": "status", "default": "选择画面中的人脸后开始分析", "value": "选择画面中的人脸后开始分析", - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 19, @@ -382,7 +475,9 @@ "kind": "bool", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 35, @@ -392,7 +487,9 @@ "kind": "bool", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 7, @@ -402,7 +499,9 @@ "kind": "float", "default": 148, "value": 148, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 8, @@ -412,7 +511,9 @@ "kind": "float", "default": 0.6, "value": 0.6, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 9, @@ -422,7 +523,9 @@ "kind": "float", "default": 0.4, "value": 0.4, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 10, @@ -432,7 +535,9 @@ "kind": "bool", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 43, @@ -442,7 +547,9 @@ "kind": "bool", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 44, @@ -452,7 +559,9 @@ "kind": "popup", "default": 0, "value": 0, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12901744656" }, { "id": 5, @@ -462,7 +571,9 @@ "kind": "float", "default": 1, "value": 1, - "published": true + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 60, @@ -470,7 +581,9 @@ "channel": "./60", "kind": "hidden-string", "value": "", - "published": false + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 61, @@ -478,7 +591,9 @@ "channel": "./61", "kind": "hidden-string", "value": "", - "published": false + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" }, { "id": 62, @@ -486,7 +601,9 @@ "channel": "./62", "kind": "hidden-string", "value": "", - "published": false + "published": false, + "hiddenCompatibility": true, + "templateFlags": "12905938976" } ], "effectSource": { @@ -498,5 +615,43 @@ "dropZone": 1, "path": "Drop Zone.tiff", "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" } } diff --git a/Templates/大鱼人像美化/README.md b/Templates/大鱼人像美化/README.md index ea3c608..3d7baba 100644 --- a/Templates/大鱼人像美化/README.md +++ b/Templates/大鱼人像美化/README.md @@ -1,37 +1,43 @@ # 大鱼人像美化 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`,不使用原版私有数据、类名、品牌资源或空 `` 猜测初始化。默认和当前归档相同,可完整解码并往返核验。 -`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 <输出文件>` 可重新包装文本值,不启动宿主。其结果明确保留宿主加载待验状态。 diff --git a/Templates/大鱼人像美化/VALIDATION.json b/Templates/大鱼人像美化/VALIDATION.json index 8ef9622..b13903e 100644 --- a/Templates/大鱼人像美化/VALIDATION.json +++ b/Templates/大鱼人像美化/VALIDATION.json @@ -1,11 +1,11 @@ { - "version": "1.0.6", - "sha256": "30ca8edf36801264c5de2c753c40b720aba8052633620713a12401098fcd2409", + "version": "1.1.0", + "sha256": "e1c0cff1dd3541ed97cab2cb39a76ad4f28dc659b3be7991cc7ac92dd8ea090b", "filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241", - "publishedFunctionalControls": 46, + "publishedFunctionalControls": 1, "informationalControls": 2, "hiddenTrackingSlots": 3, - "publishedPathsResolved": 48, + "publishedPathsResolved": 3, "sourceType": 3, "sourceMedia": "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.", "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": { "offlineFullChecks": 604, "slimChecks": 224, @@ -51,5 +43,115 @@ "maxByteErrorVsIndependentCIReference": 2, "maxUVOracleError": 1.09828022e-05, "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" } } diff --git a/Templates/大鱼人像美化/大鱼人像美化.moef b/Templates/大鱼人像美化/大鱼人像美化.moef index 9811823..28f9978 100644 --- a/Templates/大鱼人像美化/大鱼人像美化.moef +++ b/Templates/大鱼人像美化/大鱼人像美化.moef @@ -46,7 +46,7 @@ 1 - 大鱼人像美化 1.0.6:人像磨皮、肤色、脸型、五官保护和三目标人脸跟踪。模板中频磨皮默认为50%。 + 大鱼人像美化 1.1.0:磨皮、美白、瘦脸三卡片自定义面板,自动选择主体并实时跟随。模板局部磨皮默认为50%。 1920 @@ -82,52 +82,7 @@ 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 0 1 1 0 510000001 @@ -298,72 +253,81 @@ - + 8589934608 + + 1 + + + + + ********-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* + ********-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* + - - - - - + + + + + - + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - + + + + + + + + + + + - - + + 15 - - + + - - - + + + - + 选择画面中的人脸后开始分析 - - + + - - - - - + + + + + - + @@ -377,7 +341,7 @@ - + 1 diff --git a/beatygril/beatygril.xcodeproj/project.pbxproj b/beatygril/beatygril.xcodeproj/project.pbxproj index db5251b..17e015f 100644 --- a/beatygril/beatygril.xcodeproj/project.pbxproj +++ b/beatygril/beatygril.xcodeproj/project.pbxproj @@ -63,11 +63,13 @@ "/Localized: Plugin/InfoPlist.strings", "/Localized: Plugin/Localizable.strings", Plugin/beatygrilPlugIn.m, + Plugin/Info.plist, + Plugin/main.m, Plugin/PGBeautyRenderer.m, Plugin/PGFaceProcessor.m, Plugin/PGFaceSelectOSC.m, - Plugin/Info.plist, - Plugin/main.m, + Plugin/PGInspectorState.m, + Plugin/PGInspectorView.m, "Wrapper Application/Info.plist", ); target = 6945FC8A3088B7220013E108 /* Wrapper Application */; @@ -306,11 +308,11 @@ 6945FCCD3088B7220013E108 /* Debug configuration for PBXNativeTarget "XPC Service" */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_STYLE = Manual; - CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; CODE_SIGN_IDENTITY = "Developer ID Application"; - ENABLE_HARDENED_RUNTIME = YES; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = B3W25FX5H5; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; INFOPLIST_FILE = beatygril/Plugin/Info.plist; @@ -329,11 +331,11 @@ 6945FCCE3088B7220013E108 /* Release configuration for PBXNativeTarget "XPC Service" */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_STYLE = Manual; - CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; CODE_SIGN_IDENTITY = "Developer ID Application"; - ENABLE_HARDENED_RUNTIME = YES; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = B3W25FX5H5; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; INFOPLIST_FILE = beatygril/Plugin/Info.plist; @@ -353,11 +355,11 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements"; - CODE_SIGN_STYLE = Manual; - CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; CODE_SIGN_IDENTITY = "Developer ID Application"; - ENABLE_HARDENED_RUNTIME = YES; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = B3W25FX5H5; + ENABLE_HARDENED_RUNTIME = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril; @@ -370,11 +372,11 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "beatygril/Wrapper Application/SandboxEntitlements.entitlements"; - CODE_SIGN_STYLE = Manual; - CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; CODE_SIGN_IDENTITY = "Developer ID Application"; - ENABLE_HARDENED_RUNTIME = YES; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = B3W25FX5H5; + ENABLE_HARDENED_RUNTIME = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; INFOPLIST_FILE = "beatygril/Wrapper Application/Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = com.dayu.beatygril; @@ -387,9 +389,11 @@ isa = XCBuildConfiguration; buildSettings = { ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk; - ARCHS = "arm64 x86_64"; - ONLY_ACTIVE_ARCH = NO; ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = ( + arm64, + x86_64, + ); ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -448,6 +452,7 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; SDKROOT = macosx; SWIFT_VERSION = 5; }; @@ -457,9 +462,11 @@ isa = XCBuildConfiguration; buildSettings = { ADDITIONAL_SDKS = /Library/Developer/SDKs/FxPlug.sdk; - ARCHS = "arm64 x86_64"; - ONLY_ACTIVE_ARCH = NO; ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = ( + arm64, + x86_64, + ); ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -512,6 +519,7 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; SDKROOT = macosx; SWIFT_VERSION = 5; }; diff --git a/beatygril/beatygril/Plugin/Info.plist b/beatygril/beatygril/Plugin/Info.plist index 9baedf9..8276530 100644 --- a/beatygril/beatygril/Plugin/Info.plist +++ b/beatygril/beatygril/Plugin/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.0.6 + 1.1.0 CFBundleVersion - 8 + 9 NSHumanReadableCopyright PlugInKit @@ -72,7 +72,7 @@ uuid FB14B0E6-59B2-4836-916F-CD0C559E9241 version - 1.0.6 + 1.1.0 className @@ -84,7 +84,7 @@ uuid E0E432C8-3D71-4ABA-B006-E7F519E06C87 version - 1.0.6 + 1.1.0 protocolNames FxOnScreenControl diff --git a/beatygril/beatygril/Plugin/PGInspectorState.h b/beatygril/beatygril/Plugin/PGInspectorState.h new file mode 100644 index 0000000..7ca08a2 --- /dev/null +++ b/beatygril/beatygril/Plugin/PGInspectorState.h @@ -0,0 +1,42 @@ +#import +#import +#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 *PGInspectorControls(void); +FOUNDATION_EXPORT NSDictionary * _Nullable PGInspectorDescriptor(UInt32 identifier); + +@interface PGInspectorState : NSObject +@property (nonatomic, copy, readonly) NSDictionary *values; +@property (nonatomic, copy, readonly) NSSet *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)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 diff --git a/beatygril/beatygril/Plugin/PGInspectorState.m b/beatygril/beatygril/Plugin/PGInspectorState.m new file mode 100644 index 0000000..5acc7ca --- /dev/null +++ b/beatygril/beatygril/Plugin/PGInspectorState.m @@ -0,0 +1,224 @@ +#import "PGInspectorState.h" +#import +#import + +NSArray *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 *)interpolateBetween:(NSObject *)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 manager; +@property (nonatomic,strong) id action; +@property (nonatomic) BOOL interacting; +@property (nonatomic) NSTimeInterval pendingUntil; +@end +@implementation PGInspectorController +- (instancetype)initWithAPIManager:(id)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 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 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 diff --git a/beatygril/beatygril/Plugin/PGInspectorView.h b/beatygril/beatygril/Plugin/PGInspectorView.h new file mode 100644 index 0000000..23bc831 --- /dev/null +++ b/beatygril/beatygril/Plugin/PGInspectorView.h @@ -0,0 +1,15 @@ +#import +#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)manager; +/// Also permits a deterministic controller in an offscreen preview or test. +- (instancetype)initWithController:(PGInspectorController *)controller; +- (void)refreshFromHost; +@end + +NS_ASSUME_NONNULL_END diff --git a/beatygril/beatygril/Plugin/PGInspectorView.m b/beatygril/beatygril/Plugin/PGInspectorView.m new file mode 100644 index 0000000..f511000 --- /dev/null +++ b/beatygril/beatygril/Plugin/PGInspectorView.m @@ -0,0 +1,344 @@ +#import "PGInspectorView.h" +#include + +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 *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 +@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 *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=i12 ? 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 *_cards; + PGInspectorCard *_more; + NSTextField *_brand,*_website,*_status; + NSTimer *_refreshTimer; + NSUInteger _interactionDepth; +} +- (instancetype)initWithAPIManager:(id)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;index0?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 diff --git a/beatygril/beatygril/Plugin/PGParameterIDs.h b/beatygril/beatygril/Plugin/PGParameterIDs.h index 4cc0e20..2786f25 100644 --- a/beatygril/beatygril/Plugin/PGParameterIDs.h +++ b/beatygril/beatygril/Plugin/PGParameterIDs.h @@ -13,5 +13,6 @@ enum { PGAnalyzeForward=50, PGAnalyzeBackward=51, PGClearTrack=52, PGStatus=53, PGTrackData1=60, PGTrackData2=61, PGTrackData3=62, PGSmoothingEnabled=70, PGColorEnabled=71, PGShapeEnabled=72, - PGDetailEnabled=73, PGHelp=74 + PGDetailEnabled=73, PGHelp=74, + PGInspectorPanel=210 }; diff --git a/beatygril/beatygril/Plugin/beatygrilPlugIn.h b/beatygril/beatygril/Plugin/beatygrilPlugIn.h index 12dca07..83e3d22 100644 --- a/beatygril/beatygril/Plugin/beatygrilPlugIn.h +++ b/beatygril/beatygril/Plugin/beatygrilPlugIn.h @@ -2,7 +2,7 @@ #import #import "PGBeautyTypes.h" -@interface beatygrilPlugIn : NSObject +@interface beatygrilPlugIn : NSObject @property (nonatomic, assign) id apiManager; - (PGBeautySettings)settingsAtTime:(CMTime)time; - (void)analyzeForward; diff --git a/beatygril/beatygril/Plugin/beatygrilPlugIn.m b/beatygril/beatygril/Plugin/beatygrilPlugIn.m index c3255e6..946859d 100644 --- a/beatygril/beatygril/Plugin/beatygrilPlugIn.m +++ b/beatygril/beatygril/Plugin/beatygrilPlugIn.m @@ -2,6 +2,8 @@ #import "PGParameterIDs.h" #import "PGBeautyRenderer.h" #import "PGFaceProcessor.h" +#import "PGInspectorState.h" +#import "PGInspectorView.h" #import #import #import @@ -90,7 +92,6 @@ static CIImage *PGImageFromTile(FxImageTile *tile) { @property BOOL analyzing; @property BOOL analysisForward; @property (strong) NSArray *loadedTracks; -@property uint32_t compatibleTrackMask; @property BOOL reportedRender; @end @@ -120,8 +121,8 @@ static CIImage *PGImageFromTile(FxImageTile *tile) { PGSlider spec=PGSliders[i]; if (spec.identifier!=identifier) continue; NSString *name=[NSString stringWithUTF8String:spec.name]; 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]; - 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]; + 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_HIDDEN]; } return NO; } @@ -129,37 +130,47 @@ static CIImage *PGImageFromTile(FxImageTile *tile) { id api=[_apiManager apiForProtocol:@protocol(FxParameterCreationAPI_v5)]; if (!api) return PGFail(error,@"无法取得 FxPlug 参数创建接口。"); __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 (^toggle)(NSString *,UInt32,BOOL)=^(NSString *name,UInt32 identifier,BOOL value){ok=[api addToggleButtonWithName:name parameterID:identifier defaultValue:value parameterFlags:kFxParameterFlag_DEFAULT]&&ok;}; - void (^group)(NSString *,UInt32)=^(NSString *name,UInt32 identifier){ok=[api startParameterSubGroup:name parameterID:identifier 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_HIDDEN]&&ok;}; void (^end)(void)=^{ok=[api endParameterSubGroup]&&ok;}; group(@"磨皮",100); toggle(@"启用磨皮",PGSmoothingEnabled,YES); slider(PGAmount); slider(PGMidtone); slider(PGRadius); toggle(@"保护五官",PGFeatureProtect,YES); end(); 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(); group(@"细节",102); toggle(@"启用细节",PGDetailEnabled,YES); slider(PGHighPass); slider(PGSharpness); end(); group(@"脸型",103); toggle(@"启用脸型",PGShapeEnabled,YES); UInt32 shape[]={PGThin,PGBigEye,PGShort,PGNarrow,PGChin,PGVFace,PGJaw,PGCheekbone,PGSmallHead,PGSmallHeadRange}; for(NSUInteger i=0;i *)classesForCustomParameterID:(UInt32)identifier { + return identifier==PGInspectorPanel?[NSSet setWithObjects:PGInspectorState.class,NSDictionary.class,NSNumber.class,NSSet.class,nil]:[NSSet set]; +} - (PGBeautySettings)settingsAtTime:(CMTime)time { PGBeautySettings s=PGBeautyDefaults(); id api=[_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]; @@ -192,6 +203,13 @@ static CIImage *PGImageFromTile(FxImageTile *tile) { 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(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; } - (NSArray *)trackStrings { @@ -207,7 +225,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) { if(!state) return PGFail(error,@"缺少渲染状态输出位置。"); if(![_apiManager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)]) return PGFail(error,@"无法读取宿主参数。"); 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; } - (BOOL)destinationImageRect:(FxRect *)rect sourceImages:(NSArray *)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,@"渲染状态版本无效。"); PGBeautySettings s;[plist[@"settings"] getBytes:&s length:sizeof(s)]; if(s.version!=1) return PGFail(error,@"此版本不支持保存的渲染状态。"); + s.trackingEnabled=0;s.singleFace=0; CIImage *input=PGImageFromTile(sources.firstObject); if(!input) return PGFail(error,@"无法读取 IOSurface。"); @synchronized(self) { if(!_reportedRender) { @@ -255,21 +274,6 @@ static CIImage *PGImageFromTile(FxImageTile *tile) { NSArray *tracks=plist[@"tracks"]; if(![tracks isKindOfClass:NSArray.class]||tracks.count!=3) 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<CFBundlePackageType APPL CFBundleShortVersionString - 1.0.6 + 1.1.0 CFBundleVersion - 8 + 9 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright diff --git a/scripts/inspector-template.py b/scripts/inspector-template.py new file mode 100644 index 0000000..e079861 --- /dev/null +++ b/scripts/inspector-template.py @@ -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) diff --git a/scripts/test-host.sh b/scripts/test-host.sh index ef9c7f6..4964af9 100755 --- a/scripts/test-host.sh +++ b/scripts/test-host.sh @@ -15,8 +15,9 @@ xcrun clang "${compiler_extra[@]}" -fobjc-arc -fmodules -Wall -Wextra -Wno-unuse -I "$plugin_dir" -F "$sdk_frameworks" -F "$runtime_frameworks" \ -Wl,-rpath,"$runtime_frameworks" \ "$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 \ - -framework Foundation -framework CoreImage -framework CoreGraphics \ + -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/host-tests" diff --git a/scripts/test-inspector.sh b/scripts/test-inspector.sh new file mode 100755 index 0000000..e891d28 --- /dev/null +++ b/scripts/test-inspector.sh @@ -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" diff --git a/tests/host_adapter_tests.m b/tests/host_adapter_tests.m index f996613..b9e9449 100644 --- a/tests/host_adapter_tests.m +++ b/tests/host_adapter_tests.m @@ -9,6 +9,7 @@ #import "PGParameterIDs.h" #import "PGFaceProcessor.h" #import "PGBeautyRenderer.h" +#import "PGInspectorState.h" #include // This adapter simulates host callbacks, but uses real IOSurface buffers and the @@ -28,6 +29,9 @@ @property BOOL rejectAnalysis; @property BOOL targetAtCurrentTime; @property UInt32 rejectedWriteParameter; +@property NSUInteger actionDepth; +@property NSUInteger actionStarts; +@property NSUInteger actionEnds; @end @implementation PGHostAPI @@ -73,6 +77,9 @@ - (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}]; } +- (BOOL)addCustomParameterWithName:(NSString *)name parameterID:(UInt32)identifier defaultValue:(NSObject *)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 { _depth++; return [self define:identifier kind:@"group" value:nil flags:flags extra:@{@"name":name}]; @@ -104,6 +111,17 @@ _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 *)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; } - (BOOL)startForwardAnalysis:(FxAnalysisLocation)location error:(NSError **)error { _forwardRequests++; return !_rejectAnalysis; @@ -393,10 +411,10 @@ static void TrackingCoordinateTests(NSDictionary *archive, NSDictionary *hostVal 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"); 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)); - Check(expectedSaved.length && MaximumDifference(trustedBytes,expectedSaved) <= 1, @"marked saved geometry matches independent exact-time archive replay"); - Check(MaximumDifference(trustedBytes,expectedLive) > 5, @"forged marked geometry is observably different, making the legacy non-replay assertion meaningful"); + 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(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. [legacy analyzeForward]; @@ -437,7 +455,7 @@ int main(int argc, const char *argv[]) { PGHostAPI *api = [PGHostAPI new]; beatygrilPlugIn *plugin = [[beatygrilPlugIn alloc] initWithAPIManager:api]; NSError *error = nil; 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]]); 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"); diff --git a/tests/inspector_tests.m b/tests/inspector_tests.m new file mode 100644 index 0000000..eab39a2 --- /dev/null +++ b/tests/inspector_tests.m @@ -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 *)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; +}}