dsh-pet-live2d 2.3.1 → 2.3.2

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,37 @@
1
1
  # 更新日志
2
2
 
3
+ ## 2.3.2
4
+
5
+ ### 修:摸尾巴又摸不到了(这次是真的)
6
+
7
+ 用户报的:"现在又摸不到尾巴了。"(还发了一张圈出尾巴位置的截图,帮了大忙 —— 尾巴是
8
+ **右侧那只鲸鱼尾鳍**,不是翅膀。)
9
+
10
+ **真正的根因**:判定每块 drawable 时用的是**映射时算好并存下来的下标**(`entry.index`),
11
+ 而不是**调用时按 id 现查的下标**。这两者只要有一处对不上,取到的顶点就是别人的 ——
12
+ 表现是"每块单独测都能命中,聚合起来一个都不中"。
13
+
14
+ 单块测(`drawableProbe`)之所以一直正常,正是因为它**每次都按 id 现查下标**。
15
+ 现在聚合路径也这么做了:`hitsPartsGeometry` 在循环里用 `im.getDrawableIndex(entry.id)`
16
+ 重新解一次,解不出来才退回缓存值。
17
+
18
+ 修好后同一网格上的读数:`hitsTail` 从 **0 → 509** 命中(48×48),头部 1113、重叠 689
19
+ —— 重叠仍然存在(翅膀的几何本来就伸到头顶上方),所以**摸头优先于摸尾巴**那条继续保留:
20
+ 点头部给摸头,尾巴自己的区域照样摸得到。
21
+
22
+ ### 一段该记下来的教训:坏掉的诊断比没有诊断更糟
23
+
24
+ 这轮我查了很久,很大一部分时间是被**自己写的诊断函数**骗了:`partHitCounts` 里调了
25
+ `api.drawableProbe(...)`,而 `api` 是控制器**返回出去**的对象,控制器内部根本看不到它
26
+ —— 每次调用都抛 `api is not defined`,被外面的 try/catch 吞掉,于是 `hitsAll`/`hitsVisible`
27
+ **恒为 0**。外面看到的却是"这个部件完全没有几何",一条**看起来像产品坏了**的读数。
28
+
29
+ 代价:我据此排除了三个正确假设(透明度过滤、下标空间、包围盒预筛),还改坏过一次
30
+ (把顶点整条换成 core 层,连头部判定都变成 0 命中,已还原)。
31
+
32
+ 教训:**诊断函数自己也要先自证**(拿一个已知能命中的样本去测它),读数与事实矛盾时
33
+ 先怀疑测量工具,再怀疑被测对象。
34
+
3
35
  ## 2.3.1
4
36
 
5
37
  ### 修:摸头会出摸尾巴的效果
package/lib/client.js CHANGED
@@ -307,18 +307,40 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
307
307
  */
