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/client.js
CHANGED
|
@@ -76,6 +76,98 @@
|
|
|
76
76
|
try { return JSON.stringify(error); } catch (e) { return String(error); }
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The DSH locale service's own answer, or null when it has none to give.
|
|
81
|
+
*
|
|
82
|
+
* `ctx.locale` is a cordis service whose snapshot is a plain object, and which field carries the language has
|
|
83
|
+
* moved between builds (`language`, then `locale`, then `current`), so all three are asked for in that order. A
|
|
84
|
+
* service that is absent, shaped differently or throws is NOT an error here: it means this deployment has no app
|
|
85
|
+
* locale to consult and the browser's own hints answer instead, which is what the try/catch is for.
|
|
86
|
+
*/
|
|
87
|
+
function localeHintOf(locale) {
|
|
88
|
+
try {
|
|
89
|
+
var snapshot = locale && typeof locale.getSnapshot === 'function' ? locale.getSnapshot() : undefined;
|
|
90
|
+
var value = snapshot && (snapshot.language || snapshot.locale || snapshot.current);
|
|
91
|
+
return typeof value === 'string' && value ? value : null;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Which of the two languages this surface draws in — the ONE rule, asked every time it is needed.
|
|
99
|
+
*
|
|
100
|
+
* `core.resolveLanguage` owns the rule (an explicit 'zh'/'en' wins; 'auto' takes the FIRST hint that says
|
|
101
|
+
* anything, else English), and the hints are gathered here in their order of authority: the DSH locale first,
|
|
102
|
+
* because DSH is the application the pet lives inside, then the browser's own two (`navigator.language`, then the
|
|
103
|
+
* document's `lang`). English is what nothing-at-all answers, which is also the plugin's own fallback.
|
|
104
|
+
*
|
|
105
|
+
* Deliberately NOT cached in a module-level variable: the language can change while the app runs (the 界面语言
|
|
106
|
+
* preference, or DSH's own locale), and a captured answer is a surface stuck in the language it started in.
|
|
107
|
+
*/
|
|
108
|
+
function readLanguage(locale, preference) {
|
|
109
|
+
var core = globalThis.__VRMPET3D_CORE__;
|
|
110
|
+
var hints = [localeHintOf(locale)];
|
|
111
|
+
if (core && typeof core.browserLanguageHints === 'function') {
|
|
112
|
+
hints = hints.concat(core.browserLanguageHints());
|
|
113
|
+
} else {
|
|
114
|
+
/**
|
|
115
|
+
* A core chunk that predates the rule — the stale-delivery case this repository has eaten more than once.
|
|
116
|
+
* The same two hints, in the same order, from the page itself, so an older host costs the SHARING of the rule
|
|
117
|
+
* rather than the language.
|
|
118
|
+
*/
|
|
119
|
+
try {
|
|
120
|
+
hints.push(globalThis.navigator && globalThis.navigator.language);
|
|
121
|
+
hints.push(globalThis.document && globalThis.document.documentElement && globalThis.document.documentElement.lang);
|
|
122
|
+
} catch (error) { /* no window: the hints stay empty */ }
|
|
123
|
+
}
|
|
124
|
+
if (core && typeof core.resolveLanguage === 'function') return core.resolveLanguage(preference, hints);
|
|
125
|
+
if (preference === 'zh' || preference === 'en') return preference;
|
|
126
|
+
for (var i = 0; i < hints.length; i += 1) {
|
|
127
|
+
var hint = hints[i];
|
|
128
|
+
if (typeof hint !== 'string' || !hint) continue;
|
|
129
|
+
return /^(zh|cn|chinese)/i.test(hint.trim()) ? 'zh' : 'en';
|
|
130
|
+
}
|
|
131
|
+
return 'en';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The language a message with NO widget behind it is written in.
|
|
136
|
+
*
|
|
137
|
+
* The chunk loader runs before any widget exists — and before `ctx.locale` is reachable at all — so the browser's
|
|
138
|
+
* own two hints are the whole of what there is, and English is what nothing-at-all answers. Every message that
|
|
139
|
+
* HAS a widget reads that widget's own resolved language instead.
|
|
140
|
+
*/
|
|
141
|
+
function ambientLanguage() {
|
|
142
|
+
return readLanguage(null, null);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* One language's half of {@link STRINGS}, read at CALL time.
|
|
147
|
+
*
|
|
148
|
+
* 'zh' is the only value that selects the Chinese half: a widget whose language is missing (a stand-in built
|
|
149
|
+
* from the prototype) or anything unexpected reads the English half, which every entry is required to have.
|
|
150
|
+
*/
|
|
151
|
+
function words(language) {
|
|
152
|
+
return STRINGS[language === 'zh' ? 'zh' : 'en'] || STRINGS.en;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* One entry out of {@link STRINGS}, with `{name}` placeholders filled from `values`.
|
|
157
|
+
*
|
|
158
|
+
* A key missing from BOTH halves returns the key, so a gap is visible rather than silently blank — the rule
|
|
159
|
+
* `core.textOf` follows, which this cannot call because it is also used before a core chunk arrives.
|
|
160
|
+
*/
|
|
161
|
+
function word(language, key, values) {
|
|
162
|
+
var text = words(language)[key];
|
|
163
|
+
if (typeof text !== 'string') text = STRINGS.en[key];
|
|
164
|
+
if (typeof text !== 'string') return key;
|
|
165
|
+
if (!values) return text;
|
|
166
|
+
return text.replace(/\{(\w+)\}/g, function (match, name) {
|
|
167
|
+
return Object.prototype.hasOwnProperty.call(values, name) ? String(values[name]) : match;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
79
171
|
/**
|
|
80
172
|
* Every chunk request this half makes, resolved through the module system.
|
|
81
173
|
*
|
|
@@ -108,7 +200,12 @@
|
|
|
108
200
|
}
|
|
109
201
|
return require.async(chunk.file).then(function (exports) {
|
|
110
202
|
var value = exports || globalThis[chunk.global];
|
|
111
|
-
if (!value)
|
|
203
|
+
if (!value) {
|
|
204
|
+
// This failure has no widget to ask, so `ambientLanguage` answers: the page's own hints, else English.
|
|
205
|
+
throw new Error('vrmpet3d: ' + word(ambientLanguage(), 'chunkNoPublish', {
|
|
206
|
+
file: chunk.file, global: chunk.global,
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
112
209
|
loaded[chunk.global] = value;
|
|
113
210
|
return publish(chunk, value);
|
|
114
211
|
}, function (error) {
|
|
@@ -122,7 +219,7 @@
|
|
|
122
219
|
|
|
123
220
|
return pending.then(function () {
|
|
124
221
|
if (missing.length) {
|
|
125
|
-
throw new Error('vrmpet3d:
|
|
222
|
+
throw new Error('vrmpet3d: ' + word(ambientLanguage(), 'chunksMissing', { list: missing.join('; ') }));
|
|
126
223
|
}
|
|
127
224
|
return loaded;
|
|
128
225
|
});
|
|
@@ -203,6 +300,7 @@
|
|
|
203
300
|
*/
|
|
204
301
|
'.vrmpet3d-status[data-tone="gold"]{color:light-dark(#8a5a00,#ffc94d);--dsw-alias-label-shimmer:light-dark(#ffe0a3,#fff0c9)}',
|
|
205
302
|
'.vrmpet3d-status[data-tone="gold"][data-bold="1"]{font-weight:700}',
|
|
303
|
+
'.vrmpet3d-status[data-attention="1"]{pointer-events:auto;cursor:pointer;text-shadow:0 0 4px currentColor,0 0 10px currentColor,0 0 18px currentColor}',
|
|
206
304
|
'.vrmpet3d-status[data-tone="danger"]{color:light-dark(#b3261e,#ff6b6b);--dsw-alias-label-shimmer:light-dark(#ffc9c4,#ffd0cc)}',
|
|
207
305
|
/**
|
|
208
306
|
* 碎碎念: the pet's own short remark, in a bubble above the status prompt.
|
|
@@ -314,6 +412,7 @@
|
|
|
314
412
|
'.vrmpet3d-status-content{display:flex;align-items:center;min-width:0}',
|
|
315
413
|
/* `nowrap`, always: the operator's "提示文字永不换行". One line, however long the line is. */
|
|
316
414
|
'.vrmpet3d-status-text{color:inherit;font-variant-numeric:tabular-nums;white-space:nowrap}',
|
|
415
|
+
'.vrmpet3d-status-dot{pointer-events:auto;cursor:pointer}',
|
|
317
416
|
/* The words are rendered twice from ONE string: `attr()` feeds both copies, so the two cannot drift. */
|
|
318
417
|
'.vrmpet3d-status-text[data-shimmer-text]::after{content:attr(data-shimmer-text)}',
|
|
319
418
|
'.vrmpet3d-status-decoration{position:absolute;inset:0;overflow:clip;pointer-events:none;user-select:none}',
|
|
@@ -367,7 +466,12 @@
|
|
|
367
466
|
*/
|
|
368
467
|
function Placement(preferences, now, core) {
|
|
369
468
|
if (!core || typeof core.stepFall !== 'function') {
|
|
370
|
-
|
|
469
|
+
/**
|
|
470
|
+
* Said in the language the preferences name, because a Placement is built before any widget exists to be
|
|
471
|
+
* asked — the first one is constructed by the widget itself, and the floating window's by its own page.
|
|
472
|
+
*/
|
|
473
|
+
throw new Error('vrmpet3d: ' + word(
|
|
474
|
+
readLanguage(null, preferences && preferences.language), 'placementNoCore'));
|
|
371
475
|
}
|
|
372
476
|
this.core = core;
|
|
373
477
|
this.preferences = preferences;
|
|
@@ -673,25 +777,46 @@
|
|
|
673
777
|
|
|
674
778
|
/**
|
|
675
779
|
* @param {HTMLElement} host the element the slot gave us
|
|
676
|
-
* @param {object} options {
|
|
780
|
+
* @param {object} options { locale, language, onScopeChange, onModelChange }
|
|
677
781
|
*/
|
|
678
782
|
function VrmPet3DWidget(host, options) {
|
|
679
783
|
options = options || {};
|
|
680
784
|
this.host = host;
|
|
785
|
+
this.openSession = options.openSession || function () {};
|
|
786
|
+
this.conversationVisible = options.conversationVisible || function () { return true; };
|
|
681
787
|
this.core = globalThis.__VRMPET3D_CORE__;
|
|
682
788
|
this.sceneApi = globalThis.__VRMPET3D_SCENE__;
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
789
|
+
/**
|
|
790
|
+
* The DSH locale SERVICE itself, kept rather than a snapshot of it.
|
|
791
|
+
*
|
|
792
|
+
* The language has to be re-resolvable while the pet runs (`adoptLanguage`), because
|
|
793
|
+
* `preferences.language === 'auto'` means "whatever DSH says" and DSH's own locale can change under it.
|
|
794
|
+
*/
|
|
795
|
+
this.locale = options.locale || null;
|
|
796
|
+
/**
|
|
797
|
+
* An EXPLICIT language is an override, and that is what the probes and the suites inject (`{language:'zh'}`);
|
|
798
|
+
* the real mount passes `locale` and lets the user's preference decide.
|
|
799
|
+
*/
|
|
800
|
+
this.injectedLanguage = options.language === 'zh' || options.language === 'en' ? options.language : null;
|
|
801
|
+
/**
|
|
802
|
+
* Resolved BEFORE the two failures below, because those messages are shown to the user in a badge and have to
|
|
803
|
+
* be said in the same language as everything else. The preference is not readable yet (`readStorage` needs
|
|
804
|
+
* `this.document`, and that needs a core), so this pass has DSH's locale and the browser's hints only; the
|
|
805
|
+
* line after `readStorage()` re-resolves with the user's own answer.
|
|
806
|
+
*/
|
|
807
|
+
this.language = this.injectedLanguage || readLanguage(this.locale, null);
|
|
808
|
+
if (!this.core) throw new Error('vrmpet3d: ' + word(this.language, 'mountNoCore'));
|
|
809
|
+
if (!this.sceneApi) throw new Error('vrmpet3d: ' + word(this.language, 'mountNoScene'));
|
|
687
810
|
this.onScopeChange = typeof options.onScopeChange === 'function' ? options.onScopeChange : function () {};
|
|
688
811
|
this.onModelChange = typeof options.onModelChange === 'function' ? options.onModelChange : function () {};
|
|
689
812
|
this.document = host.ownerDocument || document;
|
|
690
813
|
|
|
691
814
|
this.preferences = this.readStorage();
|
|
815
|
+
this.language = this.injectedLanguage || readLanguage(this.locale, this.preferences.language);
|
|
692
816
|
this.placement = new Placement(this.preferences, function () { return Date.now(); }, this.core);
|
|
693
817
|
|
|
694
818
|
this.machine = new this.core.PetMachine(function () { return Date.now(); }, this.preferences.sleepAfterMs);
|
|
819
|
+
this.machine.requireViewedAcknowledgement = typeof options.conversationVisible === 'function';
|
|
695
820
|
/**
|
|
696
821
|
* The machine reads the settings it needs through the WIDGET, not from a copy.
|
|
697
822
|
*
|
|
@@ -985,6 +1110,13 @@
|
|
|
985
1110
|
// The model is ONE setting for both surfaces, so a change made in the window or in the settings
|
|
986
1111
|
// window moves this pet too - there is no per-surface model to keep in step.
|
|
987
1112
|
self.preferences = next;
|
|
1113
|
+
/**
|
|
1114
|
+
* 界面语言 follows the snapshot like every other preference, and only NOW is the answer readable:
|
|
1115
|
+
* `preferences.language` may be an explicit 中文/English or `auto` (what DSH's own locale says). A language
|
|
1116
|
+
* that actually CHANGED re-says this surface's own words and rebuilds the shared menu; nothing else about
|
|
1117
|
+
* the pet is touched.
|
|
1118
|
+
*/
|
|
1119
|
+
self.adoptLanguage();
|
|
988
1120
|
if (self.scene) self.scene.applyPreferences(Object.assign({}, next, { motions: self.effectiveMotions() }));
|
|
989
1121
|
self.applyMotions();
|
|
990
1122
|
self.applyPlacement();
|
|
@@ -1008,6 +1140,7 @@
|
|
|
1008
1140
|
*/
|
|
1009
1141
|
if (this.core.watchHostPreview) {
|
|
1010
1142
|
this.previewSync = this.core.watchHostPreview({
|
|
1143
|
+
onNavigate: function (id) { self.openSession(id); },
|
|
1011
1144
|
onPreview: function (preview) {
|
|
1012
1145
|
/**
|
|
1013
1146
|
* A PREVIEW ENDS A DANCE — and a preview of the DANCING state STARTS one.
|
|
@@ -1052,6 +1185,8 @@
|
|
|
1052
1185
|
if (event && event.key && !known) return;
|
|
1053
1186
|
var next = self.readStorage();
|
|
1054
1187
|
self.preferences = next;
|
|
1188
|
+
// The other document may have been the settings window's 界面语言 field: same rule as the host snapshot.
|
|
1189
|
+
self.adoptLanguage();
|
|
1055
1190
|
if (self.scene) self.scene.applyPreferences(Object.assign({}, next, { motions: self.effectiveMotions() }));
|
|
1056
1191
|
self.applyMotions();
|
|
1057
1192
|
self.applyPlacement();
|
|
@@ -1095,6 +1230,7 @@
|
|
|
1095
1230
|
// ---- DOM -----------------------------------------------------------------
|
|
1096
1231
|
|
|
1097
1232
|
VrmPet3DWidget.prototype.build = function () {
|
|
1233
|
+
var self = this;
|
|
1098
1234
|
var doc = this.document;
|
|
1099
1235
|
var root = doc.createElement('div');
|
|
1100
1236
|
root.className = 'vrmpet3d-root';
|
|
@@ -1159,6 +1295,10 @@
|
|
|
1159
1295
|
this.canvas = root.querySelector('.vrmpet3d-pet canvas');
|
|
1160
1296
|
this.badge = root.querySelector('.vrmpet3d-badge');
|
|
1161
1297
|
this.statusNode = root.querySelector('.vrmpet3d-status');
|
|
1298
|
+
this.statusNode.addEventListener('click', function (event) {
|
|
1299
|
+
event.stopPropagation();
|
|
1300
|
+
self.openStatusSession();
|
|
1301
|
+
});
|
|
1162
1302
|
this.statusIcon = root.querySelector('.vrmpet3d-status-icon');
|
|
1163
1303
|
/**
|
|
1164
1304
|
* The green dot, when a completion is waiting elsewhere.
|
|
@@ -1169,6 +1309,11 @@
|
|
|
1169
1309
|
* puts beside that session in its list, and it disappears when the user goes back there.
|
|
1170
1310
|
*/
|
|
1171
1311
|
this.statusDot = root.querySelector('.vrmpet3d-status-dot');
|
|
1312
|
+
this.statusDot.addEventListener('click', function (event) {
|
|
1313
|
+
event.stopPropagation();
|
|
1314
|
+
self.openQueuedSession('completed');
|
|
1315
|
+
});
|
|
1316
|
+
this.statusDot.title = this.language === 'en' ? 'Open completed conversation' : '打开已完成的会话';
|
|
1172
1317
|
this.statusDecoration = root.querySelector('.vrmpet3d-status-decoration');
|
|
1173
1318
|
/**
|
|
1174
1319
|
* The prompt's two text copies — the real one and the masked highlight — which always render the SAME
|
|
@@ -1198,7 +1343,7 @@
|
|
|
1198
1343
|
};
|
|
1199
1344
|
|
|
1200
1345
|
VrmPet3DWidget.prototype.applyStrings = function () {
|
|
1201
|
-
var text =
|
|
1346
|
+
var text = words(this.language);
|
|
1202
1347
|
/**
|
|
1203
1348
|
* The ONE label this surface still writes by hand.
|
|
1204
1349
|
*
|
|
@@ -1210,6 +1355,37 @@
|
|
|
1210
1355
|
this.pet.setAttribute('aria-label', text.ariaPet);
|
|
1211
1356
|
};
|
|
1212
1357
|
|
|
1358
|
+
/**
|
|
1359
|
+
* Follow 界面语言 — the preference, DSH's locale, and this surface's own words.
|
|
1360
|
+
*
|
|
1361
|
+
* Called from every path that can change the preference (a host snapshot, a storage write from another
|
|
1362
|
+
* document) because 'auto' means "whatever DSH says" and DSH's locale can change under a running pet. Only a
|
|
1363
|
+
* CHANGE does anything: this rebuilds the menu, so running it on every poll would throw away and rebuild a live
|
|
1364
|
+
* element every few seconds for nothing.
|
|
1365
|
+
*
|
|
1366
|
+
* What is re-said, and why exactly these three:
|
|
1367
|
+
* * the restore label and the aria name are the only strings this surface CAPTURES — everything else is read
|
|
1368
|
+
* out of the table when it is drawn, which is the rule that makes a live language change possible at all;
|
|
1369
|
+
* * the shared menu carries its words from `createMenu` and cannot be re-worded in place, so it is rebuilt
|
|
1370
|
+
* (the old element is destroyed by `buildMenu`, not left in the document);
|
|
1371
|
+
* * the prompt is drawn from `core.statusFor(..., this.language)` on the next frame, and its cached key is
|
|
1372
|
+
* dropped so "next frame" is when the new words arrive rather than "the next time the line differs";
|
|
1373
|
+
* * the 碎碎念 already on screen is the machine's own held line and is left alone — it expires on its own
|
|
1374
|
+
* clock, and the remark after it is picked in the new language. Clearing the machine here would also throw
|
|
1375
|
+
* away its throttle memory, which is a change nobody asked for.
|
|
1376
|
+
*/
|
|
1377
|
+
VrmPet3DWidget.prototype.adoptLanguage = function () {
|
|
1378
|
+
// A stand-in built from the prototype (a probe's own widget) has no language to change and no DOM to re-say.
|
|
1379
|
+
if (!this.language) return;
|
|
1380
|
+
var wanted = this.injectedLanguage
|
|
1381
|
+
|| readLanguage(this.locale, this.preferences && this.preferences.language);
|
|
1382
|
+
if (wanted === this.language) return;
|
|
1383
|
+
this.language = wanted;
|
|
1384
|
+
this.applyStrings();
|
|
1385
|
+
this.buildMenu();
|
|
1386
|
+
this.headStatusKey = null;
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1213
1389
|
VrmPet3DWidget.prototype.bindMenu = function () {
|
|
1214
1390
|
var self = this;
|
|
1215
1391
|
|
|
@@ -1510,7 +1686,7 @@
|
|
|
1510
1686
|
var fallback = function (reason) {
|
|
1511
1687
|
var opened = null;
|
|
1512
1688
|
try { opened = globalThis.open(url, '_blank'); } catch (error) { opened = null; }
|
|
1513
|
-
if (!opened) self.showStatus((
|
|
1689
|
+
if (!opened) self.showStatus((words(self.language).configNote || '') + ' ' + (reason || ''));
|
|
1514
1690
|
};
|
|
1515
1691
|
try {
|
|
1516
1692
|
fetch('/vrmpet3d/desktop', {
|
|
@@ -1523,9 +1699,11 @@
|
|
|
1523
1699
|
// placeholder while a VRM was being shown.
|
|
1524
1700
|
body: JSON.stringify({ action: which, modelId: self.currentModelId() }),
|
|
1525
1701
|
}).then(function (response) {
|
|
1526
|
-
|
|
1702
|
+
// Both reasons are APPENDED to `configNote` and shown in the badge, so they are user-visible sentences of
|
|
1703
|
+
// this surface's own and come out of the table like the rest.
|
|
1704
|
+
if (!response.ok) fallback(words(self.language).settingsNoWindow);
|
|
1527
1705
|
}).catch(function () {
|
|
1528
|
-
fallback(
|
|
1706
|
+
fallback(words(self.language).settingsUnreachable);
|
|
1529
1707
|
});
|
|
1530
1708
|
} catch (error) {
|
|
1531
1709
|
fallback(error && error.message);
|
|
@@ -1577,7 +1755,7 @@
|
|
|
1577
1755
|
*/
|
|
1578
1756
|
var opened = null;
|
|
1579
1757
|
try { opened = globalThis.open('/vrmpet3d/setup', '_blank'); } catch (error) { opened = null; }
|
|
1580
|
-
if (!opened) self.showStatus((
|
|
1758
|
+
if (!opened) self.showStatus((words(self.language).setupNote || ''));
|
|
1581
1759
|
return null;
|
|
1582
1760
|
});
|
|
1583
1761
|
})
|
|
@@ -1600,6 +1778,7 @@
|
|
|
1600
1778
|
*/
|
|
1601
1779
|
VrmPet3DWidget.prototype.openMenuAt = function (clientX, clientY) {
|
|
1602
1780
|
if (!this.menu) return false;
|
|
1781
|
+
if (typeof this.menu.setSurfaceVisible === 'function') this.menu.setSurfaceVisible(this.canUseIndependentPet());
|
|
1603
1782
|
var doc = this.document;
|
|
1604
1783
|
var view = doc.defaultView;
|
|
1605
1784
|
var viewportWidth = Number(view && view.innerWidth) || doc.documentElement.clientWidth || 0;
|
|
@@ -1612,6 +1791,7 @@
|
|
|
1612
1791
|
* it is read when the menu is built.
|
|
1613
1792
|
*/
|
|
1614
1793
|
this.menu.setSurfaceAllowed(this.preferences.petSurface !== 'both');
|
|
1794
|
+
if (this.menu.setDanceState) this.menu.setDanceState(this.machine.state);
|
|
1615
1795
|
/**
|
|
1616
1796
|
* 重力's check mark, read at the same moment and for the same reason: a host snapshot or a settings-window
|
|
1617
1797
|
* edit can change the preference while the page keeps running, so a menu that cached it would show the
|
|
@@ -1655,6 +1835,14 @@
|
|
|
1655
1835
|
*/
|
|
1656
1836
|
VrmPet3DWidget.prototype.buildMenu = function () {
|
|
1657
1837
|
var api = globalThis.__VRMPET3D_MENU__;
|
|
1838
|
+
/**
|
|
1839
|
+
* A REBUILD replaces the element rather than leaving the old one in the document — the menu is DOM, and a
|
|
1840
|
+
* second copy would keep its own document listeners and its own words. `adoptLanguage` is the one caller that
|
|
1841
|
+
* rebuilds (the menu carries its labels from `createMenu` and cannot be re-worded in place), and the first
|
|
1842
|
+
* call finds `this.menu` null.
|
|
1843
|
+
*/
|
|
1844
|
+
if (this.menu && typeof this.menu.destroy === 'function') this.menu.destroy();
|
|
1845
|
+
this.menu = null;
|
|
1658
1846
|
if (!api || typeof api.createMenu !== 'function') return null;
|
|
1659
1847
|
this.menuAvailable = true;
|
|
1660
1848
|
api.injectStyle(this.document, 'vrmpet3d-menu-style');
|
|
@@ -1664,6 +1852,7 @@
|
|
|
1664
1852
|
container: this.root,
|
|
1665
1853
|
language: this.language,
|
|
1666
1854
|
toSurface: 'window',
|
|
1855
|
+
showSurface: this.canUseIndependentPet(),
|
|
1667
1856
|
onChoose: function (id) { self.onMenuChoice(id); },
|
|
1668
1857
|
});
|
|
1669
1858
|
return this.menu;
|
|
@@ -2000,7 +2189,7 @@
|
|
|
2000
2189
|
*/
|
|
2001
2190
|
VrmPet3DWidget.prototype.applySurface = function (attempt) {
|
|
2002
2191
|
var self = this;
|
|
2003
|
-
var strings =
|
|
2192
|
+
var strings = words(this.language);
|
|
2004
2193
|
var surface = this.preferences.petSurface || 'auto';
|
|
2005
2194
|
/**
|
|
2006
2195
|
* What was last ASKED for, which is not the same as what is running: the window can fail to start. It is
|
|
@@ -2066,7 +2255,7 @@
|
|
|
2066
2255
|
|
|
2067
2256
|
VrmPet3DWidget.prototype.setDesktopWindow = function (enabled) {
|
|
2068
2257
|
var self = this;
|
|
2069
|
-
var strings =
|
|
2258
|
+
var strings = words(this.language);
|
|
2070
2259
|
this.syncDesktop(null, strings.desktopStarting);
|
|
2071
2260
|
return fetch('/vrmpet3d/desktop', {
|
|
2072
2261
|
method: 'POST',
|
|
@@ -2099,7 +2288,7 @@
|
|
|
2099
2288
|
.catch(function () {
|
|
2100
2289
|
// A session without the route (an older host half) is RECORDED rather than thrown: `desktopNote` is kept
|
|
2101
2290
|
// as data (see `syncDesktop`), so "the window could not start" stays answerable.
|
|
2102
|
-
var strings =
|
|
2291
|
+
var strings = words(self.language);
|
|
2103
2292
|
self.syncDesktop(null, strings.desktopUnavailable);
|
|
2104
2293
|
return null;
|
|
2105
2294
|
});
|
|
@@ -2114,8 +2303,14 @@
|
|
|
2114
2303
|
* the widget's own state. "The window is up but the pet is not drawing in it" stays answerable that way, which
|
|
2115
2304
|
* is the whole reason the host reports the page's own verdict.
|
|
2116
2305
|
*/
|
|
2306
|
+
VrmPet3DWidget.prototype.canUseIndependentPet = function () {
|
|
2307
|
+
// The host can borrow Electron even when it is itself a plain Node process.
|
|
2308
|
+
// Its runtime result, rather than the UI bridge or process type, decides this.
|
|
2309
|
+
return Boolean(this.desktopStatus && this.desktopStatus.electronAvailable === true);
|
|
2310
|
+
};
|
|
2311
|
+
|
|
2117
2312
|
VrmPet3DWidget.prototype.syncDesktop = function (status, note) {
|
|
2118
|
-
var strings =
|
|
2313
|
+
var strings = words(this.language);
|
|
2119
2314
|
this.desktopStatus = status || null;
|
|
2120
2315
|
if (note) {
|
|
2121
2316
|
this.desktopNote = note;
|
|
@@ -2200,6 +2395,7 @@
|
|
|
2200
2395
|
* place that knows it. A caller that omits it gets the click pool, which is what an older cached chunk does.
|
|
2201
2396
|
*/
|
|
2202
2397
|
VrmPet3DWidget.prototype.reactSpeech = function (anatomy, gesture) {
|
|
2398
|
+
if (this.machine && this.machine.state === 'dancing') return null;
|
|
2203
2399
|
if (!this.speech) return null;
|
|
2204
2400
|
var random = this.scene && typeof this.scene.random === 'function' ? this.scene.random : null;
|
|
2205
2401
|
var line = this.speech.react(
|
|
@@ -2796,7 +2992,7 @@
|
|
|
2796
2992
|
var self = this;
|
|
2797
2993
|
return fetch('/vrmpet3d/catalog', { credentials: 'omit' })
|
|
2798
2994
|
.then(function (response) {
|
|
2799
|
-
if (!response.ok) throw new Error('
|
|
2995
|
+
if (!response.ok) throw new Error(word(self.language, 'catalogRoute', { status: response.status }));
|
|
2800
2996
|
return response.json();
|
|
2801
2997
|
})
|
|
2802
2998
|
.then(function (body) {
|
|
@@ -2882,19 +3078,28 @@
|
|
|
2882
3078
|
*/
|
|
2883
3079
|
VrmPet3DWidget.prototype.startDance = function (motionId) {
|
|
2884
3080
|
if (!this.machine || typeof this.machine.setDancing !== 'function') return false;
|
|
3081
|
+
if (['resting', 'offline', 'dancing'].indexOf(this.machine.resolve(this.aggregate.snapshot())) === -1) return false;
|
|
2885
3082
|
var wanted = typeof motionId === 'string' && motionId ? motionId : null;
|
|
2886
3083
|
if (!wanted) {
|
|
2887
3084
|
var ids = this.danceClipIds();
|
|
2888
3085
|
if (!ids.length) {
|
|
2889
|
-
|
|
3086
|
+
// The same sentence the floating window's 跳支舞 puts in its bubble (see `lib/desktop-page.js`).
|
|
3087
|
+
this.showStatus(words(this.language).danceEmpty);
|
|
2890
3088
|
return false;
|
|
2891
3089
|
}
|
|
3090
|
+
var current = this.machine.dancing && this.machine.dancing.motionId;
|
|
3091
|
+
var alternatives = ids.filter(function (id) { return id !== current; });
|
|
3092
|
+
if (alternatives.length) ids = alternatives;
|
|
2892
3093
|
wanted = ids[Math.floor(Math.random() * ids.length)];
|
|
2893
3094
|
}
|
|
2894
3095
|
// A generous deadline: the clip's own end is what normally stops this, and a dance that outstays its welcome is worse
|
|
2895
3096
|
// than one that is cut short at three minutes.
|
|
2896
3097
|
if (!this.machine.setDancing(wanted, 180000)) return false;
|
|
2897
|
-
if (this.scene)
|
|
3098
|
+
if (this.scene) {
|
|
3099
|
+
this.scene.setPreview(null);
|
|
3100
|
+
// A previous run may already be finished, even with no preview left to clear.
|
|
3101
|
+
if (this.scene.playback && this.scene.playback.state === 'dancing') this.scene.stopPlayback(null);
|
|
3102
|
+
}
|
|
2898
3103
|
var self = this;
|
|
2899
3104
|
var dance = this.machine.dancing;
|
|
2900
3105
|
this.ensureMotion(wanted, false).then(function () {
|
|
@@ -2902,13 +3107,19 @@
|
|
|
2902
3107
|
self.scene.setPreview({ state: 'dancing', motionId: wanted });
|
|
2903
3108
|
}
|
|
2904
3109
|
});
|
|
3110
|
+
// Publish the accepted dance immediately, including when this surface is hidden.
|
|
3111
|
+
// The floating window must not wait for the next background animation frame.
|
|
3112
|
+
if (!this.disposed) this.pump();
|
|
2905
3113
|
return true;
|
|
2906
3114
|
};
|
|
2907
3115
|
|
|
2908
3116
|
/** End a dance early — the double-click gesture, and the clip's own end. */
|
|
2909
3117
|
VrmPet3DWidget.prototype.stopDance = function () {
|
|
2910
3118
|
if (!this.machine || typeof this.machine.stopDancing !== 'function') return null;
|
|
2911
|
-
|
|
3119
|
+
var stopped = this.machine.stopDancing();
|
|
3120
|
+
if (this.scene && this.scene.preview && this.scene.preview.state === 'dancing') this.scene.setPreview(null);
|
|
3121
|
+
if (stopped && !this.disposed) this.pump();
|
|
3122
|
+
return stopped;
|
|
2912
3123
|
};
|
|
2913
3124
|
|
|
2914
3125
|
VrmPet3DWidget.prototype.reloadModel = function () { var self = this;
|
|
@@ -2919,10 +3130,15 @@
|
|
|
2919
3130
|
* answered from the HTTP cache — and "reload" that returns the cached bytes is a button that does nothing.
|
|
2920
3131
|
*/
|
|
2921
3132
|
if (typeof this.scene.bustModelCache === 'function') this.scene.bustModelCache();
|
|
2922
|
-
|
|
3133
|
+
// The badge's three lines of 重载模型, read out of the table at each moment it is said — the language can change
|
|
3134
|
+
// while the load is in flight — and the same three the floating window says in its bubble.
|
|
3135
|
+
this.showStatus(words(this.language).reloading);
|
|
2923
3136
|
return this.loadModel(id).then(function (status) {
|
|
2924
3137
|
// Say what happened, because the visible difference between a good reload and a cached one is nothing at all.
|
|
2925
|
-
if (self.statusNode)
|
|
3138
|
+
if (self.statusNode) {
|
|
3139
|
+
var done = words(self.language);
|
|
3140
|
+
self.showStatus(status && status.ok ? done.reloaded : ((status && status.error) || done.reloadFailed));
|
|
3141
|
+
}
|
|
2926
3142
|
return status;
|
|
2927
3143
|
});
|
|
2928
3144
|
};
|
|
@@ -3097,7 +3313,7 @@
|
|
|
3097
3313
|
* where the clips are chosen.
|
|
3098
3314
|
*/
|
|
3099
3315
|
VrmPet3DWidget.prototype.refreshMotionNotice = function () {
|
|
3100
|
-
var text =
|
|
3316
|
+
var text = words(this.language);
|
|
3101
3317
|
var message = null;
|
|
3102
3318
|
|
|
3103
3319
|
if (!this.motionCatalog) {
|
|
@@ -3161,6 +3377,11 @@
|
|
|
3161
3377
|
running: snap.running === true,
|
|
3162
3378
|
working: Number(snap.workingCount) || 0,
|
|
3163
3379
|
waiting: Number(snap.waitingCount) || 0,
|
|
3380
|
+
statusSessionId: this.machine.state === 'waiting' ? snap.waitingSessionId
|
|
3381
|
+
: (this.machine.notice && this.machine.notice.sessionId) || null,
|
|
3382
|
+
completedSessionId: snap.completedSessionId || null,
|
|
3383
|
+
waitingSessionIds: snap.waitingSessionIds || [],
|
|
3384
|
+
completedSessionIds: snap.completedSessionIds || [],
|
|
3164
3385
|
unread: Number(snap.unreadCount) || 0,
|
|
3165
3386
|
/**
|
|
3166
3387
|
* The breakdown behind that count, as numbers — why the green dot is lit, with no session id or title in it.
|
|
@@ -3455,7 +3676,7 @@
|
|
|
3455
3676
|
VrmPet3DWidget.prototype.fetchMotionBytes = function (motionId) {
|
|
3456
3677
|
var url = '/vrmpet3d/motion?m=' + encodeURIComponent(motionId);
|
|
3457
3678
|
return fetch(url, { credentials: 'omit' }).then(function (response) {
|
|
3458
|
-
if (!response.ok) throw new Error('
|
|
3679
|
+
if (!response.ok) throw new Error(word(this.language, 'motionRoute', { status: response.status }));
|
|
3459
3680
|
return response.arrayBuffer();
|
|
3460
3681
|
});
|
|
3461
3682
|
};
|
|
@@ -3516,7 +3737,10 @@
|
|
|
3516
3737
|
self.motionLoaded[motionId] = status;
|
|
3517
3738
|
self.motionLoadedLoop[motionId] = declaredLoop;
|
|
3518
3739
|
} else {
|
|
3519
|
-
|
|
3740
|
+
// A scene that gave a reason has already said it in its own words; this is the widget's own sentence, and
|
|
3741
|
+
// `vrmpet3d:` stays in front of it because that prefix is what a report greps for.
|
|
3742
|
+
var message = (status && status.reason)
|
|
3743
|
+
|| ('vrmpet3d: ' + word(self.language, 'motionLoad', { id: motionId }));
|
|
3520
3744
|
self.showStatus(message);
|
|
3521
3745
|
self.motionStatusMessage = message;
|
|
3522
3746
|
}
|
|
@@ -3747,6 +3971,9 @@
|
|
|
3747
3971
|
var now = Date.now();
|
|
3748
3972
|
if (now - (this.focusCheckedAt || 0) >= FOCUS_POLL_MS) {
|
|
3749
3973
|
this.focusCheckedAt = now;
|
|
3974
|
+
// Locale changes do not advance the plugin settings version. Re-read alongside focus,
|
|
3975
|
+
// including background/wake pumps, so auto follows DSH without another settings write.
|
|
3976
|
+
this.adoptLanguage();
|
|
3750
3977
|
var doc = this.document;
|
|
3751
3978
|
var focused = true;
|
|
3752
3979
|
try {
|
|
@@ -3760,8 +3987,11 @@
|
|
|
3760
3987
|
}
|
|
3761
3988
|
}
|
|
3762
3989
|
|
|
3990
|
+
this.acknowledgeVisibleConversation();
|
|
3763
3991
|
var snapshot = this.aggregate.snapshot();
|
|
3764
3992
|
var state = this.machine.resolve(snapshot);
|
|
3993
|
+
this.completedUnreadCount = snapshot.completedUnreadCount;
|
|
3994
|
+
if (this.menu && this.menu.setDanceState) this.menu.setDanceState(state);
|
|
3765
3995
|
// Dance previews belong to the dance lifetime, including work interruption.
|
|
3766
3996
|
if (this.scene.preview && this.scene.preview.state === 'dancing' && state !== 'dancing') {
|
|
3767
3997
|
this.scene.setPreview(null);
|
|
@@ -3793,11 +4023,13 @@
|
|
|
3793
4023
|
*/
|
|
3794
4024
|
var clock = this.machine && typeof this.machine.statusClock === 'function' ? this.machine.statusClock() : null;
|
|
3795
4025
|
var clockKey = clock ? clock.startedAt + ':' + (clock.endedAt || '') : '';
|
|
4026
|
+
var completedKey = (snapshot.completedSessionIds || []).join(',') + '|' + (snapshot.waitingSessionIds || []).join(',');
|
|
3796
4027
|
if (state !== this.lastPublishedState || speech !== this.lastPublishedSpeech || touch !== null
|
|
3797
|
-
|| clockKey !== this.lastPublishedClockKey) {
|
|
4028
|
+
|| clockKey !== this.lastPublishedClockKey || completedKey !== this.lastPublishedCompletedKey) {
|
|
3798
4029
|
this.lastPublishedState = state;
|
|
3799
4030
|
this.lastPublishedSpeech = speech;
|
|
3800
4031
|
this.lastPublishedClockKey = clockKey;
|
|
4032
|
+
this.lastPublishedCompletedKey = completedKey;
|
|
3801
4033
|
/**
|
|
3802
4034
|
* `clock` rides along because the window has no turn frames of its own, and the UNREAD count because the window
|
|
3803
4035
|
* has no session store either: its green dot is DSH's `completionUnread`, which only this half can see.
|
|
@@ -3806,7 +4038,7 @@
|
|
|
3806
4038
|
* question with an answer (`/vrmpet3d/state`) instead of a guess. See `decisionWhy`.
|
|
3807
4039
|
*/
|
|
3808
4040
|
if (this.publishState) {
|
|
3809
|
-
this.publishState(state, speech, touch, clock, null,
|
|
4041
|
+
this.publishState(state, speech, touch, clock, null, snapshot.completedUnreadCount || 0, this.decisionWhy());
|
|
3810
4042
|
}
|
|
3811
4043
|
}
|
|
3812
4044
|
|
|
@@ -3827,7 +4059,7 @@
|
|
|
3827
4059
|
// The TURN clock, not the state's own age: 用时 is a claim about DSH's turn (`PetMachine.statusClock`), and the
|
|
3828
4060
|
// token is dropped entirely while no turn is open — DSH's own token-free 「深度求索中」.
|
|
3829
4061
|
var clock = this.machine && typeof this.machine.statusClock === 'function' ? this.machine.statusClock() : undefined;
|
|
3830
|
-
var line = this.core.statusFor(this.preferences, state, this.machine.stateSince, Date.now(), clock);
|
|
4062
|
+
var line = this.core.statusFor(this.preferences, state, this.machine.stateSince, Date.now(), clock, this.language, this.completedUnreadCount);
|
|
3831
4063
|
/**
|
|
3832
4064
|
* A state with no line clears the prompt rather than leaving the previous one up: a caption that
|
|
3833
4065
|
* outlives its state is how a pet ends up saying 深度求索中 over a finished turn.
|
|
@@ -3855,6 +4087,8 @@
|
|
|
3855
4087
|
* other, and the attribute is cheap. `data-tone` is what the stylesheet reads, so the colours live in CSS
|
|
3856
4088
|
* (where the two surfaces can differ) and the DECISION lives in `core.statusFor` (where they cannot).
|
|
3857
4089
|
*/
|
|
4090
|
+
if (['done', 'waiting', 'error'].indexOf(state) !== -1) node.setAttribute('data-attention', '1');
|
|
4091
|
+
else node.removeAttribute('data-attention');
|
|
3858
4092
|
if (line.tone) node.setAttribute('data-tone', line.tone);
|
|
3859
4093
|
else node.removeAttribute('data-tone');
|
|
3860
4094
|
if (line.bold) node.setAttribute('data-bold', '1');
|
|
@@ -3882,8 +4116,8 @@
|
|
|
3882
4116
|
// The dot is about ANOTHER session's unread completion, so it is drawn from the machine's count and not from
|
|
3883
4117
|
// the line: the line is what is happening here, the dot is what finished elsewhere.
|
|
3884
4118
|
if (this.statusDot) {
|
|
3885
|
-
this.statusDot.hidden = !(line.state
|
|
3886
|
-
&&
|
|
4119
|
+
this.statusDot.hidden = !(['working', 'waiting'].indexOf(line.state) !== -1
|
|
4120
|
+
&& Number(this.completedUnreadCount) > 0);
|
|
3887
4121
|
}
|
|
3888
4122
|
}
|
|
3889
4123
|
// Every frame, not only when the words change: the caption is anchored to the HEAD, so an orbit, a zoom
|
|
@@ -4014,6 +4248,27 @@
|
|
|
4014
4248
|
return line;
|
|
4015
4249
|
};
|
|
4016
4250
|
|
|
4251
|
+
VrmPet3DWidget.prototype.openStatusSession = function () {
|
|
4252
|
+
var state = this.machine.state;
|
|
4253
|
+
if (state === 'waiting' || state === 'done') return this.openQueuedSession(state === 'waiting' ? 'waiting' : 'completed');
|
|
4254
|
+
var snap = this.aggregate.snapshot();
|
|
4255
|
+
var id = state === 'waiting' ? snap.waitingSessionId
|
|
4256
|
+
: ((state === 'done' || state === 'error') && this.machine.notice ? this.machine.notice.sessionId : null);
|
|
4257
|
+
if (id) this.openSession(id);
|
|
4258
|
+
};
|
|
4259
|
+
|
|
4260
|
+
VrmPet3DWidget.prototype.openQueuedSession = function (kind) {
|
|
4261
|
+
var snap = this.aggregate.snapshot();
|
|
4262
|
+
var ids = kind === 'waiting' ? snap.waitingSessionIds : snap.completedSessionIds;
|
|
4263
|
+
if (!ids || !ids.length) return null;
|
|
4264
|
+
this.queueCursor = this.queueCursor || {};
|
|
4265
|
+
var index = ids.indexOf(this.queueCursor[kind]);
|
|
4266
|
+
var id = ids[(index + 1) % ids.length];
|
|
4267
|
+
this.queueCursor[kind] = id;
|
|
4268
|
+
this.openSession(id);
|
|
4269
|
+
return id;
|
|
4270
|
+
};
|
|
4271
|
+
|
|
4017
4272
|
/**
|
|
4018
4273
|
* Ask the host for a generated line, when the machine has asked for one and nobody has yet.
|
|
4019
4274
|
*
|
|
@@ -4180,7 +4435,8 @@
|
|
|
4180
4435
|
if (this.machine && typeof this.machine.dancingNow === 'function' && this.machine.dancing) {
|
|
4181
4436
|
var dancing = this.machine.dancingNow();
|
|
4182
4437
|
var playing = this.scene.playback;
|
|
4183
|
-
if (dancing && playing && playing.state === 'dancing' && playing.
|
|
4438
|
+
if (dancing && playing && playing.state === 'dancing' && playing.id === dancing.motionId && playing.record
|
|
4439
|
+
&& this.scene.motionFinished(playing.record)) {
|
|
4184
4440
|
this.machine.stopDancing();
|
|
4185
4441
|
}
|
|
4186
4442
|
}
|
|
@@ -4243,22 +4499,27 @@
|
|
|
4243
4499
|
// …and a badge the scene wrote is retracted once the scene is happy again (see `retractSceneStatus`).
|
|
4244
4500
|
this.retractSceneStatus();
|
|
4245
4501
|
|
|
4246
|
-
// The
|
|
4247
|
-
// the pet has THIS frame: the cursor can sit anywhere in the window (the listener is
|
|
4248
|
-
// on the document), and the pet can be dragged or falling while it looks. Using the
|
|
4249
|
-
// placement instead of `getBoundingClientRect` keeps this out of layout.
|
|
4502
|
+
// The rendered eyes, rather than the slot's centre, are the zero of the aim.
|
|
4250
4503
|
if (this.preferences.lookAtPointer && this.pointer) {
|
|
4251
4504
|
var pixels = this.placement.pixels();
|
|
4505
|
+
var eye = typeof this.scene.gazeOrigin === 'function' ? this.scene.gazeOrigin() : null;
|
|
4506
|
+
var gazeView = this.document && this.document.defaultView;
|
|
4507
|
+
var gazeScreenWidth = Number(gazeView && gazeView.screen && gazeView.screen.width) || this.placement.viewportWidth;
|
|
4252
4508
|
var aim = this.core.pointerAim({
|
|
4253
4509
|
pointerX: this.pointer.x,
|
|
4254
4510
|
pointerY: this.pointer.y,
|
|
4255
|
-
centreX: pixels.left + this.placement.width / 2,
|
|
4256
|
-
centreY: pixels.top + this.placement.height / 2,
|
|
4511
|
+
centreX: pixels.left + (eye ? eye.x * this.placement.width / this.scene.width : this.placement.width / 2),
|
|
4512
|
+
centreY: pixels.top + (eye ? eye.y * this.placement.height / this.scene.height : this.placement.height / 2),
|
|
4257
4513
|
// The same two settings as the floating window's (the 模型参数 window's 视线 section), with the
|
|
4258
|
-
// same "auto" default: 0 px means
|
|
4259
|
-
spanX: this.core.resolveGazeSpan(this.preferences.gazeSpanX,
|
|
4260
|
-
spanY: this.core.resolveGazeSpan(this.preferences.gazeSpanY,
|
|
4261
|
-
curve: this.preferences.gazeCurve
|
|
4514
|
+
// same "auto" default: 0 px means half the screen width for both axes.
|
|
4515
|
+
spanX: this.core.resolveGazeSpan(this.preferences.gazeSpanX, gazeScreenWidth),
|
|
4516
|
+
spanY: this.core.resolveGazeSpan(this.preferences.gazeSpanY, gazeScreenWidth),
|
|
4517
|
+
curve: this.preferences.gazeCurve,
|
|
4518
|
+
curveY: this.preferences.gazeCurveY,
|
|
4519
|
+
curveRadial: this.preferences.gazeCurveRadial,
|
|
4520
|
+
curveSpatial: this.preferences.gazeCurveSpatial,
|
|
4521
|
+
distance: this.preferences.gazeDistance,
|
|
4522
|
+
mode: this.preferences.gazeCurveMode
|
|
4262
4523
|
});
|
|
4263
4524
|
this.scene.motion.setGaze(this.scene.gazeFromAim(aim));
|
|
4264
4525
|
} else {
|
|
@@ -4270,15 +4531,30 @@
|
|
|
4270
4531
|
|
|
4271
4532
|
// ---- session bridge ------------------------------------------------------
|
|
4272
4533
|
|
|
4534
|
+
VrmPet3DWidget.prototype.acknowledgeVisibleConversation = function () {
|
|
4535
|
+
var focused = typeof this.document.hasFocus === 'function' ? this.document.hasFocus() : this.machine.focused;
|
|
4536
|
+
if (!focused || this.document.visibilityState === 'hidden' || this.machine.interaction || !this.conversationVisible()) return;
|
|
4537
|
+
// Read the visible conversation on every host update and pump, regardless of how it was opened.
|
|
4538
|
+
var input = this.aggregate.input;
|
|
4539
|
+
this.aggregate.acknowledgeViewed(selectedSessionId(input.catalog) || input.currentSessionId);
|
|
4540
|
+
};
|
|
4541
|
+
|
|
4273
4542
|
VrmPet3DWidget.prototype.setInput = function (input) {
|
|
4274
4543
|
this.aggregate.setInput(input);
|
|
4544
|
+
this.acknowledgeVisibleConversation();
|
|
4275
4545
|
var snapshot = this.aggregate.snapshot();
|
|
4276
4546
|
this.machine.resolve(snapshot);
|
|
4277
4547
|
};
|
|
4278
4548
|
|
|
4279
4549
|
VrmPet3DWidget.prototype.boundary = function (event) {
|
|
4280
4550
|
this.aggregate.boundary(event);
|
|
4281
|
-
|
|
4551
|
+
if (event && event.sessionId && !this.aggregate.mainSession(event.sessionId)) {
|
|
4552
|
+
this.machine.resolve(this.aggregate.snapshot());
|
|
4553
|
+
return;
|
|
4554
|
+
}
|
|
4555
|
+
this.machine.boundary(this.aggregate.snapshot(), Object.assign({}, event, {
|
|
4556
|
+
confirmed: event && event.reason === 'completed' && this.aggregate.running(event.sessionId) === false,
|
|
4557
|
+
}));
|
|
4282
4558
|
};
|
|
4283
4559
|
|
|
4284
4560
|
VrmPet3DWidget.prototype.resetAggregate = function (identities) {
|
|
@@ -4528,6 +4804,10 @@
|
|
|
4528
4804
|
|
|
4529
4805
|
function selectedSessionId(catalog) {
|
|
4530
4806
|
if (!catalog) return undefined;
|
|
4807
|
+
var visible = Object.values(catalog.byId || {}).find(function (row) {
|
|
4808
|
+
return row && row.retainedBy && Number(row.retainedBy.mainView) > 0;
|
|
4809
|
+
});
|
|
4810
|
+
if (visible && typeof visible.id === 'string') return visible.id;
|
|
4531
4811
|
if (typeof catalog.currentSessionId === 'string') return catalog.currentSessionId;
|
|
4532
4812
|
if (catalog.selectedSessionId) return catalog.selectedSessionId;
|
|
4533
4813
|
if (catalog.current && typeof catalog.current.id === 'string') return catalog.current.id;
|
|
@@ -4598,7 +4878,14 @@
|
|
|
4598
4878
|
return out;
|
|
4599
4879
|
}
|
|
4600
4880
|
|
|
4601
|
-
/**
|
|
4881
|
+
/**
|
|
4882
|
+
* This surface's own words, in the two languages.
|
|
4883
|
+
*
|
|
4884
|
+
* One table per surface, `{ zh: {...}, en: {...} }` with IDENTICAL key sets — the repository's shape (see
|
|
4885
|
+
* `lib/menu.js`'s `LABELS`), so a language half that is missing an entry is a line that reads in the other
|
|
4886
|
+
* language rather than a line that is blank. Read through `words`/`word` at CALL time, never captured into a
|
|
4887
|
+
* variable: the language can change while the pet runs.
|
|
4888
|
+
*/
|
|
4602
4889
|
var STRINGS = {
|
|
4603
4890
|
en: {
|
|
4604
4891
|
/* The floating window's own state, kept as data by `syncDesktop`. */
|
|
@@ -4625,6 +4912,32 @@
|
|
|
4625
4912
|
vrm: 'This VRM model is driven by humanoid bones, so only .vrma clips work here — no .vrma clip was found.',
|
|
4626
4913
|
other: 'The loaded avatar has no skeleton, so no motion clip can drive it. Load a VRM model first.',
|
|
4627
4914
|
},
|
|
4915
|
+
/*
|
|
4916
|
+
* The pet's own OPERATIONS, said in the badge: 跳支舞 with nothing to dance to, and the three moments of
|
|
4917
|
+
* 重载模型. These are the SAME four events the floating window puts in its bubble, so the two surfaces'
|
|
4918
|
+
* wording is kept in step by hand — a drift here is the pet telling the user two different things about one
|
|
4919
|
+
* gesture. `assets/motions/dance` is a path, not words, and is written the same way in both languages.
|
|
4920
|
+
*/
|
|
4921
|
+
danceEmpty: 'No dance clip is available (assets/motions/dance is empty).',
|
|
4922
|
+
reloading: 'Reloading the model…',
|
|
4923
|
+
reloaded: 'Model reloaded',
|
|
4924
|
+
reloadFailed: 'Could not reload the model',
|
|
4925
|
+
/*
|
|
4926
|
+
* DIAGNOSTICS, shown in the badge (a chunk that never arrived, a constructor that threw). `vrmpet3d:` stays
|
|
4927
|
+
* in front of every one of them because it is a greppable marker in a report and a bug report quotes it; the
|
|
4928
|
+
* sentence AFTER it is what a user reads. The names in braces are data — a file, a global, an id, a number.
|
|
4929
|
+
*/
|
|
4930
|
+
mountNoCore: 'lib/core.js is not loaded.',
|
|
4931
|
+
mountNoScene: 'lib/scene.js is not loaded.',
|
|
4932
|
+
placementNoCore: 'Placement needs the core module (stepFall).',
|
|
4933
|
+
chunkNoPublish: '{file} loaded without publishing {global}',
|
|
4934
|
+
chunksMissing: 'DSH did not deliver {list}',
|
|
4935
|
+
catalogRoute: 'catalog route returned HTTP {status}',
|
|
4936
|
+
motionRoute: 'motion route returned HTTP {status}',
|
|
4937
|
+
motionLoad: 'could not load motion {id}',
|
|
4938
|
+
// The two reasons 打开设置 can fail, appended to `configNote` and shown in the badge.
|
|
4939
|
+
settingsNoWindow: 'the launcher could not open a window',
|
|
4940
|
+
settingsUnreachable: 'the host is not reachable',
|
|
4628
4941
|
},
|
|
4629
4942
|
zh: {
|
|
4630
4943
|
/* 悬浮窗自身的状态,由 `syncDesktop` 作为数据保存。 */
|
|
@@ -4648,6 +4961,23 @@
|
|
|
4648
4961
|
vrm: 'VRM 模型使用人形骨骼,因此只能用 .vrma 动作文件——未找到 .vrma 文件。',
|
|
4649
4962
|
other: '当前替身没有骨骼,任何动作文件都无法驱动它。请先加载 VRM 模型。',
|
|
4650
4963
|
},
|
|
4964
|
+
/* 宠物自身的操作提示,写在角标里;与外置悬浮窗气泡里的同一件事保持同一套措辞。路径不翻译。 */
|
|
4965
|
+
danceEmpty: '没有可用的舞蹈动作(assets/motions/dance 是空的)',
|
|
4966
|
+
reloading: '正在重载模型…',
|
|
4967
|
+
reloaded: '模型已重载',
|
|
4968
|
+
reloadFailed: '模型重载失败',
|
|
4969
|
+
/* 诊断信息:角标里给用户看的。`vrmpet3d:` 前缀保留,它是报告里可检索的标记;花括号里是数据。 */
|
|
4970
|
+
mountNoCore: '未加载 lib/core.js。',
|
|
4971
|
+
mountNoScene: '未加载 lib/scene.js。',
|
|
4972
|
+
placementNoCore: 'Placement 需要 core 模块(stepFall)。',
|
|
4973
|
+
chunkNoPublish: '{file} 加载后没有发布 {global}',
|
|
4974
|
+
chunksMissing: 'DSH 没有下发 {list}',
|
|
4975
|
+
catalogRoute: '模型目录路由返回 HTTP {status}',
|
|
4976
|
+
motionRoute: '动作路由返回 HTTP {status}',
|
|
4977
|
+
motionLoad: '无法加载动作 {id}',
|
|
4978
|
+
/* 打开设置失败的两种原因,接在 `configNote` 后面显示在角标里。 */
|
|
4979
|
+
settingsNoWindow: '启动器无法打开窗口',
|
|
4980
|
+
settingsUnreachable: '无法连接到宿主',
|
|
4651
4981
|
}
|
|
4652
4982
|
};
|
|
4653
4983
|
|
|
@@ -4655,18 +4985,6 @@
|
|
|
4655
4985
|
// Plugin
|
|
4656
4986
|
// ===========================================================================
|
|
4657
4987
|
|
|
4658
|
-
function readLanguage(locale) {
|
|
4659
|
-
try {
|
|
4660
|
-
var snapshot = locale && typeof locale.getSnapshot === 'function' ? locale.getSnapshot() : undefined;
|
|
4661
|
-
var value = snapshot && (snapshot.language || snapshot.locale || snapshot.current);
|
|
4662
|
-
if (typeof value === 'string' && value.toLowerCase().indexOf('zh') === 0) return 'zh';
|
|
4663
|
-
var lang = document.documentElement.lang || '';
|
|
4664
|
-
return lang.toLowerCase().indexOf('zh') === 0 ? 'zh' : 'en';
|
|
4665
|
-
} catch (error) {
|
|
4666
|
-
return 'en';
|
|
4667
|
-
}
|
|
4668
|
-
}
|
|
4669
|
-
|
|
4670
4988
|
/**
|
|
4671
4989
|
* Subscribe to live turn boundaries over the plugin's own host service.
|
|
4672
4990
|
*
|
|
@@ -4819,7 +5137,23 @@
|
|
|
4819
5137
|
if (disposed) return;
|
|
4820
5138
|
try {
|
|
4821
5139
|
widget = new VrmPet3DWidget(host, {
|
|
4822
|
-
|
|
5140
|
+
/**
|
|
5141
|
+
* The DSH locale SERVICE, not a snapshot of it: the widget resolves 界面语言 itself — from the
|
|
5142
|
+
* user's `preferences.language`, this locale and the browser's hints — and re-resolves it
|
|
5143
|
+
* whenever the settings change (`adoptLanguage`), because `auto` means "whatever DSH says".
|
|
5144
|
+
*/
|
|
5145
|
+
locale: ctx.locale,
|
|
5146
|
+
conversationVisible: function () {
|
|
5147
|
+
var layout = typeof ctx.get === 'function' ? ctx.get('layout') : ctx.layout;
|
|
5148
|
+
return !layout || !layout.panelInfo || layout.panelInfo.getSnapshot().activePanelId === null;
|
|
5149
|
+
},
|
|
5150
|
+
openSession: function (id) {
|
|
5151
|
+
var entries = ctx.slots.entries('sidebar.workspaces');
|
|
5152
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
5153
|
+
var injected = typeof entries[i].inject === 'function' ? entries[i].inject() : entries[i].inject;
|
|
5154
|
+
if (injected && typeof injected.open === 'function') { injected.open(id); return; }
|
|
5155
|
+
}
|
|
5156
|
+
},
|
|
4823
5157
|
onScopeChange: function (scope) { if (changeScope.current) changeScope.current(scope); },
|
|
4824
5158
|
/**
|
|
4825
5159
|
* Report the state to the host, so the floating window can show the same prompt.
|