dsh-plugin-vrmpet3d 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/desktop/main.js +45 -3
- package/desktop/preload.js +6 -2
- package/lib/client.core.js +209 -13
- package/lib/client.js +278 -42
- package/lib/config-page.html +71 -45
- package/lib/config-page.js +1120 -229
- package/lib/core.js +209 -13
- package/lib/desktop-page.js +144 -8
- package/lib/global-page.html +140 -99
- package/lib/index.js +150 -33
- package/lib/model-picker.js +139 -49
- package/lib/picker-page.html +34 -8
- package/lib/picker-page.js +192 -11
- package/lib/setup-page.html +68 -49
- package/lib/setup-page.js +395 -42
- package/lib/whale.js +191 -0
- package/package.json +1 -1
package/lib/core.js
CHANGED
|
@@ -71,16 +71,32 @@
|
|
|
71
71
|
* The `{duration}` token and its machinery stay — a line the user writes may still carry it (see
|
|
72
72
|
* {@link STATUS_DURATION_TOKEN}) — but the shipped line is DSH's token-free 「深度求索中 ···」, which is also what the
|
|
73
73
|
* app shows while the latest turn is not open.
|
|
74
|
+
*
|
|
75
|
+
* ONE TABLE PER LANGUAGE, keyed state → language, the same shape `SPEECH_LINES` uses for its pools and for the
|
|
76
|
+
* same reason: a status line is TEXT THE USER READS, so an interface in English that still said 「任务达成」 would
|
|
77
|
+
* be a half-translated one. The two halves are written rather than translated word for word, and the Chinese is
|
|
78
|
+
* the operator's own wording, unchanged since 2026-10-13.
|
|
79
|
+
*
|
|
80
|
+
* An EDIT wins over both: `statusEntryFor` only reaches for these while the user's own box is empty, so a line
|
|
81
|
+
* typed by hand is one line in one language and stays exactly that, in either interface language. That is the
|
|
82
|
+
* whole reason this is a per-language DEFAULT rather than a second field in the settings.
|
|
74
83
|
*/
|
|
75
84
|
var DEFAULT_STATUS_TEXT = {
|
|
76
|
-
resting: '',
|
|
77
|
-
working: '深度求索中 ···',
|
|
78
|
-
waiting: '我有问题要问你',
|
|
79
|
-
done: '任务达成',
|
|
80
|
-
error: '遇到问题了...',
|
|
81
|
-
offline: '',
|
|
85
|
+
resting: { zh: '', en: '' },
|
|
86
|
+
working: { zh: '深度求索中 ···', en: 'Working ···' },
|
|
87
|
+
waiting: { zh: '我有问题要问你', en: 'I have a question for you' },
|
|
88
|
+
done: { zh: '任务达成', en: 'Task complete' },
|
|
89
|
+
error: { zh: '遇到问题了...', en: 'Something went wrong...' },
|
|
90
|
+
offline: { zh: '', en: '' },
|
|
82
91
|
};
|
|
83
92
|
|
|
93
|
+
/** One state's shipped line in one language; `''` for a state that says nothing by default. */
|
|
94
|
+
function defaultStatusText(state, language) {
|
|
95
|
+
var entry = DEFAULT_STATUS_TEXT[state];
|
|
96
|
+
if (!entry || typeof entry !== 'object') return '';
|
|
97
|
+
return typeof entry[language === 'en' ? 'en' : 'zh'] === 'string' ? entry[language === 'en' ? 'en' : 'zh'] : '';
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
/**
|
|
85
101
|
* The token a status line may carry where the live elapsed time goes, and the units it is written with.
|
|
86
102
|
*
|
|
@@ -96,6 +112,155 @@
|
|
|
96
112
|
en: { hour: 'h ', minute: 'm ', second: 's' },
|
|
97
113
|
};
|
|
98
114
|
|
|
115
|
+
/**
|
|
116
|
+
* The three answers to 「界面语言」, and the ONE rule that turns them into the language a surface draws in.
|
|
117
|
+
*
|
|
118
|
+
* `auto` is the shipped answer and means "whatever the surface is already running in": DSH's own locale inside
|
|
119
|
+
* the app, the browser's on a host-served page, and on a surface that can see neither (a suite, a headless
|
|
120
|
+
* host) English — the plugin's own fallback rather than a guess about the operator.
|
|
121
|
+
*
|
|
122
|
+
* The other two are the user overruling that, and they exist because the two questions are NOT the same one:
|
|
123
|
+
* DSH can be running in English while its owner wants the pet in Chinese, and the first-run guide is read once,
|
|
124
|
+
* before any of that has settled. Operator, 2026-10-15: 「把全部界面做中英双语自动识别,并在通用设置和初始安装
|
|
125
|
+
* 引导页的第一页欢迎页面中加上语言选择」.
|
|
126
|
+
*
|
|
127
|
+
* `auto` is STORED as `auto`, never resolved on write. A profile that stored the resolved 'zh' would freeze the
|
|
128
|
+
* language on the day it was created, and the user who later switched DSH to English would keep a Chinese pet
|
|
129
|
+
* with no way to tell that from a deliberate choice — the same mistake `DEFAULTS.petSurface` documents for
|
|
130
|
+
* 显示位置, and it is why this is a preference the CLEANER knows about rather than a value a page writes.
|
|
131
|
+
*/
|
|
132
|
+
var LANGUAGE_SETTINGS = ['auto', 'zh', 'en'];
|
|
133
|
+
|
|
134
|
+
/** `'auto'` for anything that is not one of the three — an unknown stored value is a reset, not an error. */
|
|
135
|
+
function cleanLanguage(value) {
|
|
136
|
+
return LANGUAGE_SETTINGS.indexOf(value) === -1 ? 'auto' : value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Which of the two languages this surface draws in, given the user's setting and what the environment says.
|
|
141
|
+
*
|
|
142
|
+
* `hints` is a LIST, most authoritative first — DSH's locale, then the document's own `lang`, then
|
|
143
|
+
* `navigator.language` — and the FIRST hint that says anything decides. That order is the rule and not a detail:
|
|
144
|
+
* DSH is the application the pet lives inside, so its language outranks the browser's, and a page DSH has
|
|
145
|
+
* already marked `lang="zh"` outranks a browser whose own preference is en-US.
|
|
146
|
+
*
|
|
147
|
+
* The first hint decides the ZH-or-not question instead of the chain continuing past it, because this plugin has
|
|
148
|
+
* exactly two languages. A hint of `fr` is not a reason to go looking for a Chinese one further down the chain;
|
|
149
|
+
* it is a reason to fall back to English, which is also what `auto` with nothing to go on means.
|
|
150
|
+
*/
|
|
151
|
+
function resolveLanguage(preference, hints) {
|
|
152
|
+
var wanted = cleanLanguage(preference);
|
|
153
|
+
if (wanted !== 'auto') return wanted;
|
|
154
|
+
var list = Array.isArray(hints) ? hints : [hints];
|
|
155
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
156
|
+
var hint = list[i];
|
|
157
|
+
if (typeof hint !== 'string' || !hint) continue;
|
|
158
|
+
return /^(zh|cn|chinese)/i.test(hint.trim()) ? 'zh' : 'en';
|
|
159
|
+
}
|
|
160
|
+
return 'en';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The environment's own two hints, in the order they outrank each other — what a surface that has no DSH locale
|
|
165
|
+
* to ask uses. `scope` exists so a suite can hand in a fake window instead of moving the real one.
|
|
166
|
+
*
|
|
167
|
+
* The BROWSER comes first, and the document's own `lang` second, which is the opposite of the order the app's
|
|
168
|
+
* widget uses — deliberately. Every page this plugin serves ships `<html lang="zh">` (the plugin was written in
|
|
169
|
+
* Chinese), so a page that read its own markup first could never be auto-detected as English: the hint would be
|
|
170
|
+
* its own default rather than anything about the user. `navigator.language` is the browser's answer and in the
|
|
171
|
+
* floating window it is Electron's, i.e. the machine's.
|
|
172
|
+
*/
|
|
173
|
+
function browserLanguageHints(scope) {
|
|
174
|
+
var win = scope || (typeof globalThis !== 'undefined' ? globalThis : null);
|
|
175
|
+
var doc = win && win.document;
|
|
176
|
+
var nav = win && win.navigator;
|
|
177
|
+
return [
|
|
178
|
+
nav ? (nav.language || (nav.languages && nav.languages[0])) : null,
|
|
179
|
+
doc && doc.documentElement ? doc.documentElement.lang : null,
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* One surface's own wording, in the two languages, chosen by {@link resolveLanguage}.
|
|
185
|
+
*
|
|
186
|
+
* Every surface keeps its OWN table — `lib/menu.js`'s `LABELS`, `lib/model-picker.js`'s `TEXT`, the settings
|
|
187
|
+
* pages' `TEXT` — rather than one global dictionary, because that is already this repository's shape (a surface
|
|
188
|
+
* owns the words it draws) and because a single table for six surfaces would make every one of them edit the
|
|
189
|
+
* same file. What IS shared is the SHAPE: `{ zh: {...}, en: {...} }`, this accessor, and {@link applyI18n}.
|
|
190
|
+
*
|
|
191
|
+
* A language half that is missing or empty falls back to the other one rather than to blank: a half-translated
|
|
192
|
+
* surface reads as Chinese in one spot, which is a bug somebody reports, where a blank label is a page that
|
|
193
|
+
* looks broken. `textOf` below is what makes a GAP visible, and a suite is what keeps one from shipping.
|
|
194
|
+
*/
|
|
195
|
+
function textFor(table, language) {
|
|
196
|
+
if (!table || typeof table !== 'object') return {};
|
|
197
|
+
var wanted = language === 'en' ? table.en : table.zh;
|
|
198
|
+
var other = language === 'en' ? table.zh : table.en;
|
|
199
|
+
if (wanted && typeof wanted === 'object') return wanted;
|
|
200
|
+
return other && typeof other === 'object' ? other : {};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** One entry out of a surface's table. A missing key returns the KEY, so a gap is visible on screen. */
|
|
204
|
+
function textOf(table, language, key) {
|
|
205
|
+
var value = textFor(table, language)[key];
|
|
206
|
+
return typeof value === 'string' ? value : key;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Fill a document's marked-up text from one surface's table.
|
|
211
|
+
*
|
|
212
|
+
* The markup carries the CHINESE text as well as the key, and that is on purpose: this plugin's pages all ship a
|
|
213
|
+
* bootstrap watchdog for the case where a script never ran, so a page whose script DID not run should still say
|
|
214
|
+
* something in one language rather than show nothing. `verify-desktop.mjs` asserts the two agree, which is what
|
|
215
|
+
* stops the markup and the table from drifting apart.
|
|
216
|
+
*
|
|
217
|
+
* Four attributes, one per place text lives: `data-i18n` writes `textContent`, `data-i18n-html` writes
|
|
218
|
+
* `innerHTML` and is for OUR OWN sentence with a `<strong>` in it (never for anything a user or a model wrote),
|
|
219
|
+
* and `data-i18n-placeholder` / `data-i18n-title` / `data-i18n-aria` write those three attributes.
|
|
220
|
+
*
|
|
221
|
+
* A key the table does not have leaves the markup ALONE rather than printing the key: the visible text stays the
|
|
222
|
+
* Chinese fallback, and the missing entry is the suite's business, not the user's.
|
|
223
|
+
*
|
|
224
|
+
* @returns {number} how many places were rewritten, so a suite can tell "applied nothing" from "no markup".
|
|
225
|
+
*/
|
|
226
|
+
function applyI18n(root, table, language) {
|
|
227
|
+
var doc = root && typeof root.querySelectorAll === 'function'
|
|
228
|
+
? root
|
|
229
|
+
: (typeof globalThis !== 'undefined' ? globalThis.document : null);
|
|
230
|
+
if (!doc || typeof doc.querySelectorAll !== 'function') return 0;
|
|
231
|
+
var words = textFor(table, language);
|
|
232
|
+
var selector = '[data-i18n],[data-i18n-html],[data-i18n-placeholder],[data-i18n-title],[data-i18n-aria]';
|
|
233
|
+
var nodes;
|
|
234
|
+
try {
|
|
235
|
+
nodes = doc.querySelectorAll(selector);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
var applied = 0;
|
|
240
|
+
var write = function (node, attribute, key, html) {
|
|
241
|
+
var value = words[key];
|
|
242
|
+
if (typeof value !== 'string') return;
|
|
243
|
+
if (attribute === 'data-i18n') node.textContent = value;
|
|
244
|
+
else if (html) node.innerHTML = value;
|
|
245
|
+
else node.setAttribute(attribute, value);
|
|
246
|
+
applied += 1;
|
|
247
|
+
};
|
|
248
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
249
|
+
var node = nodes[i];
|
|
250
|
+
var key = node.getAttribute('data-i18n');
|
|
251
|
+
if (key) write(node, 'data-i18n', key, false);
|
|
252
|
+
key = node.getAttribute('data-i18n-html');
|
|
253
|
+
if (key) write(node, 'data-i18n-html', key, true);
|
|
254
|
+
key = node.getAttribute('data-i18n-placeholder');
|
|
255
|
+
if (key) write(node, 'placeholder', key, false);
|
|
256
|
+
key = node.getAttribute('data-i18n-title');
|
|
257
|
+
if (key) write(node, 'title', key, false);
|
|
258
|
+
key = node.getAttribute('data-i18n-aria');
|
|
259
|
+
if (key) write(node, 'aria-label', key, false);
|
|
260
|
+
}
|
|
261
|
+
return applied;
|
|
262
|
+
}
|
|
263
|
+
|
|
99
264
|
/**
|
|
100
265
|
* The states whose prompt carries DSH's whale mark.
|
|
101
266
|
*
|
|
@@ -1080,6 +1245,12 @@
|
|
|
1080
1245
|
/** Default pose and behaviour knobs. */
|
|
1081
1246
|
var DEFAULTS = {
|
|
1082
1247
|
scope: 'global',
|
|
1248
|
+
/**
|
|
1249
|
+
* 界面语言 — `auto` follows the surface's own environment ({@link resolveLanguage}), and 中文 / English overrule
|
|
1250
|
+
* it. It sits first in the table rather than beside the appearance rows because it is the one preference that
|
|
1251
|
+
* decides how every OTHER row is spelled.
|
|
1252
|
+
*/
|
|
1253
|
+
language: 'auto',
|
|
1083
1254
|
size: 100,
|
|
1084
1255
|
/**
|
|
1085
1256
|
* WHERE the pet is shown, as the cleaner's DEFAULT rather than a string repeated beside it.
|
|
@@ -1925,6 +2096,13 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
1925
2096
|
var out = {};
|
|
1926
2097
|
var source = raw && typeof raw === 'object' ? raw : {};
|
|
1927
2098
|
out.scope = source.scope === 'current' ? 'current' : 'global';
|
|
2099
|
+
/**
|
|
2100
|
+
* 界面语言 — cleaned like every other enumerated preference: an unknown value (an older profile, a typo, a page
|
|
2101
|
+
* that sent the wrong thing) is `auto`, which resolves to the surface's own environment anyway. It is NOT
|
|
2102
|
+
* counted as authorship by `authoredWeight` over in `lib/settings.js`: a language is a way of looking at the
|
|
2103
|
+
* pet, not something the user wrote into it.
|
|
2104
|
+
*/
|
|
2105
|
+
out.language = cleanLanguage(source.language);
|
|
1928
2106
|
// An unknown surface falls back to the shipped default, not to a string repeated here.
|
|
1929
2107
|
out.petSurface = PET_SURFACES.indexOf(source.petSurface) === -1 ? DEFAULTS.petSurface : source.petSurface;
|
|
1930
2108
|
out.size = Number.isFinite(Number(source.size)) ? clamp(Number(source.size), SIZE_RANGE.min, SIZE_RANGE.max) : DEFAULTS.size;
|
|
@@ -2480,8 +2658,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2480
2658
|
// ---------------------------------------------------------------------------
|
|
2481
2659
|
|
|
2482
2660
|
/** One state's status line, exactly as the renderer draws it: text, then the tag. */
|
|
2483
|
-
function statusLineFor(preferences, state) {
|
|
2484
|
-
var entry = statusEntryFor(preferences, state);
|
|
2661
|
+
function statusLineFor(preferences, state, language) {
|
|
2662
|
+
var entry = statusEntryFor(preferences, state, language);
|
|
2485
2663
|
if (!entry) return '';
|
|
2486
2664
|
var tag = typeof entry.tag === 'string' ? entry.tag : '';
|
|
2487
2665
|
return (entry.text + tag).slice(0, STATUS_LIMITS.textLength + STATUS_LIMITS.tagLength);
|
|
@@ -2493,11 +2671,15 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2493
2671
|
* Always an OBJECT, never null: every state has something to say or an explicit reason to say nothing,
|
|
2494
2672
|
* and a caller that had to distinguish "unconfigured" from "configured empty" would get it wrong in the
|
|
2495
2673
|
* one case that matters (a user who cleared a line).
|
|
2674
|
+
*
|
|
2675
|
+
* `language` picks WHICH shipped default fills an empty box — see {@link DEFAULT_STATUS_TEXT}. It is the third
|
|
2676
|
+
* parameter and optional, so a caller that does not know the surface's language keeps the Chinese default the
|
|
2677
|
+
* suites and the older call sites already pin.
|
|
2496
2678
|
*/
|
|
2497
|
-
function statusEntryFor(preferences, state) {
|
|
2679
|
+
function statusEntryFor(preferences, state, language) {
|
|
2498
2680
|
var map = preferences && preferences.status && typeof preferences.status === 'object' ? preferences.status : {};
|
|
2499
2681
|
var stored = map[state] && typeof map[state] === 'object' ? map[state] : {};
|
|
2500
|
-
var fallback =
|
|
2682
|
+
var fallback = defaultStatusText(state, language);
|
|
2501
2683
|
var text = typeof stored.text === 'string' ? stored.text : fallback;
|
|
2502
2684
|
var fallbackAnimation = Object.prototype.hasOwnProperty.call(DEFAULT_STATUS_ANIMATION, state)
|
|
2503
2685
|
? DEFAULT_STATUS_ANIMATION[state]
|
|
@@ -3626,11 +3808,17 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3626
3808
|
* clause is DROPPED — DSH's own token-free 「深度求索中」 rather than a number invented here. Passed per call so the
|
|
3627
3809
|
* two surfaces (the app's pet and the floating window) can only differ by what they are told, never by the rule.
|
|
3628
3810
|
*
|
|
3811
|
+
* `language` is the SIXTH parameter and it reaches two places: which shipped default an empty box falls back to
|
|
3812
|
+
* ({@link DEFAULT_STATUS_TEXT}), and the units a `{duration}` clause is written in — DSH writes 1分30秒 in one
|
|
3813
|
+
* language and 1m 30s in the other, so a line translated without its clock would read as a foreign number.
|
|
3814
|
+
* Optional, like the status-entry language above: a caller that has no language to pass keeps the Chinese answer
|
|
3815
|
+
* every existing suite pins.
|
|
3816
|
+
*
|
|
3629
3817
|
* @returns {{state: string, text: string, tag: string, animation: string, seconds: number}|null}
|
|
3630
3818
|
*/
|
|
3631
|
-
function statusFor(preferences, state, enteredAt, now, clock) {
|
|
3819
|
+
function statusFor(preferences, state, enteredAt, now, clock, language) {
|
|
3632
3820
|
if (STATUS_STATES.indexOf(state) === -1) return null;
|
|
3633
|
-
var entry = statusEntryFor(preferences, state);
|
|
3821
|
+
var entry = statusEntryFor(preferences, state, language);
|
|
3634
3822
|
if (!entry.text && !entry.tag) return null;
|
|
3635
3823
|
var at = Number.isFinite(now) ? now : Date.now();
|
|
3636
3824
|
/**
|
|
@@ -3683,7 +3871,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3683
3871
|
}
|
|
3684
3872
|
text = elapsed === null
|
|
3685
3873
|
? durationClauseRemoved(text)
|
|
3686
|
-
: text.split(STATUS_DURATION_TOKEN).join(formatRunDuration(elapsed));
|
|
3874
|
+
: text.split(STATUS_DURATION_TOKEN).join(formatRunDuration(elapsed, language));
|
|
3687
3875
|
}
|
|
3688
3876
|
var look = statusTone(state);
|
|
3689
3877
|
return {
|
|
@@ -7299,6 +7487,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
7299
7487
|
STATES: STATES,
|
|
7300
7488
|
INTERACTION_STATES: INTERACTION_STATES,
|
|
7301
7489
|
STATUS_STATES: STATUS_STATES,
|
|
7490
|
+
LANGUAGE_SETTINGS: LANGUAGE_SETTINGS,
|
|
7491
|
+
cleanLanguage: cleanLanguage,
|
|
7492
|
+
resolveLanguage: resolveLanguage,
|
|
7493
|
+
browserLanguageHints: browserLanguageHints,
|
|
7494
|
+
textFor: textFor,
|
|
7495
|
+
textOf: textOf,
|
|
7496
|
+
applyI18n: applyI18n,
|
|
7497
|
+
defaultStatusText: defaultStatusText,
|
|
7302
7498
|
DEFAULT_STATUS_TEXT: DEFAULT_STATUS_TEXT,
|
|
7303
7499
|
STATUS_TAGS: STATUS_TAGS,
|
|
7304
7500
|
STATUS_ANIMATIONS: STATUS_ANIMATIONS,
|
package/lib/desktop-page.js
CHANGED
|
@@ -65,6 +65,49 @@
|
|
|
65
65
|
*/
|
|
66
66
|
var MENU_MARGIN = 4;
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* This page's own words, in the two languages — `{ zh: {...}, en: {...} }` with IDENTICAL key sets, the
|
|
70
|
+
* repository's per-surface shape (`lib/menu.js`'s `LABELS`, `lib/client.js`'s `STRINGS`).
|
|
71
|
+
*
|
|
72
|
+
* These four are the only strings this page PAINTS that are its own rather than the app's: the three moments
|
|
73
|
+
* of 重载模型 and 跳支舞 with nothing to dance to, all of them said in the pet's bubble (`showLocalSpeech`).
|
|
74
|
+
* The app's pet says the same four things in its badge for the same gestures, so the wording here is kept in
|
|
75
|
+
* step with that table BY HAND — a drift is one gesture answered in two different sentences. `assets/motions/dance`
|
|
76
|
+
* is a path rather than words and is written the same way in both halves.
|
|
77
|
+
*
|
|
78
|
+
* Read at CALL time through `word`: `preferences.language` can change while this window runs.
|
|
79
|
+
*/
|
|
80
|
+
var TEXT = {
|
|
81
|
+
zh: {
|
|
82
|
+
reloading: '正在重载模型…',
|
|
83
|
+
reloaded: '模型已重载',
|
|
84
|
+
reloadFailed: '模型重载失败',
|
|
85
|
+
danceEmpty: '没有可用的舞蹈动作(assets/motions/dance 是空的)',
|
|
86
|
+
},
|
|
87
|
+
en: {
|
|
88
|
+
reloading: 'Reloading the model…',
|
|
89
|
+
reloaded: 'Model reloaded',
|
|
90
|
+
reloadFailed: 'Could not reload the model',
|
|
91
|
+
danceEmpty: 'No dance clip is available (assets/motions/dance is empty).',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One entry out of {@link TEXT}, in the language on screen — read at CALL time, never captured.
|
|
97
|
+
*
|
|
98
|
+
* `'en'` is the only value that selects the English half: every language this page resolves is exactly 'zh' or
|
|
99
|
+
* 'en' (see the mount), and anything unexpected reads the Chinese half, which is the fallback the markup and
|
|
100
|
+
* `lib/menu.js`'s own `strings()` already use. An entry missing from THAT half falls back to the other one, and
|
|
101
|
+
* an entry missing from both returns the key — a gap that SHOWS rather than `undefined` drawn in the pet's
|
|
102
|
+
* bubble, which is the rule `core.textOf` follows.
|
|
103
|
+
*/
|
|
104
|
+
function word(language, key) {
|
|
105
|
+
var half = language === 'en' ? TEXT.en : TEXT.zh;
|
|
106
|
+
if (typeof half[key] === 'string') return half[key];
|
|
107
|
+
var other = language === 'en' ? TEXT.zh : TEXT.en;
|
|
108
|
+
return typeof other[key] === 'string' ? other[key] : key;
|
|
109
|
+
}
|
|
110
|
+
|
|
68
111
|
/**
|
|
69
112
|
* This page's own stylesheet, injected once.
|
|
70
113
|
*
|
|
@@ -593,8 +636,21 @@
|
|
|
593
636
|
// 是不对的。删掉两个单独的设置面板。」); every setting it used to offer is on one of the two settings pages.
|
|
594
637
|
// ---------------------------------------------------------------------------------------
|
|
595
638
|
var doc = canvas.ownerDocument || globalThis.document || null;
|
|
596
|
-
|
|
597
|
-
|
|
639
|
+
/**
|
|
640
|
+
* 界面语言, resolved through the ONE shared rule rather than guessed here.
|
|
641
|
+
*
|
|
642
|
+
* An explicit `preferences.language` wins, and `auto` follows the FIRST hint that says anything — and this
|
|
643
|
+
* page has no DSH locale to ask (it is served by the launcher, not by DSH), so the hints are the browser's own
|
|
644
|
+
* two: `navigator.language`, which in this window is Electron's and therefore the machine's, and then the
|
|
645
|
+
* document's `lang`. English is what nothing-at-all answers.
|
|
646
|
+
*
|
|
647
|
+
* A core chunk that predates the rule (the stale-delivery case this repository has eaten more than once) falls
|
|
648
|
+
* back to the browser hint this page used before, so an older host costs the SHARING of the rule, not the
|
|
649
|
+
* language. The preference is re-read by `adoptLanguage` whenever the settings change.
|
|
650
|
+
*/
|
|
651
|
+
var language = typeof core.resolveLanguage === 'function'
|
|
652
|
+
? core.resolveLanguage(preferences.language, core.browserLanguageHints())
|
|
653
|
+
: ((globalThis.navigator && /^zh/i.test(globalThis.navigator.language || '')) ? 'zh' : 'en');
|
|
598
654
|
installDesktopStyles(doc);
|
|
599
655
|
var menu = null;
|
|
600
656
|
var menuOpen = false;
|
|
@@ -725,6 +781,13 @@
|
|
|
725
781
|
*/
|
|
726
782
|
function buildMenu() {
|
|
727
783
|
var api = globalThis.__VRMPET3D_MENU__;
|
|
784
|
+
/**
|
|
785
|
+
* A REBUILD replaces the element rather than leaving the old one in the body: the menu carries its words
|
|
786
|
+
* from `createMenu` and cannot be re-worded in place, so `adoptLanguage` builds a new one — and a second
|
|
787
|
+
* copy would keep its own document listeners. `menuOpen`/`menuBox` are the page's own and are cleared by
|
|
788
|
+
* `closeMenu`/`onMenuClosed` before this runs.
|
|
789
|
+
*/
|
|
790
|
+
if (menu && typeof menu.destroy === 'function') menu.destroy();
|
|
728
791
|
if (!api || typeof api.createMenu !== 'function' || !doc) return null;
|
|
729
792
|
api.injectStyle(doc, 'vrmpet3d-menu-style');
|
|
730
793
|
return api.createMenu({
|
|
@@ -836,6 +899,40 @@
|
|
|
836
899
|
}
|
|
837
900
|
|
|
838
901
|
menu = buildMenu();
|
|
902
|
+
/**
|
|
903
|
+
* Follow 界面语言 — the preference, this page's environment, and its own four sentences.
|
|
904
|
+
*
|
|
905
|
+
* Called from every path that replaces `preferences` (a host snapshot, a storage write from the settings
|
|
906
|
+
* window), because the language is a preference like any other and a change made there has to reach this
|
|
907
|
+
* window without a reload. 'auto' means what the browser says here, and the browser can be re-read at any
|
|
908
|
+
* time; only a CHANGE does anything, since this rebuilds the menu.
|
|
909
|
+
*
|
|
910
|
+
* What a change touches, and why exactly these:
|
|
911
|
+
* * `language` is read at CALL time by everything that words this page (the say request, the shipped pool
|
|
912
|
+
* for a touched part, `core.statusFor`'s default line and its clock units, the published readout, the four
|
|
913
|
+
* bubble sentences), so a new value reaches all of them by itself;
|
|
914
|
+
* * the MENU carries its labels from `createMenu`, so it is rebuilt — and a menu that is OPEN is closed
|
|
915
|
+
* first, because its rows are the old words and its closing is what gives the window its pet-sized box
|
|
916
|
+
* back;
|
|
917
|
+
* * the prompt is redrawn here rather than on the next frame: this page's frame loop re-renders on a cached
|
|
918
|
+
* key, and a language change does not alter the STATE, so the cached line would otherwise keep the old
|
|
919
|
+
* words until the state changed.
|
|
920
|
+
*/
|
|
921
|
+
function adoptLanguage() {
|
|
922
|
+
var next = typeof core.resolveLanguage === 'function'
|
|
923
|
+
? core.resolveLanguage(preferences.language, core.browserLanguageHints())
|
|
924
|
+
: ((globalThis.navigator && /^zh/i.test(globalThis.navigator.language || '')) ? 'zh' : 'en');
|
|
925
|
+
if (next === language) return;
|
|
926
|
+
language = next;
|
|
927
|
+
if (menuOpen) closeMenu();
|
|
928
|
+
menu = buildMenu();
|
|
929
|
+
statusKey = null;
|
|
930
|
+
renderStatus();
|
|
931
|
+
layout();
|
|
932
|
+
reportRect();
|
|
933
|
+
publish();
|
|
934
|
+
}
|
|
935
|
+
|
|
839
936
|
/**
|
|
840
937
|
* The model catalog, fetched once — and re-read whenever the store names a model this list cannot answer for
|
|
841
938
|
* (`refreshCatalog`). See the snapshot handler for the import-while-running case that made that necessary.
|
|
@@ -1228,7 +1325,12 @@
|
|
|
1228
1325
|
body: JSON.stringify({
|
|
1229
1326
|
state: hostState || null,
|
|
1230
1327
|
event: stateEventName(),
|
|
1231
|
-
|
|
1328
|
+
/**
|
|
1329
|
+
* The language this page is DRAWN in, not `preferences.language` raw: 'auto' is not a language, and a
|
|
1330
|
+
* host asked for a line in 'auto' would have to guess at exactly the thing this page has already
|
|
1331
|
+
* resolved. The host's prompt is built from this, so a line arrives in the language on screen.
|
|
1332
|
+
*/
|
|
1333
|
+
language: language,
|
|
1232
1334
|
part: item.id,
|
|
1233
1335
|
structure: structure || null,
|
|
1234
1336
|
chain: chain || null,
|
|
@@ -1413,7 +1515,7 @@
|
|
|
1413
1515
|
if (!statusNode) return;
|
|
1414
1516
|
// The TURN clock the app sent with the state, not this page's own age for it: 用时 is a claim about DSH's turn,
|
|
1415
1517
|
// and with no turn to report the token is dropped (DSH's own token-free 「深度求索中」). See `PetMachine.statusClock`.
|
|
1416
|
-
var line = core.statusFor(preferences, hostState, hostStateAt, Date.now(), hostStateClock);
|
|
1518
|
+
var line = core.statusFor(preferences, hostState, hostStateAt, Date.now(), hostStateClock, language);
|
|
1417
1519
|
if (!line) {
|
|
1418
1520
|
statusKey = null;
|
|
1419
1521
|
statusShown = null;
|
|
@@ -1610,12 +1712,15 @@
|
|
|
1610
1712
|
if (!id) return Promise.resolve(null);
|
|
1611
1713
|
if (typeof scene.bustModelCache === 'function') scene.bustModelCache();
|
|
1612
1714
|
// The bubble is this page's own way of saying something transient — the same channel a touched part uses.
|
|
1613
|
-
|
|
1715
|
+
// The three lines of 重载模型 come from this page's own table, and are the same three the app's pet puts in
|
|
1716
|
+
// its badge for the same menu row.
|
|
1717
|
+
showLocalSpeech(word(language, 'reloading'), 3);
|
|
1614
1718
|
return loadModelSerialized(id).then(function (status) {
|
|
1615
1719
|
layout();
|
|
1616
1720
|
scene.applyPreferences(preferences);
|
|
1617
1721
|
publish();
|
|
1618
|
-
showLocalSpeech(status && status.ok ?
|
|
1722
|
+
showLocalSpeech(status && status.ok ? word(language, 'reloaded')
|
|
1723
|
+
: ((status && status.error) || word(language, 'reloadFailed')), 3);
|
|
1619
1724
|
return status;
|
|
1620
1725
|
});
|
|
1621
1726
|
}
|
|
@@ -1638,9 +1743,13 @@
|
|
|
1638
1743
|
&& prefixes.some(function (prefix) { return String(entry.name || '').indexOf(prefix) === 0; });
|
|
1639
1744
|
}).map(function (entry) { return entry.id; });
|
|
1640
1745
|
if (!ids.length) {
|
|
1641
|
-
|
|
1746
|
+
// The same sentence the app's pet shows in its badge for its own 跳支舞 row.
|
|
1747
|
+
showLocalSpeech(word(language, 'danceEmpty'), 3);
|
|
1642
1748
|
return Promise.resolve(null);
|
|
1643
1749
|
}
|
|
1750
|
+
var current = dancePreview && dancePreview.motionId;
|
|
1751
|
+
var alternatives = ids.filter(function (id) { return id !== current; });
|
|
1752
|
+
if (alternatives.length) ids = alternatives;
|
|
1644
1753
|
var picked = ids[Math.floor(Math.random() * ids.length)];
|
|
1645
1754
|
return fetch('/vrmpet3d/preview', {
|
|
1646
1755
|
method: 'POST',
|
|
@@ -2448,7 +2557,7 @@
|
|
|
2448
2557
|
*
|
|
2449
2558
|
* Sent ONCE per dance: the flag resets as soon as the drawn state leaves 跳舞, so a finished clip cannot post every frame.
|
|
2450
2559
|
*/
|
|
2451
|
-
if (drawn === 'dancing' && scene.playback && scene.playback.
|
|
2560
|
+
if (drawn === 'dancing' && scene.playback && scene.playback.state === 'dancing' && scene.playback.record && scene.motionFinished(scene.playback.record)) {
|
|
2452
2561
|
if (!danceEndedReported) {
|
|
2453
2562
|
danceEndedReported = true;
|
|
2454
2563
|
try {
|
|
@@ -3410,6 +3519,19 @@
|
|
|
3410
3519
|
canvas.addEventListener('pointermove', onPointerMove);
|
|
3411
3520
|
canvas.addEventListener('pointerup', onPointerUp);
|
|
3412
3521
|
canvas.addEventListener('pointercancel', onPointerUp);
|
|
3522
|
+
function onDoubleClick(event) {
|
|
3523
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
3524
|
+
if (hostState !== 'dancing' && !dancePreview) return;
|
|
3525
|
+
dancePreview = null;
|
|
3526
|
+
if (hostState === 'dancing') hostState = 'resting';
|
|
3527
|
+
scene.setPreview(null);
|
|
3528
|
+
scene.setState('resting');
|
|
3529
|
+
fetch('/vrmpet3d/preview', {
|
|
3530
|
+
method: 'POST', credentials: 'omit',
|
|
3531
|
+
headers: { 'content-type': 'application/json' }, body: '{}',
|
|
3532
|
+
}).catch(function () { return null; });
|
|
3533
|
+
}
|
|
3534
|
+
canvas.addEventListener('dblclick', onDoubleClick);
|
|
3413
3535
|
canvas.addEventListener('wheel', onWheel, { passive: false });
|
|
3414
3536
|
canvas.addEventListener('contextmenu', onContextMenu);
|
|
3415
3537
|
/**
|
|
@@ -3558,6 +3680,12 @@
|
|
|
3558
3680
|
placement.preferences = next;
|
|
3559
3681
|
placement.setSize(client.BASE_PET_SIZE.width, client.BASE_PET_SIZE.height);
|
|
3560
3682
|
placement.clampToViewport();
|
|
3683
|
+
/**
|
|
3684
|
+
* 界面语言 is a preference like the rest, and only now is the answer readable — `next.language` may be an
|
|
3685
|
+
* explicit 中文/English or `auto`, which this page answers from its own environment. After the size clamp,
|
|
3686
|
+
* because a language change lays the window out again.
|
|
3687
|
+
*/
|
|
3688
|
+
adoptLanguage();
|
|
3561
3689
|
/**
|
|
3562
3690
|
* 重力 comes from the store like everything else, and it is PHYSICS rather than a value the scene
|
|
3563
3691
|
* re-applies — so it has to be pushed here too. Without this line the switch would work until the next
|
|
@@ -3633,6 +3761,10 @@
|
|
|
3633
3761
|
onPreview: function (preview) {
|
|
3634
3762
|
dancePreview = preview && preview.state === 'dancing' ? preview : null;
|
|
3635
3763
|
if (dancePreview) {
|
|
3764
|
+
// Each preview notification is a NEW run, including the same sole clip.
|
|
3765
|
+
danceEndedReported = false;
|
|
3766
|
+
scene.setPreview(null);
|
|
3767
|
+
if (scene.playback && scene.playback.state === 'dancing') scene.stopPlayback(null);
|
|
3636
3768
|
ensureMotion(preview.motionId, false);
|
|
3637
3769
|
return;
|
|
3638
3770
|
}
|
|
@@ -3717,6 +3849,9 @@
|
|
|
3717
3849
|
placement.preferences = next;
|
|
3718
3850
|
placement.setSize(client.BASE_PET_SIZE.width, client.BASE_PET_SIZE.height);
|
|
3719
3851
|
placement.clampToViewport();
|
|
3852
|
+
// A storage write is the fallback path for a host without the settings route, and the settings window is
|
|
3853
|
+
// where 界面语言 is chosen: the same re-resolution the snapshot path does, after the size clamp.
|
|
3854
|
+
adoptLanguage();
|
|
3720
3855
|
// The scene re-reads the whole preference set: springs and expressions are applied from it, so
|
|
3721
3856
|
// a change made in the settings window lands on the pet through this one call.
|
|
3722
3857
|
scene.applyPreferences(Object.assign({}, next, { motions: effectiveMotions() }));
|
|
@@ -3909,6 +4044,7 @@
|
|
|
3909
4044
|
canvas.removeEventListener('pointermove', onPointerMove);
|
|
3910
4045
|
canvas.removeEventListener('pointerup', onPointerUp);
|
|
3911
4046
|
canvas.removeEventListener('pointercancel', onPointerUp);
|
|
4047
|
+
canvas.removeEventListener('dblclick', onDoubleClick);
|
|
3912
4048
|
canvas.removeEventListener('wheel', onWheel);
|
|
3913
4049
|
canvas.removeEventListener('contextmenu', onContextMenu);
|
|
3914
4050
|
if (menu && typeof menu.destroy === 'function') menu.destroy();
|