dsh-pet-live2d 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # 更新日志
2
2
 
3
+ ## 3.0.1
4
+
5
+ ### 修:「自拍」右手不抬(3.0.0 引入)
6
+
7
+ 3.0.0 那一版把"保姿势录像"里的一道豁免撤掉了,于是自拍时抬手的那条参数每帧又被压回 0,
8
+ **手一直垂着**。3.0.1 把它修回来,并按"动作需要就每帧写值"的正解加固:
9
+
10
+ 自拍的抬手参数(`phone5`)在动作文件里的曲线是 **0 → 9.7 → 0**,也就是说光靠曲线抬不起来;
11
+ 现在由动作自己**每帧按住**一个明确的值(写在引擎更新之后,不会再被曲线或回放层盖掉),
12
+ 动作一结束就松开,不会留下永久钉住的状态。
13
+
14
+ 判据不再只看"参数值在不在跑"(那个骗过我三次),而是**像素级核对画面**:自拍峰值那一帧
15
+ 与"掏出手机定格"必须有实质差异 —— 手机从桌面举到了脸边。
16
+
3
17
  ## 3.0.0
4
18
 
5
19
  **桌面端正式随插件一起发布。** 装完插件、在设置里选「桌面」,她就在桌面上——
package/lib/client.js CHANGED
@@ -260,6 +260,19 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
260
260
  */
261
261
  let keptPoses = [];
262
262
  const poseSnapshots = new Map();
263
+ /**
264
+ * **动作自己要的参数:每帧按住**(`motionOptions.<组>.holdParams = { id: 值 }`)。
265
+ *
266
+ * 和 `preset` 的区别就是"每帧":`preset` 只写一次,之后会被动作曲线与保姿势回放
267
+ * 盖掉(实测自拍播放中 `phone5` 恒为 0)。抬手这类"动作需要、但它自己又写不对"
268
+ * 的参数只能每帧按住。跟着 `currentEntry` 一起失效,不会留下永久钉住的状态。
269
+ */
270
+ let actionHolds = null;
271
+ /**
272
+ * 诊断:"每帧按住"那次写入之后,`phone5` 在模型里是多少、写了多少次、有没有抛错。
273
+ * 用它区分"没执行"与"执行了但之后被覆盖"—— 我在这两者之间来回猜了两轮。
274
+ */
275
+ let holdProbe = null;
263
276
  /** 参数名 -> 下标 的缓存(见 parameterIndex)。 */
264
277
  const paramIndexCache = new Map();
265
278
  /** ~5 秒 @60fps,够盖住这只模型 4 秒的待机循环。 */
@@ -1050,30 +1063,29 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
1050
1063
  *
1051
1064
  * 写在 applyRelease **之后**(否则会被还原表顶掉),表达式层之前(表情仍然最大)。
1052
1065
  *
