dsh-plugin-vrmpet3d 0.1.0 → 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.
- package/README.md +2 -0
- package/desktop/main.js +47 -4
- package/desktop/preload.js +7 -2
- package/lib/client.core.js +487 -53
- package/lib/client.js +391 -57
- package/lib/client.menu.js +14 -4
- package/lib/client.scene.js +134 -7
- package/lib/config-page.html +82 -45
- package/lib/config-page.js +1271 -252
- package/lib/core.js +487 -53
- package/lib/desktop-page.js +298 -44
- package/lib/desktop.js +18 -1
- package/lib/global-page.html +172 -101
- package/lib/index.js +196 -58
- package/lib/menu.js +14 -4
- package/lib/model-picker.js +139 -49
- package/lib/picker-page.html +34 -8
- package/lib/picker-page.js +192 -11
- package/lib/scene.js +134 -7
- package/lib/setup-page.html +68 -49
- package/lib/setup-page.js +395 -42
- package/lib/vrm-info.js +16 -1
- package/lib/whale.js +191 -0
- package/package.json +1 -1
package/lib/scene.js
CHANGED
|
@@ -67,10 +67,20 @@
|
|
|
67
67
|
var GAZE_YAW_LIMIT = ((80 * Math.PI) / 180) / GAZE_WEIGHT;
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
70
|
+
* Automatic pitch limit, before the gaze weight. Settings expose separate actual
|
|
71
|
+
* upward/downward angles in degrees; both default to 60 degrees.
|
|
72
72
|
*/
|
|
73
|
-
var GAZE_PITCH_LIMIT =
|
|
73
|
+
var GAZE_PITCH_LIMIT = (60 * Math.PI / 180) / GAZE_WEIGHT;
|
|
74
|
+
|
|
75
|
+
function gazePitchLimits(preferences) {
|
|
76
|
+
var prefs = preferences || {};
|
|
77
|
+
function limit(key) {
|
|
78
|
+
return Number.isFinite(prefs[key])
|
|
79
|
+
? Math.max(0, Math.min(60, prefs[key])) * Math.PI / 180 / GAZE_WEIGHT
|
|
80
|
+
: GAZE_PITCH_LIMIT;
|
|
81
|
+
}
|
|
82
|
+
return { up: limit('gazePitchUp'), down: limit('gazePitchDown') };
|
|
83
|
+
}
|
|
74
84
|
|
|
75
85
|
/**
|
|
76
86
|
* How far the head may turn IN TOTAL while an authored clip is driving it, in radians.
|
|
@@ -786,7 +796,8 @@
|
|
|
786
796
|
}
|
|
787
797
|
|
|
788
798
|
var yaw = Math.max(-GAZE_YAW_LIMIT, Math.min(GAZE_YAW_LIMIT, aim.yaw)) * GAZE_WEIGHT * this.gazeBlend;
|
|
789
|
-
var
|
|
799
|
+
var pitchLimits = gazePitchLimits(this.scene && this.scene.preferences);
|
|
800
|
+
var pitch = Math.max(-pitchLimits.down, Math.min(pitchLimits.up, aim.pitch)) * GAZE_WEIGHT * this.gazeBlend;
|
|
790
801
|
var axes = rest ? this.gazeAxes(head) : null;
|
|
791
802
|
|
|
792
803
|
if (layeredOnClip) {
|
|
@@ -799,8 +810,7 @@
|
|
|
799
810
|
var budget = Math.max(0, HEAD_LAYER_LIMIT - clipTurn);
|
|
800
811
|
yaw = Math.max(-budget, Math.min(budget, yaw));
|
|
801
812
|
// Pitch keeps its own (smaller) limit, but never more than what is left of the budget either.
|
|
802
|
-
pitch = Math.max(-Math.min(
|
|
803
|
-
Math.min(Math.min(GAZE_PITCH_LIMIT * GAZE_WEIGHT, budget), pitch));
|
|
813
|
+
pitch = Math.max(-budget, Math.min(budget, pitch));
|
|
804
814
|
if (axes) {
|
|
805
815
|
this._quat.setFromAxisAngle(axes.yaw, yaw);
|
|
806
816
|
this._quat2.setFromAxisAngle(axes.pitch, pitch);
|
|
@@ -1669,6 +1679,9 @@
|
|
|
1669
1679
|
if (this.disposed) return;
|
|
1670
1680
|
var w = Math.max(1, Math.round(width));
|
|
1671
1681
|
var h = Math.max(1, Math.round(height));
|
|
1682
|
+
// Layout works in fractional CSS pixels. Reapplying the same rounded size clears
|
|
1683
|
+
// the framebuffer between rAFs when a settings reply arrives during an orbit.
|
|
1684
|
+
if (this.width === w && this.height === h) return;
|
|
1672
1685
|
this.renderer.setSize(w, h, false);
|
|
1673
1686
|
this.camera.aspect = w / h;
|
|
1674
1687
|
this.camera.updateProjectionMatrix();
|
|
@@ -1755,6 +1768,8 @@
|
|
|
1755
1768
|
|
|
1756
1769
|
/** Replace the current avatar with a hardware-ready one. */
|
|
1757
1770
|
Scene.prototype.installAvatar = function (avatar, root) {
|
|
1771
|
+
(this.toonMaterials || []).forEach(function (entry) { if (entry.plain) entry.plain.dispose(); });
|
|
1772
|
+
this.toonMaterials = null;
|
|
1758
1773
|
// Stop before the old avatar is replaced: stopping is what undoes the clip's
|
|
1759
1774
|
// pose, and it has to happen while the old model is still installed. Every
|
|
1760
1775
|
// clip is then dropped, because a clip is fitted to one model — the cached
|
|
@@ -1779,6 +1794,7 @@
|
|
|
1779
1794
|
this.frameAvatar(root);
|
|
1780
1795
|
this.stage.add(root);
|
|
1781
1796
|
this.avatar = avatar;
|
|
1797
|
+
this.captureToonMaterials(root);
|
|
1782
1798
|
this.kind = avatar.kind;
|
|
1783
1799
|
/**
|
|
1784
1800
|
* …and the collider switch, applied to the avatar that was just adopted.
|
|
@@ -1844,6 +1860,7 @@
|
|
|
1844
1860
|
* model is loaded, so on a fresh pet that call finds nothing to snapshot and the first sweep would have
|
|
1845
1861
|
* had no gravity to lean against (measured: `windState().joints === 0` on a real model with two chains).
|
|
1846
1862
|
*/
|
|
1863
|
+
this.applySprings(this.preferences);
|
|
1847
1864
|
this.captureSpringBase();
|
|
1848
1865
|
this.expressionBehaviour = { blink: 0, blinkExpression: 'blink', emotion: null, emotionWeight: 0 };
|
|
1849
1866
|
this.expressionState = { nextBlinkAt: null, blinkUntil: 0, nextIdleEmotionAt: null, emotionUntil: 0 };
|
|
@@ -1852,6 +1869,73 @@
|
|
|
1852
1869
|
this.motion.setGazeIdle(this.preferences.gazeIdleSeconds);
|
|
1853
1870
|
this.updateShadowPlane();
|
|
1854
1871
|
this.refitCamera();
|
|
1872
|
+
this.applyToonPreferences();
|
|
1873
|
+
};
|
|
1874
|
+
|
|
1875
|
+
/** Keep the loaded materials (and expression bindings) intact so the switch is reversible. */
|
|
1876
|
+
Scene.prototype.captureToonMaterials = function (root) {
|
|
1877
|
+
var self = this;
|
|
1878
|
+
this.toonMeshes = [];
|
|
1879
|
+
this.toonMaterials = [];
|
|
1880
|
+
var seen = new Map();
|
|
1881
|
+
root.traverse(function (node) {
|
|
1882
|
+
if (!node.isMesh || !node.material) return;
|
|
1883
|
+
var originals = Array.isArray(node.material) ? node.material : [node.material];
|
|
1884
|
+
var entries = originals.map(function (material) {
|
|
1885
|
+
if (!material.isMToonMaterial) return null;
|
|
1886
|
+
if (seen.has(material)) return seen.get(material);
|
|
1887
|
+
var entry = { material: material, toony: material.shadingToonyFactor, shift: material.shadingShiftFactor,
|
|
1888
|
+
shade: material.shadeColorFactor.clone(), width: material.outlineWidthFactor, plain: null };
|
|
1889
|
+
seen.set(material, entry);
|
|
1890
|
+
self.toonMaterials.push(entry);
|
|
1891
|
+
return entry;
|
|
1892
|
+
});
|
|
1893
|
+
if (entries.some(Boolean)) self.toonMeshes.push({ node: node, original: node.material, entries: entries, originals: originals });
|
|
1894
|
+
});
|
|
1895
|
+
};
|
|
1896
|
+
|
|
1897
|
+
Scene.prototype.applyToonPreferences = function () {
|
|
1898
|
+
var self = this;
|
|
1899
|
+
var id = this.currentModelId || this.preferences.modelId;
|
|
1900
|
+
var prefs = (this.preferences.toon || {})[id] || {};
|
|
1901
|
+
var disabled = prefs.enabled === false;
|
|
1902
|
+
(this.toonMaterials || []).forEach(function (entry) {
|
|
1903
|
+
var material = entry.material;
|
|
1904
|
+
material.shadingToonyFactor = prefs.shadingToonyFactor === undefined ? entry.toony : prefs.shadingToonyFactor;
|
|
1905
|
+
material.shadingShiftFactor = prefs.shadingShiftFactor === undefined ? entry.shift : prefs.shadingShiftFactor;
|
|
1906
|
+
material.shadeColorFactor.copy(entry.shade);
|
|
1907
|
+
if (prefs.shadeStrength !== undefined) material.shadeColorFactor.lerp(material.color, 1 - prefs.shadeStrength);
|
|
1908
|
+
material.outlineWidthFactor = entry.width * (prefs.outlineScale === undefined ? 1 : prefs.outlineScale);
|
|
1909
|
+
if (disabled && !entry.plain) {
|
|
1910
|
+
var plain = new self.THREE.MeshStandardMaterial();
|
|
1911
|
+
['map', 'alphaMap', 'alphaTest', 'transparent', 'opacity', 'side', 'depthWrite', 'depthTest',
|
|
1912
|
+
'normalMap', 'normalScale', 'emissive', 'emissiveMap', 'emissiveIntensity', 'color'].forEach(function (key) {
|
|
1913
|
+
if (material[key] === undefined) return;
|
|
1914
|
+
if (plain[key] && typeof plain[key].copy === 'function') plain[key].copy(material[key]);
|
|
1915
|
+
else plain[key] = material[key];
|
|
1916
|
+
});
|
|
1917
|
+
plain.visible = !material.isOutline;
|
|
1918
|
+
entry.plain = plain;
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
(this.toonMeshes || []).forEach(function (mesh) {
|
|
1922
|
+
mesh.node.material = disabled
|
|
1923
|
+
? (Array.isArray(mesh.original) ? mesh.originals.map(function (m, i) { return mesh.entries[i] ? mesh.entries[i].plain : m; })
|
|
1924
|
+
: mesh.entries[0].plain)
|
|
1925
|
+
: mesh.original;
|
|
1926
|
+
});
|
|
1927
|
+
this.toonDisabled = disabled;
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
Scene.prototype.syncPlainMaterials = function () {
|
|
1931
|
+
if (!this.toonDisabled) return;
|
|
1932
|
+
(this.toonMaterials || []).forEach(function (entry) {
|
|
1933
|
+
if (!entry.plain) return;
|
|
1934
|
+
// Expressions still target the authored MToon material, including UV animation.
|
|
1935
|
+
entry.plain.color.copy(entry.material.color);
|
|
1936
|
+
entry.plain.emissive.copy(entry.material.emissive);
|
|
1937
|
+
entry.plain.opacity = entry.material.opacity;
|
|
1938
|
+
});
|
|
1855
1939
|
};
|
|
1856
1940
|
|
|
1857
1941
|
/** A soft ground blob, so the pet does not appear to float. */
|
|
@@ -3380,7 +3464,8 @@
|
|
|
3380
3464
|
var x = Math.max(-1, Math.min(1, aim.x));
|
|
3381
3465
|
var y = Math.max(-1, Math.min(1, aim.y));
|
|
3382
3466
|
// Screen y grows downward, pitch grows upward.
|
|
3383
|
-
|
|
3467
|
+
var limits = gazePitchLimits(this.preferences);
|
|
3468
|
+
return { yaw: x * GAZE_YAW_LIMIT, pitch: -y * (y < 0 ? limits.up : limits.down) };
|
|
3384
3469
|
};
|
|
3385
3470
|
|
|
3386
3471
|
/** Advance one frame. `dt` is in seconds. */
|
|
@@ -3437,6 +3522,7 @@
|
|
|
3437
3522
|
this.advanceExpressions(dt);
|
|
3438
3523
|
this.applyExpressions(this.preferences);
|
|
3439
3524
|
|
|
3525
|
+
this.syncPlainMaterials();
|
|
3440
3526
|
this.renderer.render(this.scene, this.camera);
|
|
3441
3527
|
this.frame += 1;
|
|
3442
3528
|
};
|
|
@@ -3448,6 +3534,7 @@
|
|
|
3448
3534
|
Scene.prototype.applyPreferences = function (preferences) {
|
|
3449
3535
|
var raw = preferences && typeof preferences === 'object' ? preferences : {};
|
|
3450
3536
|
this.preferences = this.core.cleanPreferences(preferences);
|
|
3537
|
+
this.applyToonPreferences();
|
|
3451
3538
|
// A preferences push only carries `motions` when the caller meant to change
|
|
3452
3539
|
// it. cleanPreferences always produces a (possibly empty) key, so adopting it
|
|
3453
3540
|
// unconditionally would silently wipe a map set through setMotionMap.
|
|
@@ -3641,6 +3728,40 @@
|
|
|
3641
3728
|
};
|
|
3642
3729
|
};
|
|
3643
3730
|
|
|
3731
|
+
/** The rendered eyes' midpoint, in canvas CSS pixels, independent of framing and camera orbit. */
|
|
3732
|
+
Scene.prototype.gazeOrigin = function () {
|
|
3733
|
+
var avatar = this.avatar;
|
|
3734
|
+
if (!avatar || !this.THREE || !this.camera) return null;
|
|
3735
|
+
var point = this._gazeOriginVec || (this._gazeOriginVec = new this.THREE.Vector3());
|
|
3736
|
+
var other = this._gazeOtherEyeVec || (this._gazeOtherEyeVec = new this.THREE.Vector3());
|
|
3737
|
+
var humanoid = avatar.vrm && avatar.vrm.humanoid;
|
|
3738
|
+
var left = humanoid && typeof humanoid.getRawBoneNode === 'function' ? humanoid.getRawBoneNode('leftEye') : null;
|
|
3739
|
+
var right = humanoid && typeof humanoid.getRawBoneNode === 'function' ? humanoid.getRawBoneNode('rightEye') : null;
|
|
3740
|
+
left = left || (avatar.bones && avatar.bones.leftEye);
|
|
3741
|
+
right = right || (avatar.bones && avatar.bones.rightEye);
|
|
3742
|
+
var source;
|
|
3743
|
+
if (left && right) {
|
|
3744
|
+
left.getWorldPosition(point); right.getWorldPosition(other);
|
|
3745
|
+
point.add(other).multiplyScalar(0.5);
|
|
3746
|
+
source = 'eyes';
|
|
3747
|
+
} else if (avatar.vrm && avatar.vrm.lookAt && typeof avatar.vrm.lookAt.getLookAtWorldPosition === 'function') {
|
|
3748
|
+
// The loader has already converted VRM 0/1 coordinate conventions for this offset.
|
|
3749
|
+
if (avatar.root) avatar.root.updateWorldMatrix(true, true);
|
|
3750
|
+
avatar.vrm.lookAt.getLookAtWorldPosition(point);
|
|
3751
|
+
source = 'lookAt';
|
|
3752
|
+
} else {
|
|
3753
|
+
var head = gazeNodeFor(avatar);
|
|
3754
|
+
if (!head) return null;
|
|
3755
|
+
head.getWorldPosition(point);
|
|
3756
|
+
source = 'head';
|
|
3757
|
+
}
|
|
3758
|
+
this.camera.updateMatrixWorld(true);
|
|
3759
|
+
var projected = this.projectToCanvas(point);
|
|
3760
|
+
if (!projected || projected.z < -1 || projected.z > 1) return null;
|
|
3761
|
+
projected.source = source;
|
|
3762
|
+
return projected;
|
|
3763
|
+
};
|
|
3764
|
+
|
|
3644
3765
|
/**
|
|
3645
3766
|
* Apply the draught to the chains, decay it, and keep it LOCAL.
|
|
3646
3767
|
*
|
|
@@ -3865,6 +3986,10 @@
|
|
|
3865
3986
|
*/
|
|
3866
3987
|
Scene.prototype.setSpringDefaults = function (defaults) {
|
|
3867
3988
|
this.springDefaults = defaults && typeof defaults === 'object' ? defaults : null;
|
|
3989
|
+
// Catalog defaults can arrive after the avatar and preferences on either surface.
|
|
3990
|
+
// Apply them now; waiting for another preference edit makes a reload appear to fix physics.
|
|
3991
|
+
this.applySprings(this.preferences);
|
|
3992
|
+
this.captureSpringBase();
|
|
3868
3993
|
};
|
|
3869
3994
|
|
|
3870
3995
|
/**
|
|
@@ -4931,6 +5056,8 @@
|
|
|
4931
5056
|
|
|
4932
5057
|
Scene.prototype.dispose = function () {
|
|
4933
5058
|
if (this.disposed) return;
|
|
5059
|
+
(this.toonMeshes || []).forEach(function (mesh) { mesh.node.material = mesh.original; });
|
|
5060
|
+
(this.toonMaterials || []).forEach(function (entry) { if (entry.plain) entry.plain.dispose(); });
|
|
4934
5061
|
this.disposed = true;
|
|
4935
5062
|
// Stop and uncache every mixer before the renderer goes: a mixer holds a
|
|
4936
5063
|
// binding table that references the model's bones, so leaving them behind
|
package/lib/setup-page.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>vrmpet3d · 初次安装引导</title>
|
|
6
|
+
<title data-i18n="pageTitle">vrmpet3d · 初次安装引导</title>
|
|
7
7
|
<meta name="color-scheme" content="dark light">
|
|
8
8
|
<!--
|
|
9
9
|
The first-run guide: its screens and a finish, on its own route (`/vrmpet3d/setup`). Which of the six the flow
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
<body data-page="setup">
|
|
50
50
|
<header class="page-header">
|
|
51
51
|
<div class="eyebrow">VRMPET3D · SETUP</div>
|
|
52
|
-
<h1>初次安装引导</h1>
|
|
53
|
-
<div id="status" role="status" aria-live="polite">正在读取…</div>
|
|
52
|
+
<h1 data-i18n="guideTitle">初次安装引导</h1>
|
|
53
|
+
<div id="status" role="status" aria-live="polite" data-i18n="reading">正在读取…</div>
|
|
54
54
|
</header>
|
|
55
55
|
|
|
56
56
|
<!--
|
|
@@ -62,29 +62,44 @@
|
|
|
62
62
|
step that is not there and a gap where one never was. The words live here and the policy in the attributes; both
|
|
63
63
|
files are read together by `verify-desktop.mjs`.
|
|
64
64
|
-->
|
|
65
|
-
<ol class="setup-steps" id="setup-steps" aria-label="引导进度">
|
|
66
|
-
<li data-screen="setup-0">欢迎</li>
|
|
67
|
-
<li data-screen="setup-1">偏好模型</li>
|
|
68
|
-
<li data-screen="setup-2" data-desktop-only="1">桌宠界面</li>
|
|
69
|
-
<li data-screen="setup-3">AI 碎碎念</li>
|
|
70
|
-
<li data-screen="setup-4" data-hidden="1">模型动作</li>
|
|
71
|
-
<li data-screen="setup-5">完成</li>
|
|
65
|
+
<ol class="setup-steps" id="setup-steps" aria-label="引导进度" data-i18n-aria="stepsAria">
|
|
66
|
+
<li data-screen="setup-0" data-i18n="stepWelcome">欢迎</li>
|
|
67
|
+
<li data-screen="setup-1" data-i18n="stepModel">偏好模型</li>
|
|
68
|
+
<li data-screen="setup-2" data-desktop-only="1" data-i18n="stepSurface">桌宠界面</li>
|
|
69
|
+
<li data-screen="setup-3" data-i18n="stepSpeech">AI 碎碎念</li>
|
|
70
|
+
<li data-screen="setup-4" data-hidden="1" data-i18n="stepMotions">模型动作</li>
|
|
71
|
+
<li data-screen="setup-5" data-i18n="stepDone">完成</li>
|
|
72
72
|
</ol>
|
|
73
73
|
|
|
74
74
|
<main>
|
|
75
75
|
<!-- ① 欢迎 -->
|
|
76
76
|
<section class="settings-section setup-screen" id="setup-0">
|
|
77
|
-
<h2>欢迎,也先说清楚风险</h2>
|
|
77
|
+
<h2 data-i18n="welcomeTitle">欢迎,也先说清楚风险</h2>
|
|
78
78
|
<div class="card">
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
<!--
|
|
80
|
+
界面语言 sits ABOVE the risk note (operator, 2026-10-15: 「在初始安装引导页的第一页欢迎页面中加上语言选择」):
|
|
81
|
+
a reader who cannot read the paragraphs picks the language before reading them. It is the one choice on this
|
|
82
|
+
page that is written and applied the moment it changes rather than at 完成 — see `wireChoices` in setup-page.js.
|
|
83
|
+
|
|
84
|
+
The paragraphs below are single-line on purpose: `core.applyI18n` rewrites their whole `innerHTML`, and the
|
|
85
|
+
markup's own text is the no-JS fallback a parity check compares against the zh half of the table.
|
|
86
|
+
-->
|
|
87
|
+
<div class="row">
|
|
88
|
+
<label class="k" for="setup-language" data-i18n="language">界面语言</label>
|
|
89
|
+
<select id="setup-language">
|
|
90
|
+
<option value="auto" data-i18n="languageAuto">自动</option>
|
|
91
|
+
<option value="zh" data-i18n="languageZh">中文</option>
|
|
92
|
+
<option value="en" data-i18n="languageEn">English</option>
|
|
93
|
+
</select>
|
|
94
|
+
</div>
|
|
95
|
+
<p data-i18n-html="welcomeRisk">这是一个<strong>早期版本</strong>的插件,作者也是第一次开发 DSH 插件。它<strong>还没有经过系统性的验证</strong>,可能存在 bug,也可能带来性能下降;设置页面的排版也可能比较混乱。</p>
|
|
96
|
+
<p class="setup-warning" data-i18n-html="welcomeWarning">如果你在使用中遇到任何性能问题,请<strong>第一时间禁用这个插件</strong>,再慢慢排查。</p>
|
|
82
97
|
</div>
|
|
83
98
|
</section>
|
|
84
99
|
|
|
85
100
|
<!-- ② 偏好模型 -->
|
|
86
101
|
<section class="settings-section setup-screen" id="setup-1" hidden>
|
|
87
|
-
<h2>偏好模型</h2>
|
|
102
|
+
<h2 data-i18n="stepModel">偏好模型</h2>
|
|
88
103
|
<!--
|
|
89
104
|
The picker itself is `lib/model-picker.js`, mounted here and mounted again on the 切换模型 page
|
|
90
105
|
(`/vrmpet3d/picker`, opened from either pet's right-click menu). The operator, 2026-10-13:
|
|
@@ -100,13 +115,13 @@
|
|
|
100
115
|
|
|
101
116
|
<!-- ③ 桌宠界面 -->
|
|
102
117
|
<section class="settings-section setup-screen" id="setup-2" hidden>
|
|
103
|
-
<h2>桌宠出现在哪里</h2>
|
|
118
|
+
<h2 data-i18n="surfaceTitle">桌宠出现在哪里</h2>
|
|
104
119
|
<div class="card">
|
|
105
120
|
<div class="setup-choice" id="setup-surface">
|
|
106
121
|
<label><input type="radio" name="setup-surface" value="auto">
|
|
107
|
-
<strong>自动</strong><span class="note">Desktop端自动开启独立桌宠</span></label>
|
|
122
|
+
<strong data-i18n="surfaceAuto">自动</strong><span class="note" data-i18n="surfaceAutoNote">Desktop端自动开启独立桌宠</span></label>
|
|
108
123
|
<label><input type="radio" name="setup-surface" value="app">
|
|
109
|
-
<strong>只在内置桌宠</strong></label>
|
|
124
|
+
<strong data-i18n="surfaceApp">只在内置桌宠</strong></label>
|
|
110
125
|
</div>
|
|
111
126
|
<p class="hint" id="setup-surface-note"></p>
|
|
112
127
|
</div>
|
|
@@ -119,36 +134,33 @@
|
|
|
119
134
|
所以这里说的是**本次真正会用的那个目录**,不是一句泛泛的话。
|
|
120
135
|
-->
|
|
121
136
|
<div class="card">
|
|
122
|
-
<p><strong>浮窗会借用一份 DSH 自带的运行时。</strong>独立浮窗需要无边框、置顶、点击穿透的系统窗口,这只有 Electron
|
|
123
|
-
|
|
124
|
-
<strong>硬链接</strong>指向 DSH 自己的那批客户端文件 —— 于是它看起来就是一份没改动过的 DSH。</p>
|
|
125
|
-
<p><strong>硬链接与 DSH 共享同一份数据,不额外占盘</strong>(新增的只有一个小 app 壳),并且它<strong>必须与 DSH 同盘</strong>:
|
|
126
|
-
硬链接不能跨卷。删掉这个目录是安全的,下次启用浮窗会重新建一份。</p>
|
|
137
|
+
<p data-i18n-html="runtimeBorrow"><strong>浮窗会借用一份 DSH 自带的运行时。</strong>独立浮窗需要无边框、置顶、点击穿透的系统窗口,这只有 Electron 能给,而插件不自带(那会多出两百多兆)。所以启用浮窗时,启动器会在 <strong>DSH 安装目录所在的分区</strong>里建一个运行时目录,用 <strong>硬链接</strong>指向 DSH 自己的那批客户端文件 —— 于是它看起来就是一份没改动过的 DSH。</p>
|
|
138
|
+
<p data-i18n-html="runtimeLinks"><strong>硬链接与 DSH 共享同一份数据,不额外占盘</strong>(新增的只有一个小 app 壳),并且它<strong>必须与 DSH 同盘</strong>:硬链接不能跨卷。删掉这个目录是安全的,下次启用浮窗会重新建一份。</p>
|
|
127
139
|
<p class="hint" id="setup-runtime-path"></p>
|
|
128
140
|
</div>
|
|
129
141
|
</section>
|
|
130
142
|
|
|
131
143
|
<!-- ④ AI 碎碎念 -->
|
|
132
144
|
<section class="settings-section setup-screen" id="setup-3" hidden>
|
|
133
|
-
<h2>AI 碎碎念</h2>
|
|
145
|
+
<h2 data-i18n="speechTitle">AI 碎碎念</h2>
|
|
134
146
|
<div class="card">
|
|
135
147
|
<div class="row">
|
|
136
|
-
<label class="k" for="setup-speech-enabled">开启碎碎念</label>
|
|
148
|
+
<label class="k" for="setup-speech-enabled" data-i18n="speechEnabled">开启碎碎念</label>
|
|
137
149
|
<input type="checkbox" id="setup-speech-enabled">
|
|
138
150
|
</div>
|
|
139
151
|
<div class="setup-choice" id="setup-speech-source">
|
|
140
152
|
<label><input type="radio" name="setup-speech-source" value="builtin">
|
|
141
|
-
<strong>内置台词</strong><span class="note">内置台词基本已经够用</span></label>
|
|
153
|
+
<strong data-i18n="speechBuiltin">内置台词</strong><span class="note" data-i18n="speechBuiltinNote">内置台词基本已经够用</span></label>
|
|
142
154
|
<label><input type="radio" name="setup-speech-source" value="ai">
|
|
143
|
-
<strong>AI 生成</strong></label>
|
|
155
|
+
<strong data-i18n="speechAi">AI 生成</strong></label>
|
|
144
156
|
</div>
|
|
145
157
|
<div class="row" id="setup-ai-row">
|
|
146
|
-
<label class="k" for="setup-ai-provider">AI 来源</label>
|
|
158
|
+
<label class="k" for="setup-ai-provider" data-i18n="speechProvider">AI 来源</label>
|
|
147
159
|
<select id="setup-ai-provider"></select>
|
|
148
160
|
<select id="setup-ai-model"></select>
|
|
149
161
|
</div>
|
|
150
162
|
<!-- Shown with the row above: the AI's own words are configured on the OTHER page, in the same 碎碎念 section. -->
|
|
151
|
-
<p class="hint" id="setup-ai-note" hidden>人格信息和不同状态的提示词,都在 通用设置 → 碎碎念 里设置。</p>
|
|
163
|
+
<p class="hint" id="setup-ai-note" hidden data-i18n="speechAiNote">人格信息和不同状态的提示词,都在 通用设置 → 碎碎念 里设置。</p>
|
|
152
164
|
<p class="hint" id="setup-speech-note"></p>
|
|
153
165
|
</div>
|
|
154
166
|
<!--
|
|
@@ -161,12 +173,8 @@
|
|
|
161
173
|
is about the SOFTWARE, this one about what the pet SAYS and what the plugin touches.
|
|
162
174
|
-->
|
|
163
175
|
<div class="card" id="setup-speech-disclaimer">
|
|
164
|
-
<p><strong>碎碎念只是玩笑,请勿当真。</strong
|
|
165
|
-
|
|
166
|
-
<p>插件<strong>不会对你的项目或文件做任何实际操作</strong>(不执行命令、不改动代码),只读取会话的<strong>运行状态</strong>
|
|
167
|
-
(进行中 / 等待确认 / 完成 等)来决定表情和动作,会话文字、工具参数与附件都不会被它读取;你导入的模型或动作,
|
|
168
|
-
只会被复制到插件自己的目录里。<strong>它也不会干扰 DSH 本身的运行</strong>,且不带任何运行时依赖。不过在配置较差的
|
|
169
|
-
机器上,3D 渲染与弹簧骨物理可能带来轻微的性能影响 —— 关掉桌宠,或把尺寸调小一些,就能缓解。</p>
|
|
176
|
+
<p data-i18n-html="speechDisclaimerA"><strong>碎碎念只是玩笑,请勿当真。</strong>这些话要么来自内置台词,要么由你配置的模型即兴生成,都属于角色扮演:不是宠物的真实判断,也不构成任何建议。</p>
|
|
177
|
+
<p data-i18n-html="speechDisclaimerB">插件<strong>不会对你的项目或文件做任何实际操作</strong>(不执行命令、不改动代码),只读取会话的<strong>运行状态</strong>(进行中 / 等待确认 / 完成 等)来决定表情和动作,会话文字、工具参数与附件都不会被它读取;你导入的模型或动作,只会被复制到插件自己的目录里。<strong>它也不会干扰 DSH 本身的运行</strong>,且不带任何运行时依赖。不过在配置较差的机器上,3D 渲染与弹簧骨物理可能带来轻微的性能影响 —— 关掉桌宠,或把尺寸调小一些,就能缓解。</p>
|
|
170
178
|
</div>
|
|
171
179
|
</section>
|
|
172
180
|
|
|
@@ -177,17 +185,17 @@
|
|
|
177
185
|
where the same import lives for anyone who does want it.
|
|
178
186
|
-->
|
|
179
187
|
<section class="settings-section setup-screen" id="setup-4" hidden>
|
|
180
|
-
<h2>模型动作</h2>
|
|
188
|
+
<h2 data-i18n="motionsTitle">模型动作</h2>
|
|
181
189
|
<div class="card">
|
|
182
190
|
<div class="setup-actions">
|
|
183
|
-
<button id="setup-motion-pick" class="primary">选择动作文件夹…</button>
|
|
184
|
-
<button id="setup-motion-manual" class="ghost">手动输入路径</button>
|
|
191
|
+
<button id="setup-motion-pick" class="primary" data-i18n="motionsPick">选择动作文件夹…</button>
|
|
192
|
+
<button id="setup-motion-manual" class="ghost" data-i18n="motionsManual">手动输入路径</button>
|
|
185
193
|
<span class="note" id="setup-motion-count"></span>
|
|
186
194
|
</div>
|
|
187
195
|
<div class="row" id="setup-motion-manual-row" hidden>
|
|
188
|
-
<label class="k" for="setup-motion-path">文件夹路径</label>
|
|
196
|
+
<label class="k" for="setup-motion-path" data-i18n="motionsPath">文件夹路径</label>
|
|
189
197
|
<input type="text" id="setup-motion-path" class="grow" placeholder="D:\motions\my-pack">
|
|
190
|
-
<button id="setup-motion-add" class="ghost">导入</button>
|
|
198
|
+
<button id="setup-motion-add" class="ghost" data-i18n="motionsAdd">导入</button>
|
|
191
199
|
</div>
|
|
192
200
|
<div class="setup-list" id="setup-motion-list"></div>
|
|
193
201
|
<p class="hint" id="setup-motion-note"></p>
|
|
@@ -197,12 +205,12 @@
|
|
|
197
205
|
|
|
198
206
|
<!-- 完成 -->
|
|
199
207
|
<section class="settings-section setup-screen" id="setup-5" hidden>
|
|
200
|
-
<h2>完成</h2>
|
|
208
|
+
<h2 data-i18n="doneTitle">完成</h2>
|
|
201
209
|
<div class="card">
|
|
202
|
-
<p class="setup-done">配置已保存。</p>
|
|
210
|
+
<p class="setup-done" data-i18n="doneSaved">配置已保存。</p>
|
|
203
211
|
<div class="setup-actions">
|
|
204
|
-
<button id="setup-finish" class="primary">完成</button>
|
|
205
|
-
<button id="setup-close" class="ghost" hidden>关闭窗口</button>
|
|
212
|
+
<button id="setup-finish" class="primary" data-i18n="finishButton">完成</button>
|
|
213
|
+
<button id="setup-close" class="ghost" hidden data-i18n="closeWindow">关闭窗口</button>
|
|
206
214
|
</div>
|
|
207
215
|
<p class="hint" id="setup-finish-note"></p>
|
|
208
216
|
</div>
|
|
@@ -210,16 +218,27 @@
|
|
|
210
218
|
</main>
|
|
211
219
|
|
|
212
220
|
<footer class="setup-nav">
|
|
213
|
-
<button id="setup-later" class="ghost">稍后配置</button>
|
|
221
|
+
<button id="setup-later" class="ghost" data-i18n="later">稍后配置</button>
|
|
214
222
|
<div class="setup-nav-right">
|
|
215
|
-
<button id="setup-back" class="ghost">上一步</button>
|
|
216
|
-
<button id="setup-next" class="primary">下一步</button>
|
|
223
|
+
<button id="setup-back" class="ghost" data-i18n="back">上一步</button>
|
|
224
|
+
<button id="setup-next" class="primary" data-i18n="next">下一步</button>
|
|
217
225
|
</div>
|
|
218
226
|
</footer>
|
|
219
227
|
|
|
220
|
-
<!--
|
|
228
|
+
<!--
|
|
229
|
+
The same watchdog the two settings pages carry: a page that cannot initialise says so rather than sitting blank.
|
|
230
|
+
|
|
231
|
+
It runs BEFORE core.js, so it cannot use the shared table or `resolveLanguage` — hence the two-line map and picker
|
|
232
|
+
below. A stalled page is exactly the page whose reader needs a sentence they can read, and this is the only place
|
|
233
|
+
on the page where English and Chinese both have to fit in one script with no dependency.
|
|
234
|
+
-->
|
|
221
235
|
<script>
|
|
222
236
|
(function () {
|
|
237
|
+
var STUCK = {
|
|
238
|
+
zh: '初始化卡住:脚本已加载但没有完成初始化(看看窗口的控制台,或宿主是否提供 /vrmpet3d/setup-state)。',
|
|
239
|
+
en: 'Initialisation stalled: the script loaded but never finished (check the window console, or whether the host serves /vrmpet3d/setup-state).',
|
|
240
|
+
};
|
|
241
|
+
var language = /^(zh|cn|chinese)/i.test(navigator.language || '') ? 'zh' : 'en';
|
|
223
242
|
function show(text) {
|
|
224
243
|
var node = document.getElementById('status');
|
|
225
244
|
if (node) { node.textContent = text; node.className = 'warn'; }
|
|
@@ -227,7 +246,7 @@
|
|
|
227
246
|
window.setTimeout(function () {
|
|
228
247
|
var node = document.getElementById('status');
|
|
229
248
|
if (node && !node.getAttribute('data-ready')) {
|
|
230
|
-
show(
|
|
249
|
+
show(STUCK[language]);
|
|
231
250
|
}
|
|
232
251
|
}, 3000);
|
|
233
252
|
})();
|