dsh-plugin-vrmpet3d 0.1.1 → 0.1.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.
@@ -91,6 +91,20 @@
91
91
  zh: {
92
92
  // ---- the chrome, and the name each page gives itself ----
93
93
  titleModel: 'vrmpet3d · 模型参数',
94
+ gazePitchUp: '最大抬头角度', gazePitchDown: '最大低头角度', gazePitchTest: '测试 20° / 30°',
95
+ gazeCurveMode: '曲线模式', gazeAxes: '分轴曲线', gazeRadial: '二维距离曲线',
96
+ gazeSpatial: '空间角度', gazeCurveSpatial: '空间角度曲率', gazeDistance: '注视距离',
97
+ gazeSpatialHint: '距离以满偏距离为单位,越大转头越缓。同高时,鼠标越偏向左右,俯仰越小;与双眼同高时俯仰为零。默认曲率 1、注视距离 0.1。',
98
+ gazeSpanHint: '满偏距离填 0 为自动:水平和垂直均使用屏幕宽度的一半。',
99
+ gazeCurveX: '水平曲率', gazeCurveY: '垂直曲率', gazeCurveRadial: '二维距离曲率',
100
+ gazeCurveHint: '数值越小,中心附近越灵敏;1 为线性。二维距离模式按满偏距离归一化后计算方向与强度,可减少远处光标上下穿过视线水平线时的俯仰跳变。各模式的参数分别保存。',
101
+ gazePitchHint: '自动为抬头、低头各 60°。范围 0–60°;0 表示不向该方向转头。这是视线跟踪的附加角度,动作本身的头部姿势仍会保留。',
102
+ toonTitle: '卡通增强', toonState: '材质状态', toonOriginal: '模型原始', toonReset: '恢复模型原始参数',
103
+ toonNote: '参数仅保存到当前模型,留空使用模型原始值。关闭 MToon 后使用普通光照材质。描边倍率只调整模型已有的描边。',
104
+ toonLoading: '正在读取当前模型材质…', toonUnsupported: '当前模型未声明 MToon 材质,暂不支持转换。',
105
+ toonStatus: '{n}/{total} 个原生 MToon 材质 · 当前{state}', toonEnabled: '开启', toonDisabled: '关闭',
106
+ toonToony: '明暗分界硬度', toonShift: '明暗分界偏移', toonShade: '阴影强度', toonOutline: '描边倍率',
107
+ toonMixed: '模型原始(各材质不同)', toonNoOutline: '模型没有启用描边',
94
108
  titleGlobal: 'vrmpet3d · 通用设置',
95
109
  headingModel: '模型参数',
96
110
  headingGlobal: '通用设置',
@@ -417,6 +431,20 @@
417
431
  en: {
418
432
  // ---- the chrome, and the name each page gives itself ----
419
433
  titleModel: 'vrmpet3d · Model parameters',
434
+ gazePitchUp: 'Maximum upward pitch', gazePitchDown: 'Maximum downward pitch', gazePitchTest: 'Try 20° / 30°',
435
+ gazeCurveMode: 'Curve mode', gazeAxes: 'Separate axes', gazeRadial: '2D distance',
436
+ gazeSpatial: 'Spatial angles', gazeCurveSpatial: 'Spatial angle curve', gazeDistance: 'Target distance',
437
+ gazeSpatialHint: 'Distance is in span units; larger values soften head turns. At a fixed height, moving sideways reduces pitch. At eye level, pitch is zero. Defaults: curve 1, target distance 0.1.',
438
+ gazeSpanHint: 'Enter 0 for automatic spans: both axes use half the screen width.',
439
+ gazeCurveX: 'Horizontal curve', gazeCurveY: 'Vertical curve', gazeCurveRadial: '2D distance curve',
440
+ gazeCurveHint: 'Lower values increase sensitivity near the center; 1 is linear. The 2D mode uses distance normalized by each axis span, reducing abrupt pitch changes as a distant pointer crosses eye level. Each mode keeps its own values.',
441
+ gazePitchHint: 'Automatic: 60° up and down. Range: 0–60°. Zero disables tracking in that direction. Gaze adds these angles while preserving the animation’s head pose.',
442
+ toonTitle: 'Cartoon enhancement', toonState: 'Material status', toonOriginal: 'Model original', toonReset: 'Restore original materials',
443
+ toonNote: 'Saved for this model only. Leave a value blank to use its original. Turning MToon off uses standard lit materials. Outline scale adjusts existing outlines only.',
444
+ toonLoading: 'Reading model materials…', toonUnsupported: 'This model has no MToon materials. Conversion is not supported yet.',
445
+ toonStatus: '{n}/{total} native MToon materials · Currently {state}', toonEnabled: 'on', toonDisabled: 'off',
446
+ toonToony: 'Shading hardness', toonShift: 'Shading shift', toonShade: 'Shade strength', toonOutline: 'Outline scale',
447
+ toonMixed: 'Original (varies by material)', toonNoOutline: 'This model has no enabled outlines',
420
448
  titleGlobal: 'vrmpet3d · General settings',
421
449
  headingModel: 'Model parameters',
422
450
  headingGlobal: 'General settings',
@@ -1523,6 +1551,7 @@
1523
1551
  }
1524
1552
 
1525
1553
  function renderModel() {
1554
+ renderToon();
1526
1555
  var spec = element('model-spec');
1527
1556
  var meta = element('model-meta');
1528
1557
  if (!spec || !meta) return;
@@ -1553,6 +1582,67 @@
1553
1582
  // ---------------------------------------------------------------------------
1554
1583
  // Expressions
1555
1584
  // ---------------------------------------------------------------------------
1585
+ function renderToon() {
1586
+ var card = element('toon-card');
1587
+ if (!card) return;
1588
+ var id = modelIdInUse();
1589
+ var info = modelInfo && modelInfoId === id && modelInfo.toon;
1590
+ var materials = info && info.materials || [];
1591
+ var supported = Boolean(id && materials.length);
1592
+ var prefs = (preferences.toon || {})[id] || {};
1593
+ var mode = element('toon-mode');
1594
+ var state = element('toon-state');
1595
+ state.textContent = !info ? (id ? t('toonLoading') : t('noModelChosen')) : !supported ? t('toonUnsupported')
1596
+ : t('toonStatus').replace('{n}', materials.length).replace('{total}', info.total)
1597
+ .replace('{state}', t(prefs.enabled === false ? 'toonDisabled' : 'toonEnabled'));
1598
+ mode.value = prefs.enabled === undefined ? 'original' : prefs.enabled ? 'on' : 'off';
1599
+ mode.disabled = !supported;
1600
+ function save(key, value) {
1601
+ if (id !== modelIdInUse()) return;
1602
+ var all = Object.assign({}, preferences.toon || {});
1603
+ var next = Object.assign({}, all[id] || {});
1604
+ if (value === undefined) delete next[key]; else next[key] = value;
1605
+ if (Object.keys(next).length) all[id] = next; else delete all[id];
1606
+ preferences.toon = core.cleanPreferences({ toon: all }).toon;
1607
+ persist();
1608
+ renderToon();
1609
+ }
1610
+ mode.onchange = function () { save('enabled', mode.value === 'original' ? undefined : mode.value === 'on'); };
1611
+ var reset = element('toon-reset');
1612
+ reset.disabled = !id || !Object.keys(prefs).length;
1613
+ reset.onclick = function () {
1614
+ var all = Object.assign({}, preferences.toon || {});
1615
+ delete all[id]; preferences.toon = all; persist(); renderToon();
1616
+ };
1617
+ var controls = element('toon-controls');
1618
+ controls.textContent = '';
1619
+ [
1620
+ ['shadingToonyFactor', 'toonToony', 0, 1, 0.01],
1621
+ ['shadingShiftFactor', 'toonShift', -1, 1, 0.01],
1622
+ ['shadeStrength', 'toonShade', 0, 1, 0.05],
1623
+ ['outlineScale', 'toonOutline', 0, 3, 0.05],
1624
+ ].forEach(function (spec) {
1625
+ var row = document.createElement('div'); row.className = 'row';
1626
+ var label = document.createElement('label'); label.className = 'k'; label.htmlFor = 'toon-' + spec[0]; label.textContent = t(spec[1]);
1627
+ var input = document.createElement('input'); input.type = 'number'; input.id = label.htmlFor;
1628
+ input.min = spec[2]; input.max = spec[3]; input.step = spec[4];
1629
+ input.value = prefs[spec[0]] === undefined ? '' : prefs[spec[0]];
1630
+ var original = materials.map(function (m) { return m[spec[0]]; });
1631
+ input.placeholder = spec[0] === 'shadeStrength' || spec[0] === 'outlineScale' ? '1'
1632
+ : original.length && original.every(function (v) { return v === original[0]; }) ? String(Number(original[0].toFixed(2))) : t('toonMixed');
1633
+ var noOutline = spec[0] === 'outlineScale' && !materials.some(function (m) { return m.outline; });
1634
+ input.disabled = !supported || prefs.enabled === false || noOutline;
1635
+ input.onchange = function () {
1636
+ if (input.value !== '' && !Number.isFinite(Number(input.value))) return;
1637
+ save(spec[0], input.value === '' ? undefined : Number(input.value));
1638
+ };
1639
+ row.appendChild(label); row.appendChild(input);
1640
+ var note = document.createElement('span'); note.className = 'note';
1641
+ note.textContent = noOutline ? t('toonNoOutline') : spec[2] + ' … ' + spec[3]; row.appendChild(note);
1642
+ controls.appendChild(row);
1643
+ });
1644
+ }
1645
+
1556
1646
  function expressionRows() {
1557
1647
  if (!modelInfo || !Array.isArray(modelInfo.expressions)) return [];
1558
1648
  return modelInfo.expressions.slice().sort(function (left, right) {
@@ -4336,16 +4426,17 @@
4336
4426
  * Both are SETTINGS rather than constants in the renderer (they were `GAZE_CURVE_EDGE_SLOPE` and a fixed
4337
4427
  * span), because the right shape depends on the display and on the person: on a wide monitor the default
4338
4428
  * span makes the head turn very little, and anyone who wants it livelier lowers the curve's edge slope
4339
- * instead of reaching for a different pet. `0` in either span field means AUTO — the frame's own width —
4429
+ * instead of reaching for a different pet. `0` in either span field means AUTO — half the screen width —
4340
4430
  * and the hint under the row says which number that resolves to for the floating window.
4341
4431
  */
4342
4432
  function bindGazeControls() {
4343
- var curve = element('pref-gaze-curve');
4344
- var curveValue = element('pref-gaze-curve-value');
4345
- var curveReset = element('pref-gaze-curve-reset');
4433
+ var curveMode = element('pref-gaze-mode');
4346
4434
  var spanX = element('pref-gaze-span-x');
4347
4435
  var spanY = element('pref-gaze-span-y');
4348
4436
  var spanReset = element('pref-gaze-span-reset');
4437
+ var pitchUp = element('pref-gaze-pitch-up');
4438
+ var pitchDown = element('pref-gaze-pitch-down');
4439
+ var pitchReset = element('pref-gaze-pitch-reset');
4349
4440
  var idle = element('pref-gaze-idle');
4350
4441
  var idleReset = element('pref-gaze-idle-reset');
4351
4442
  // The draught's four numbers live in the same section — one place for everything that answers "how does the
@@ -4366,9 +4457,20 @@
4366
4457
  persist();
4367
4458
  };
4368
4459
  var showCurve = function () {
4369
- var slope = Number(preferences.gazeCurve);
4370
- if (curveValue) curveValue.textContent = slope.toFixed(2) + (slope >= 0.995 ? t('straightLine') : '');
4371
- if (curve) curve.value = String(Math.round(slope * 100));
4460
+ if (curveMode) curveMode.value = preferences.gazeCurveMode;
4461
+ var radial = element('gaze-curves-radial');
4462
+ if (radial) radial.hidden = preferences.gazeCurveMode !== 'radial';
4463
+ var spatial = element('gaze-curves-spatial');
4464
+ if (spatial) spatial.hidden = preferences.gazeCurveMode !== 'spatial';
4465
+ var distance = element('pref-gaze-distance');
4466
+ if (distance) distance.value = String(preferences.gazeDistance);
4467
+ [['radial', 'gazeCurveRadial'], ['spatial', 'gazeCurveSpatial']].forEach(function (entry) {
4468
+ var control = element('pref-gaze-curve-' + entry[0]);
4469
+ var value = element('pref-gaze-curve-' + entry[0] + '-value');
4470
+ var current = preferences[entry[1]];
4471
+ if (control) control.value = String(Math.round(current * 100));
4472
+ if (value) value.textContent = current.toFixed(2) + (current >= 0.995 ? t('straightLine') : '');
4473
+ });
4372
4474
  };
4373
4475
  var showSpans = function () {
4374
4476
  if (spanX) spanX.value = String(Math.round(Number(preferences.gazeSpanX) || 0));
@@ -4397,18 +4499,21 @@
4397
4499
  if (windSpeed) windSpeed.value = String(Math.round(Number(preferences.windSpeedForMax)));
4398
4500
  };
4399
4501
 
4400
- if (curve) {
4401
- curve.addEventListener('input', function () {
4402
- write('gazeCurve', Number(curve.value) / 100);
4403
- showCurve();
4404
- });
4405
- }
4406
- if (curveReset) {
4407
- curveReset.addEventListener('click', function () {
4408
- write('gazeCurve', core.DEFAULTS.gazeCurve);
4409
- showCurve();
4410
- });
4411
- }
4502
+ if (curveMode) curveMode.onchange = function () { write('gazeCurveMode', curveMode.value === 'radial' ? 'radial' : 'spatial'); showCurve(); };
4503
+ [['radial', 'gazeCurveRadial'], ['spatial', 'gazeCurveSpatial']].forEach(function (entry) {
4504
+ var control = element('pref-gaze-curve-' + entry[0]);
4505
+ var reset = element('pref-gaze-curve-' + entry[0] + '-reset');
4506
+ if (control) control.oninput = function () { write(entry[1], Number(control.value) / 100); showCurve(); };
4507
+ if (reset) reset.onclick = function () { write(entry[1], core.DEFAULTS[entry[1]]); showCurve(); };
4508
+ });
4509
+ var targetDistance = element('pref-gaze-distance');
4510
+ var targetDistanceReset = element('pref-gaze-distance-reset');
4511
+ if (targetDistance) targetDistance.onchange = function () {
4512
+ var value = targetDistance.valueAsNumber;
4513
+ write('gazeDistance', Number.isFinite(value) ? Math.max(0.1, Math.min(5, value)) : core.DEFAULTS.gazeDistance);
4514
+ showCurve();
4515
+ };
4516
+ if (targetDistanceReset) targetDistanceReset.onclick = function () { write('gazeDistance', core.DEFAULTS.gazeDistance); showCurve(); };
4412
4517
  if (spanX) {
4413
4518
  spanX.addEventListener('change', function () {
4414
4519
  write('gazeSpanX', Math.max(0, Number(spanX.value) || 0));
@@ -4495,6 +4600,24 @@
4495
4600
  });
4496
4601
  }
4497
4602
  showCurve();
4603
+ function showPitch() {
4604
+ if (pitchUp) pitchUp.value = String(Math.round(preferences.gazePitchUp * 10) / 10);
4605
+ if (pitchDown) pitchDown.value = String(Math.round(preferences.gazePitchDown * 10) / 10);
4606
+ }
4607
+ [[pitchUp, 'gazePitchUp'], [pitchDown, 'gazePitchDown']].forEach(function (entry) {
4608
+ if (!entry[0]) return;
4609
+ entry[0].onchange = function () {
4610
+ var value = entry[0].valueAsNumber;
4611
+ write(entry[1], Number.isFinite(value) ? Math.max(0, Math.min(60, value)) : core.DEFAULTS[entry[1]]);
4612
+ showPitch();
4613
+ };
4614
+ });
4615
+ function setPitch(up, down) {
4616
+ preferences.gazePitchUp = up; preferences.gazePitchDown = down;
4617
+ persist(); showPitch();
4618
+ }
4619
+ if (pitchReset) pitchReset.onclick = function () { setPitch(core.DEFAULTS.gazePitchUp, core.DEFAULTS.gazePitchDown); };
4620
+ showPitch();
4498
4621
  showSpans();
4499
4622
  showIdle();
4500
4623
  showWind();
@@ -4533,7 +4656,8 @@
4533
4656
  // work area, and the floating pet's horizontal full deflection IS that width.
4534
4657
  var auto = element('pref-gaze-span-auto');
4535
4658
  if (auto) {
4536
- var work = status.display && status.display.workArea ? Math.round(status.display.workArea.width) : 0;
4659
+ var work = status.page && status.page.gaze && Number.isFinite(status.page.gaze.autoSpan) ? Math.round(status.page.gaze.autoSpan) :
4660
+ status.display && status.display.workArea ? Math.round(status.display.workArea.width / 2) : 0;
4537
4661
  auto.textContent = work > 0 ? fill(t('spanNow'), { px: work }) : t('noScreenSize');
4538
4662
  }
4539
4663
  showAppliedGaze(status);
@@ -4564,6 +4688,8 @@
4564
4688
  } else {
4565
4689
  live.appliedGaze = {
4566
4690
  curve: Number(gaze.curve),
4691
+ curveY: Number.isFinite(gaze.curveY) ? gaze.curveY : Number(gaze.curve),
4692
+ mode: gaze.mode === 'radial' ? 'radial' : 'spatial',
4567
4693
  spanX: Math.round(Number(gaze.spanX) || 0),
4568
4694
  spanY: Math.round(Number(gaze.spanY) || 0),
4569
4695
  aim: { x: Number((gaze.aim && gaze.aim.x) || 0), y: Number((gaze.aim && gaze.aim.y) || 0) },
@@ -4581,7 +4707,9 @@
4581
4707
  }
4582
4708
  var aim = live.appliedGaze;
4583
4709
  host.textContent = fill(t('gazeApplied'), {
4584
- curve: aim.curve.toFixed(2),
4710
+ curve: aim.mode === 'spatial' ? t('gazeSpatial') + ' ' + aim.curve.toFixed(2)
4711
+ : aim.mode === 'radial' ? t('gazeRadial') + ' ' + aim.curve.toFixed(2)
4712
+ : t('gazeAxes') + ' ' + aim.curve.toFixed(2) + ' / ' + aim.curveY.toFixed(2),
4585
4713
  spanX: aim.spanX,
4586
4714
  spanY: aim.spanY,
4587
4715
  aimX: aim.aim.x.toFixed(2),
@@ -5052,10 +5180,10 @@
5052
5180
  * which it is. The resolved spans are what "auto" currently means, so the number is checkable.
5053
5181
  */
5054
5182
  gaze: {
5055
- controls: Boolean(element('pref-gaze-curve') && element('pref-gaze-span-x') && element('pref-gaze-span-y')),
5056
- curve: Number(preferences.gazeCurve),
5057
- spanX: core.resolveGazeSpan(preferences.gazeSpanX, 0),
5058
- spanY: core.resolveGazeSpan(preferences.gazeSpanY, 0),
5183
+ controls: Boolean(element('pref-gaze-mode') && element('pref-gaze-curve-spatial') && element('pref-gaze-curve-radial') && element('pref-gaze-span-x') && element('pref-gaze-span-y')),
5184
+ curve: Number(preferences.gazeCurveMode === 'radial' ? preferences.gazeCurveRadial : preferences.gazeCurveSpatial),
5185
+ spanX: core.resolveGazeSpan(preferences.gazeSpanX, Number(globalThis.screen && globalThis.screen.width) || 0),
5186
+ spanY: core.resolveGazeSpan(preferences.gazeSpanY, Number(globalThis.screen && globalThis.screen.width) || 0),
5059
5187
  // The readout of what the RENDERER applies, and the numbers behind it: `applied` is null until the
5060
5188
  // first /vrmpet3d/desktop answer lands, which is why a check asserts the element exists rather than a
5061
5189
  // value that depends on a running window.