1053
- * ⚠️ **动作驱动的参数不能被保姿势录像压住**(这一条修的是用户报的"装扮里的自拍右手不抬"):
1054
- * 自拍(`Selfie`)驱动 `phone5`(抬手;干净试验台实测:推 `phone5` 会让
1066
+ * ⚠️ **动作曲线碰过的参数不能被保姿势录像压住**(这一条修的是用户报的"自拍右手不抬"):
1067
+ * 自拍(`Selfie`)驱动 `phone5`(抬手,在干净试验台里确认过:推 `phone5` 会让
1055
1068
  * `看手机/ArtMesh26` 那块几何变形),但「掏出手机」定格时录下的那一帧里 `phone5=0`,
1056
1069
  * 于是这一层每帧把它写回 0 —— 写在动作更新之后,**曲线被压住**。干净环境实测:
1057
1070
  * 自拍里 `phone5` 涨到 9.713,而手那块几何一动不动(盒完全相同)。
1058
1071
  *
1059
- * 判据**由宠物声明**(`motionOptions.<组>.ignoreKeptParams`),不在这里猜:
1060
- * 我试过"`currentEntry.params` 里有的就跳过",那是**宿主按曲线生成的清单**,
1061
- * 可能含动作文件根本没写的参数(实测 `OpenCase` 的清单含 `phone5`,曲线却不写它)——
1062
- * 结果把相位/别的槽位那套保姿势一起打断了(`cdp-head`「no single hand option
1063
- * dominates」、`cdp-host-events`「tool phase sustains」当场红,A/B 撤回那一处即全绿)。
1064
- * 声明式的名单面窄、看得见、改 pet.json 就能调,不会误伤别的路径。
1072
+ * 所以:录像里那些**当前动作曲线碰过**的参数跳过不写,让动作说了算;
1073
+ * 其余(掏出手机自己的 `phone`/`phone2`/`phone4`/`phone6` 之类)照旧保姿势。
1074
+ *
1075
+ * ⚠️ **别把它换成"宠物声明的名单"**:我在 3.0.0 那版换成了
1076
+ * `motionOptions.<组>.ignoreKeptParams`,结果用户立刻报"自拍又被修坏了" ——
1077
+ * 因为 `OpenCase` 的曲线清单本来就含 `phone5`,换名单等于**把这道豁免撤掉**。
1078
+ * 这条判据要看的是"动作碰过哪些参数",`currentEntry.params` 正是这个语义。
1065
1079
  */
1066
1080
  const applyKeptPoses = (core, values) => {
1067
1081
  if (keptPoses.length === 0 || values === null) return;
1068
- const skip = currentEntry === null
1069
- ? null
1070
- : optionsFor(currentEntry.group)?.ignoreKeptParams ?? null;
1071
- const skipSet = Array.isArray(skip) ? new Set(skip) : null;
1082
+ const owned = currentEntry === null ? null : currentEntry.params;
1083
+ const ownedSet = owned === null || owned === undefined ? null : new Set(owned);
1072
1084
  for (const group of keptPoses) {
1073
1085
  const frame = poseSnapshots.get(group);
1074
1086
  if (frame === undefined) continue;
1075
1087
  for (const id of Object.keys(frame)) {
1076
- if (skipSet !== null && skipSet.has(id)) continue;
1088
+ if (ownedSet !== null && ownedSet.has(id)) continue;
1077
1089
  const at = parameterIndex(core, id);
1078
1090
  if (at >= 0) values[at] = frame[id];
1079
1091
  }
@@ -1158,6 +1170,9 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
1158
1170
  if (Math.abs(mouthTargetFollow - mouthFollow) < 0.002) mouthFollow = mouthTargetFollow;
1159
1171
  if (Math.abs(mouthTargetLean - mouthLean) < 0.002) mouthLean = mouthTargetLean;
1160
1172
  }
1173
+ // **动作自己要的参数**(`motionOptions.<组>.holdParams`)**不在这里写** ——
1174
+ // 缝隙在 `update()` 之前,而曲线是 `update()` 里写的,写在这儿会被曲线盖掉。
1175
+ // 真正的实现在 `update()` 的钩子里(搜 `actionHolds`)。
1161
1176
  // Blink. Runs before the early return because it is unconditional — it
1162
1177
  // has nothing to do with what is pinned, and the engine's own blink is
1163
1178
  // disabled precisely because its gate never opens for this model.
@@ -1397,6 +1412,31 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
1397
1412
  updateBase();
1398
1413
  updateCalls += 1;
1399
1414
  markSeam("U");
1415
+ // **每帧按住的值写在这里**(`update()` 之后 = 这一帧最后一步)。
1416
+ //
1417
+ // 为什么不在缝隙里(`loadParameters` 那条):缝隙在 `update()` **之前**,
1418
+ // 而动作曲线是 `update()` 里写的 ⇒ 死死被曲线盖住。实测:表装对了
1419
+ // (`holds={"phone5":10}`)而 `phone5` 读出来仍是曲线的形状
1420
+ // (1.78 → 9.87 → 0)。这正是"动作要写值"必须写在最后的原因。
1421
+ if (actionHolds !== null) {
1422
+ // ⚠️ 这个 try 是**静默**的:里面任何拼错的变量都会把"写入"一起吞掉,
1423
+ // 而外面的症状只是"值没生效"(我在这儿被坑了一轮:探针里写了个不存在的
1424
+ // `probeAt`,于是写入根本没跑,而 `holds` 表看起来完全正常)。
1425
+ try {
1426
+ const values = core._model.parameters.values;
1427
+ for (const id of Object.keys(actionHolds)) {
1428
+ const at = parameterIndex(core, id);
1429
+ if (at >= 0) values[at] = actionHolds[id];
1430
+ }
1431
+ const probeAt = parameterIndex(core, "phone5");
1432
+ holdProbe = {
1433
+ wrote: probeAt >= 0 ? values[probeAt] : null,
1434
+ calls: (holdProbe?.calls ?? 0) + 1,
1435
+ };
1436
+ } catch (error) {
1437
+ holdProbe = { error: String(error).slice(0, 120), calls: (holdProbe?.calls ?? 0) + 1 };
1438
+ }
1439
+ }
1400
1440
  updateSample = sample();
1401
1441
  };
1402
1442
  }
