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/README.md
CHANGED
|
@@ -42,6 +42,7 @@ no runtime dependencies · works offline
|
|
|
42
42
|
| 互动 | 点击与长按部位触发表情和碎碎念;拖动、缩放、旋转视角、视线跟随和弹簧骨物理 |
|
|
43
43
|
| 碎碎念 | 内置台词、自定义台词,或使用 DSH 模型 / OpenAI 兼容接口生成 AI 台词 |
|
|
44
44
|
| 导入 | 模型以文件导入、动作包以文件夹导入,复制到插件存储后不依赖原路径 |
|
|
45
|
+
| 界面语言 | 全部界面中英双语,默认跟随系统 / DSH 自动识别;「通用设置 → 显示与外观」和初次引导欢迎页各有一个语言选择(自动 / 中文 / English) |
|
|
45
46
|
|
|
46
47
|
## 运行要求
|
|
47
48
|
|
|
@@ -155,6 +156,7 @@ The marketplace screenshot order is declared in the root [screenshots.json](scre
|
|
|
155
156
|
| Interaction | Click or hold body parts for expressions and chatter; drag, resize, rotate the view, follow the pointer and use spring-bone physics |
|
|
156
157
|
| Chatter | Built-in or custom lines, or AI-generated lines using a DSH model or an OpenAI-compatible service |
|
|
157
158
|
| Import | Import models as files and motion packs as folders. Imported files are copied into plugin storage and no longer depend on their original paths |
|
|
159
|
+
| Language | The whole interface is Chinese and English, auto-detected from the system or DSH by default; a chooser (Auto / 中文 / English) sits in General settings → Display, and on the first-run guide's welcome screen |
|
|
158
160
|
|
|
159
161
|
### Requirements
|
|
160
162
|
|
package/desktop/main.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Everything here was measured before it was written (see HANDOFF §12): a transparent
|
|
9
9
|
// always-on-top window renders WebGL with a real alpha channel, can be resized to the whole
|
|
10
10
|
// screen, and `capturePage()` returns pixels that are mostly fully transparent.
|
|
11
|
-
const { app, BrowserWindow, dialog, ipcMain, screen } = require('electron');
|
|
11
|
+
const { app, BrowserWindow, dialog, ipcMain, screen, shell } = require('electron');
|
|
12
12
|
const fs = require('node:fs');
|
|
13
13
|
const path = require('node:path');
|
|
14
14
|
|
|
@@ -37,6 +37,47 @@ const DEFAULT_PET_WINDOW = { width: 240, height: 320 };
|
|
|
37
37
|
*/
|
|
38
38
|
const VRMPET3D_STATUS_PREFIX = 'vrmpet3d-desktop: ';
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The words THIS process puts on screen itself: the two native dialog titles and the model filter's name.
|
|
42
|
+
*
|
|
43
|
+
* Nothing else here is a string a user reads — every other sentence in this file is a log line or a self-test
|
|
44
|
+
* report — and these three are the exception because Electron, not a page, draws them. The window title is
|
|
45
|
+
* deliberately NOT in this table: it names the launcher's own window (`vrmpet3d - model settings`) and is the same
|
|
46
|
+
* ASCII wherever the user is, so a translated one would add a second name for a window the user finds by that one.
|
|
47
|
+
*
|
|
48
|
+
* The launcher is a SEPARATE PROCESS with no access to `lib/core.js`, so it keeps its own copy of the repository's
|
|
49
|
+
* two-language shape (`{ zh: {...}, en: {...} }`, identical key sets) rather than importing the host's rules.
|
|
50
|
+
*/
|
|
51
|
+
const DIALOG_TEXT = {
|
|
52
|
+
zh: { motionTitle: '导入动作目录', modelTitle: '导入模型文件', modelFilter: 'VRM / glTF 模型' },
|
|
53
|
+
en: { motionTitle: 'Import motion folder', modelTitle: 'Import model file', modelFilter: 'VRM / glTF model' },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Which of the two this process draws in, for one request.
|
|
58
|
+
*
|
|
59
|
+
* The PAGE wins whenever it says something this process understands. The settings page knows the user's
|
|
60
|
+
* `preferences.language`, which may be a deliberate 中文 on an English machine or the reverse, so it is the better
|
|
61
|
+
* authority when it speaks; it may pass that as `{ kind, language }`, and `'auto'` (or nothing at all) is not a
|
|
62
|
+
* language, so the fallback is `app.getLocale()` — the machine's own locale, which is exactly what `'auto'` means
|
|
63
|
+
* for a surface with no DSH locale to consult.
|
|
64
|
+
*
|
|
65
|
+
* The field is OPTIONAL and nothing here requires it: an older page that sends only `{ kind }` (without a language argument) gets the locale, and a page that sends a value neither of the two languages
|
|
66
|
+
* ignores it rather than having it guessed at.
|
|
67
|
+
*/
|
|
68
|
+
function dialogLanguage(options) {
|
|
69
|
+
const wanted = options && typeof options === 'object' ? options.language : null;
|
|
70
|
+
if (wanted === 'zh' || wanted === 'en') return wanted;
|
|
71
|
+
let locale = '';
|
|
72
|
+
try {
|
|
73
|
+
// After `ready`, which every handler here is; a locale that cannot be read is not a reason to fail a dialog.
|
|
74
|
+
locale = app.getLocale() || '';
|
|
75
|
+
} catch (_error) {
|
|
76
|
+
locale = '';
|
|
77
|
+
}
|
|
78
|
+
return /^(zh|cn|chinese)/i.test(String(locale).trim()) ? 'zh' : 'en';
|
|
79
|
+
}
|
|
80
|
+
|
|
40
81
|
/**
|
|
41
82
|
* The payload the page expects for a cursor sample.
|
|
42
83
|
*
|
|
@@ -563,6 +604,7 @@ app.whenReady().then(async () => {
|
|
|
563
604
|
if (!probing) sendCursor(lastCursor);
|
|
564
605
|
};
|
|
565
606
|
|
|
607
|
+
ipcMain.on('vrmpet3d:focus-host', () => { shell.openExternal('dsh://open').catch(() => {}); });
|
|
566
608
|
ipcMain.handle('vrmpet3d:config', () => ({
|
|
567
609
|
modelId: options.modelId,
|
|
568
610
|
assetBase: '/vrmpet3d/asset',
|
|
@@ -590,15 +632,16 @@ app.whenReady().then(async () => {
|
|
|
590
632
|
*/
|
|
591
633
|
ipcMain.handle('vrmpet3d:pick-path', async (_event, options) => {
|
|
592
634
|
const kind = options && typeof options === 'object' && options.kind === 'motion' ? 'motion' : 'model';
|
|
635
|
+
const text = DIALOG_TEXT[dialogLanguage(options)] || DIALOG_TEXT.en;
|
|
593
636
|
try {
|
|
594
637
|
const result = await dialog.showOpenDialog(win, kind === 'motion' ? {
|
|
595
|
-
title:
|
|
638
|
+
title: text.motionTitle,
|
|
596
639
|
properties: ['openDirectory'],
|
|
597
640
|
defaultPath: app.getPath('home'),
|
|
598
641
|
} : {
|
|
599
|
-
title:
|
|
642
|
+
title: text.modelTitle,
|
|
600
643
|
properties: ['openFile'],
|
|
601
|
-
filters: [{ name:
|
|
644
|
+
filters: [{ name: text.modelFilter, extensions: ['vrm', 'glb', 'gltf'] }],
|
|
602
645
|
defaultPath: app.getPath('home'),
|
|
603
646
|
});
|
|
604
647
|
if (result.canceled || !Array.isArray(result.filePaths) || !result.filePaths.length) return null;
|
package/desktop/preload.js
CHANGED
|
@@ -13,6 +13,7 @@ contextBridge.exposeInMainWorld('vrmpet3dDesktop', {
|
|
|
13
13
|
* Sent as an invoke so the page can await it before mounting anything.
|
|
14
14
|
*/
|
|
15
15
|
config: () => ipcRenderer.invoke('vrmpet3d:config'),
|
|
16
|
+
focusHost: () => ipcRenderer.send('vrmpet3d:focus-host'),
|
|
16
17
|
|
|
17
18
|
/** The global cursor position, pushed by the main process at frame rate. */
|
|
18
19
|
onCursor: (callback) => {
|
|
@@ -39,7 +40,9 @@ contextBridge.exposeInMainWorld('vrmpet3dDesktop', {
|
|
|
39
40
|
* plugin's own storage. The host stays the only writer of that list, and the `VRMPET3D_MODEL_FILES_FILE` /
|
|
40
41
|
* `VRMPET3D_IMPORT_DIR` overrides the suites rely on are not bypassed.
|
|
41
42
|
*/
|
|
42
|
-
pickModelFile: () => ipcRenderer.invoke('vrmpet3d:pick-path', {
|
|
43
|
+
pickModelFile: (language) => ipcRenderer.invoke('vrmpet3d:pick-path', {
|
|
44
|
+
kind: 'model', language: language === 'zh' || language === 'en' ? language : undefined,
|
|
45
|
+
}),
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* The same dialog for an imported MOTION folder (`/vrmpet3d/motions`), which stays a DIRECTORY: a motion pack is
|
|
@@ -47,7 +50,9 @@ contextBridge.exposeInMainWorld('vrmpet3dDesktop', {
|
|
|
47
50
|
*
|
|
48
51
|
* One channel for both, with `kind` deciding a FILE dialog from a folder one and the title each shows.
|
|
49
52
|
*/
|
|
50
|
-
pickMotionDir: () => ipcRenderer.invoke('vrmpet3d:pick-path', {
|
|
53
|
+
pickMotionDir: (language) => ipcRenderer.invoke('vrmpet3d:pick-path', {
|
|
54
|
+
kind: 'motion', language: language === 'zh' || language === 'en' ? language : undefined,
|
|
55
|
+
}),
|
|
51
56
|
|
|
52
57
|
/**
|
|
53
58
|
* Tell the launcher that a VIEW ORBIT is starting or ending — and let IT take the pointer lock.
|