308
308
  const drawableIndicesForParts = (parts) => {
309
309
  const raw = model?.internalModel?.coreModel?._model;
310
+ const im = model?.internalModel;
310
311
  const parent = raw?.drawables?.parentPartIndices;
311
312
  const partIds = raw?.parts?.ids;
312
- if (parent === undefined || partIds === undefined) return null;
313
+ const coreIds = raw?.drawables?.ids;
314
+ if (parent === undefined || partIds === undefined || coreIds === undefined) return null;
313
315
  const ids = Array.from(partIds).map(String);
314
316
  if (ids.length === 0 || parts.length === 0) return null;
315
317
  const wanted = new Set(parts);
318
+ const coreIndexById = new Map();
319
+ for (let i = 0; i < coreIds.length; i += 1) coreIndexById.set(String(coreIds[i]), i);
320
+ // **以包装层的顺序为主**:顶点就是按 `internalModel.getDrawableIDs()` 的顺序读的
321
+ // (单块 `drawableProbe` 走得通的正是这条路)。反过来"遍历原始表再翻译下标",
322
+ // 只要两套顺序有一处不一致,取到的顶点就是别的 drawable —— 表现就是"几何明明
323
+ // 又大又真,聚合判定一个都不中"。这里逐个 id 反查它属于哪个部件,不存在歧义。
324
+ const wrapperIds = typeof im?.getDrawableIDs === "function"
325
+ ? Array.from(im.getDrawableIDs()).map(String)
326
+ : null;
316
327
  const out = [];
317
- for (let i = 0; i < parent.length; i += 1) {
318
- const partIndex = parent[i];
319
- if (partIndex < 0 || partIndex >= ids.length) continue;
320
- if (!wanted.has(ids[partIndex])) continue;
321
- out.push({ index: i, part: partIndex });
328
+ if (wrapperIds !== null) {
329
+ for (let w = 0; w < wrapperIds.length; w += 1) {
330
+ const coreIndex = coreIndexById.get(wrapperIds[w]);
331
+ if (coreIndex === undefined) continue;
332
+ const partIndex = parent[coreIndex];
333
+ if (partIndex === undefined || partIndex < 0 || partIndex >= ids.length) continue;
334
+ if (!wanted.has(ids[partIndex])) continue;
335
+ out.push({ id: wrapperIds[w], index: w, coreIndex, part: partIndex });
336
+ }
337
+ } else {
338
+ for (let i = 0; i < parent.length; i += 1) {
339
+ const partIndex = parent[i];
340
+ if (partIndex < 0 || partIndex >= ids.length) continue;
341
+ if (!wanted.has(ids[partIndex])) continue;
342
+ out.push({ id: String(coreIds[i]), index: i, coreIndex: i, part: partIndex });
343
+ }
322
344
  }
323
345
  return out.length > 0 ? out : null;
324
346
  };
@@ -359,25 +381,30 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
359
381
  const hitsPartsGeometry = (indices, x, y) => {
360
382
  const im = model?.internalModel;
361
383
  if (im === undefined || im === null) return null;
362
- // 顶点在**包装层**、三角形索引在**core 层**(实测:`im` 有 getDrawableVertices,
363
- // `im.coreModel` 才有 getDrawableVertexIndices)—— 只查一个就会静默返回 null、
364
- // 悄悄退回方框判定。两边各取所长。
384
+ // 顶点用**包装层**的(core 的 `getDrawableVertices` 不在同一个坐标空间里:整条换成
385
+ // core 之后连头部判定都变成 0 命中)。三角形索引只有 core 有;两套按**同一个下标**
386
+ // 取用时是自洽的(单块 `drawableProbe` 用同一组合能命中,已实测)。
387
+ const core = im.coreModel;
365
388
  const verticesOf = typeof im.getDrawableVertices === "function"
366
389
  ? (index) => im.getDrawableVertices(index)
367
- : (typeof im.coreModel?.getDrawableVertices === "function"
368
- ? (index) => im.coreModel.getDrawableVertices(index)
369
- : null);
370
- const indicesOf = typeof im.getDrawableVertexIndices === "function"
371
- ? (index) => im.getDrawableVertexIndices(index)
372
- : (typeof im.coreModel?.getDrawableVertexIndices === "function"
373
- ? (index) => im.coreModel.getDrawableVertexIndices(index)
374
- : null);
390
+ : (typeof core?.getDrawableVertices === "function" ? (index) => core.getDrawableVertices(index) : null);
391
+ const indicesOf = typeof core?.getDrawableVertexIndices === "function"
392
+ ? (index) => core.getDrawableVertexIndices(index)
393
+ : (typeof im.getDrawableVertexIndices === "function" ? (index) => im.getDrawableVertexIndices(index) : null);
375
394
  if (verticesOf === null || indicesOf === null) return null;
376
395
  if (indices === null) return null;
377
396
  let read = false;
378
397
  try {
379
398
  for (const entry of indices) {
380
- const verts = verticesOf(entry.index);
399
+ // **调用时用 id 重新解一次下标**,别信缓存的 `entry.index`:
400
+ // 单块 `drawableProbe` 走得通、聚合路径走不通,两者剩下的唯一差别就是这个
401
+ // (它每次都用 id 现查,聚合路径用映射时算好并存下来的那份)。
402
+ let index = entry.index;
403
+ if (typeof entry.id === "string" && typeof im.getDrawableIndex === "function") {
404
+ const fresh = im.getDrawableIndex(entry.id);
405
+ if (fresh >= 0) index = fresh;
406
+ }
407
+ const verts = verticesOf(index);
381
408
  if (verts === undefined || verts === null || verts.length < 6) continue;
382
409
  read = true;
383
410
  // 先用这个 drawable 的包围盒排除(绝大多数部件一眼就出局,不用扫三角形)。
@@ -2114,10 +2141,130 @@ window.__ModuleLoader__.load({ id: "dsh-pet-live2d", factory: (require) => {
2114
2141
  box: maxX > minX ? { minX: Math.round(minX), maxX: Math.round(maxX), minY: Math.round(minY), maxY: Math.round(maxY) } : null,
2115
2142
  hitsAll,
2116
2143
  hitsVisible,
2144
+ // 每块报:id / 包装层下标 / 原始表下标。
2145
+ // **别再在这里调 `api.xxx`**:`api` 是控制器返回出去的那个对象,控制器内部
2146
+ // 看不到它 —— 调了就是 `api is not defined`,整个函数抛异常、所有读数变 0,
2147
+ // 而外面看起来像"这个部件完全没有几何"(我为此白查了三轮)。
2148
+ entries: entries.map((entry) => ({
2149
+ id: entry.id ?? null,
2150
+ index: entry.index,
2151
+ core: entry.coreIndex,
2152
+ part: entry.part,
2153
+ })),
2117
2154
  });
2118
2155
  }
2119
2156
  return out;
2120
2157
  },
2158
+ /**
2159
+ * Diagnostic: 全部 drawable 的 id / 所属部件名 / 顶点数 / 包围盒(模型空间)。
2160
+ *
2161
+ * 找"她身上真正在画尾巴的那一个"用这个:作者给**部件**起了中文名(尾巴翅膀/猫尾/
2162
+ * 大翅膀…),但 drawable 本身多半叫 `ArtMesh123`。按包围盒的位置就能认出来
2163
+ * (尾巴在身体下后方、翅膀在两侧)。
2164
+ */
2165
+ drawableTable: () => {
2166
+ const im = model?.internalModel;
2167
+ const raw = im?.coreModel?._model;
2168
+ const ids = typeof im?.getDrawableIDs === "function" ? Array.from(im.getDrawableIDs()).map(String) : [];
2169
+ const partIds = raw?.parts?.ids === undefined ? [] : Array.from(raw.parts.ids).map(String);
2170
+ const parent = raw?.drawables?.parentPartIndices;
2171
+ const cdi3Names = MANIFEST.current?.partNames ?? {};
2172
+ return ids.map((id, index) => {
2173
+ const partIndex = parent === undefined ? -1 : parent[index];
2174
+ const partId = partIndex >= 0 && partIndex < partIds.length ? partIds[partIndex] : null;
2175
+ let minX = Infinity;
2176
+ let minY = Infinity;
2177
+ let maxX = -Infinity;
2178
+ let maxY = -Infinity;
2179
+ let vertexCount = 0;
2180
+ try {
2181
+ const verts = im?.getDrawableVertices?.(index);
2182
+ if (verts !== undefined && verts !== null) {
2183
+ vertexCount = Math.floor(verts.length / 2);
2184
+ for (let k = 0; k < verts.length; k += 2) {
2185
+ if (verts[k] < minX) minX = verts[k];
2186
+ if (verts[k] > maxX) maxX = verts[k];
2187
+ if (verts[k + 1] < minY) minY = verts[k + 1];
2188
+ if (verts[k + 1] > maxY) maxY = verts[k + 1];
2189
+ }
2190
+ }
2191
+ } catch {
2192
+ /* 跳过 */
2193
+ }
2194
+ return {
2195
+ id,
2196
+ index,
2197
+ partId,
2198
+ partName: partId === null ? "(无部件)" : (cdi3Names[partId] ?? partId),
2199
+ vertexCount,
2200
+ box: maxX > minX ? { minX: Math.round(minX), maxX: Math.round(maxX), minY: Math.round(minY), maxY: Math.round(maxY) } : null,
2201
+ };
2202
+ });
2203
+ },
2204
+ /**
2205
+ * Diagnostic: 单看一个 drawable 的三角面判定为什么命中/不命中。
2206
+ *
2207
+ * 头部判定好用、尾巴判定一个都不中(而它的几何明明又大又真)——这种"同一段代码
2208
+ * 对不同 drawable 表现不同"的问题,只能把中间量摊开看:顶点数、索引数、索引范围、
2209
+ * 以及用**它自己的重心**去测的结果。
2210
+ */
2211
+ drawableProbe: (id) => {
2212
+ const im = model?.internalModel;
2213
+ const index = typeof im?.getDrawableIndex === "function" ? im.getDrawableIndex(id) : -1;
2214
+ if (index < 0) return { id, found: false };
2215
+ const vertices = im?.getDrawableVertices?.(index);
2216
+ const indices = (typeof im?.getDrawableVertexIndices === "function"
2217
+ ? im.getDrawableVertexIndices(index)
2218
+ : im?.coreModel?.getDrawableVertexIndices?.(index));
2219
+ const verts = vertices === undefined || vertices === null ? [] : Array.from(vertices);
2220
+ const idx = indices === undefined || indices === null ? [] : Array.from(indices);
2221
+ let minIndex = Infinity;
2222
+ let maxIndex = -Infinity;
2223
+ for (const value of idx) {
2224
+ if (value < minIndex) minIndex = value;
2225
+ if (value > maxIndex) maxIndex = value;
2226
+ }
2227
+ const vertexCount = Math.floor(verts.length / 2);
2228
+ // 用顶点的平均位置当查询点:它一定在几何内部(凸的情况下)。
2229
+ let sumX = 0;
2230
+ let sumY = 0;
2231
+ for (let i = 0; i < verts.length; i += 2) {
2232
+ sumX += verts[i];
2233
+ sumY += verts[i + 1];
2234
+ }
2235
+ const cx = vertexCount === 0 ? 0 : sumX / vertexCount;
2236
+ const cy = vertexCount === 0 ? 0 : sumY / vertexCount;
2237
+ let triangles = 0;
2238
+ let degenerate = 0;
2239
+ let contains = 0;
2240
+ for (let i = 0; i + 2 < idx.length; i += 3) {
2241
+ triangles += 1;
2242
+ const a = idx[i];
2243
+ const b = idx[i + 1];
2244
+ const c = idx[i + 2];
2245
+ if (a * 2 + 1 >= verts.length || b * 2 + 1 >= verts.length || c * 2 + 1 >= verts.length) continue;
2246
+ const area = (verts[b * 2] - verts[a * 2]) * (verts[c * 2 + 1] - verts[a * 2 + 1])
2247
+ - (verts[c * 2] - verts[a * 2]) * (verts[b * 2 + 1] - verts[a * 2 + 1]);
2248
+ if (Math.abs(area) < 1e-6) {
2249
+ degenerate += 1;
2250
+ continue;
2251
+ }
2252
+ if (pointInTriangle(cx, cy, verts, a, b, c)) contains += 1;
2253
+ }
2254
+ return {
2255
+ id,
2256
+ found: true,
2257
+ index,
2258
+ vertexCount,
2259
+ indexCount: idx.length,
2260
+ minIndex: Number.isFinite(minIndex) ? minIndex : null,
2261
+ maxIndex: Number.isFinite(maxIndex) ? maxIndex : null,
2262
+ triangles,
2263
+ degenerate,
2264
+ centroidHits: contains,
2265
+ centroid: { x: Math.round(cx), y: Math.round(cy) },
2266
+ };
2267
+ },
2121
2268
  /**
2122
2269
  * Play a motion with its declared policy applied; used by the panel, the
2123
2270
  * tap reaction and the session-phase driver.
package/lib/index.js CHANGED
@@ -356,6 +356,31 @@ function readReactionList(value) {
356
356
  return Array.isArray(value) ? value.filter((name) => typeof name === 'string' && name !== '') : []
357
357
  }
358
358
 
359
+ /**
360
+ * 部件 id → 作者给的中文名(`Part46` → `脸蛋`)。
361
+ *
362
+ * 诊断用(`drawableTable()`):把 drawable 归到有意义的部件名下,才能一眼看出
363
+ * "她身上真正在画尾巴的是哪个 drawable" —— 光看 `ArtMesh123` 是认不出来的。
364
+ */
365
+ function readPartNames(dir, modelPath) {
366
+ try {
367
+ const base = modelPath.replace(/\.model3\.json$/i, '')
368
+ const file = findCdi3(dir, base)
369
+ if (file === undefined) return {}
370
+ const parts = readJson(file)?.Parts
371
+ if (!Array.isArray(parts)) return {}
372
+ const out = {}
373
+ for (const part of parts) {
374
+ if (part === null || typeof part !== 'object') continue
375
+ if (typeof part.Id !== 'string' || typeof part.Name !== 'string') continue
376
+ if (part.Name !== '') out[part.Id] = part.Name
377
+ }
378
+ return out
379
+ } catch {
380
+ return {}
381
+ }
382
+ }
383
+
359
384
  /** Scan one pet directory into a catalog entry, or undefined when unusable. */
360
385
  export function scanPet(dir, id) {
361
386
  const manifestFile = join(dir, 'pet.json')
@@ -512,6 +537,8 @@ export function scanPet(dir, id) {
512
537
  // 空数组 = 没 cdi3 或没挑到,浏览器半区退回旧行为。
513
538
  headParts: readPartsMatching(dir, modelPath, HEAD_PART_NAME_HINTS),
514
539
  tailParts: readPartsMatching(dir, modelPath, TAIL_PART_NAME_HINTS),
540
+ // 部件 id → 中文名(诊断用,见客户端 drawableTable())。
541
+ partNames: readPartNames(dir, modelPath),
515
542
  // 台词:宠物自己的声音(问候 / 点击 / 摸头 / 摸尾巴 / 转晕 / 归位 / 每个相位)。
516
543
  // 用户能在设置里逐条改,改过的存浏览器;这里是**默认值**。
517
544
  lines: typeof block.lines === 'object' && block.lines !== null ? block.lines : {},
@@ -719,6 +746,7 @@ function catalogRoute() {
719
746
  motionGuards: pet.motionGuards,
720
747
  headParts: pet.headParts,
721
748
  tailParts: pet.tailParts,
749
+ partNames: pet.partNames,
722
750
  lines: pet.lines,
723
751
  patReactions: pet.patReactions,
724
752
  tailReactions: pet.tailReactions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-pet-live2d",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "Live2D 桌宠插件:为 DSH Web GUI 挂一只可拖动、会跟随鼠标、能换动作与表情的 Live2D 宠物。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",