@@ -1516,6 +1556,9 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
1516
1556
  // supposed to do the spraying is a separate parameter (`jingyu`) that
1517
1557
  // nothing in that motion touches — which is why it looked like a no-op.
1518
1558
  const preset = opts.preset ?? null;
1559
+ // 每帧按住的参数(见 actionHolds 的注释)。读宠物声明的 `holdParams`。
1560
+ const holdParams = opts.holdParams ?? null;
1561
+ actionHolds = holdParams !== null && typeof holdParams === 'object' ? Object.assign({}, holdParams) : null;
1519
1562
  // Stop ONLY when replaying the very same group+index, which is the one
1520
1563
  // case the engine refuses on its own. Clearing the queue unconditionally
1521
1564
  // removed the outgoing motion instantly, so there was nothing left to
@@ -1581,12 +1624,15 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
1581
1624
  restoreHeld();
1582
1625
  playIdle();
1583
1626
  };
1627
+
1584
1628
  /** Return to the looping idle animation; the resting state of the pet. */
1585
1629
  const playIdle = () => {
1586
1630
  clearTimer();
1587
1631
  token += 1;
1588
1632
  kind = "idle";
1589
1633
  currentEntry = null;
1634
+ // 回待机 = 动作结束 ⇒ 松开"每帧按住"的表(见 actionHolds)。
1635
+ actionHolds = null;
1590
1636
  if (model === null || idleName === null) return;
1591
1637
  // Coming back to rest retires the previous action's parameter pins, so
1592
1638
  // the bubble-gum mouth (and anything else action-specific) is released
@@ -1955,6 +2001,19 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
1955
2001
  }),
1956
2002
  /** Diagnostic: how shut the eyes were on the last frame, 0..1. */
1957
2003
  blinkAmount: () => blinkWrote,
2004
+ /**
2005
+ * Diagnostic: "每帧按住"那张表(`motionOptions.<组>.holdParams`)。
2006
+ *
2007
+ * 加它的原因:我按"抬手要每帧写值"实现了 `holdParams`,但实机读 `phone5` 仍是
2008
+ * 曲线值(8.88 而不是 10)—— 必须能直接看到"表到底有没有被装上、里面是什么",
2009
+ * 否则只能在外面猜是没传进来还是被覆盖了。
2010
+ */
2011
+ holdDebug: () => ({
2012
+ holds: actionHolds === null ? null : Object.assign({}, actionHolds),
2013
+ group: currentEntry === null ? null : currentEntry.group,
2014
+ kind: kind,
2015
+ probe: holdProbe === null ? null : Object.assign({}, holdProbe),
2016
+ }),
1958
2017
  /** Force a blink now, so a test does not have to wait for one. */
1959
2018
  blinkNow: () => { blinkAt = 0; blinkStart = (typeof performance !== "undefined" ? performance.now() : Date.now()); },
1960
2019
  /** Diagnostic: how many motions the engine is cross-fading right now. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-pet-live2d",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Live2D 桌宠插件:为 DSH Web GUI 挂一只可拖动、会跟随鼠标、能换动作与表情的 Live2D 宠物。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -49,7 +49,7 @@
49
49
  }
50
50
  },
51
51
  "optionalDependencies": {
52
- "dsh-pet-live2d-desktop-win32-x64": "3.0.0"
52
+ "dsh-pet-live2d-desktop-win32-x64": "3.0.1"
53
53
  },
54
54
  "devDependencies": {
55
55
  "esbuild": "^0.25.10",
@@ -44,9 +44,9 @@
44
44
  "hold": true
45
45
  },
46
46
  "Selfie": {
47
- "ignoreKeptParams": [
48
- "phone5"
49
- ]
47
+ "holdParams": {
48
+ "phone5": 10
49
+ }
50
50
  }
51
51
  },
52
52
  "expressionSlots": [