chore: 迁移到 pnpm 并更新打包与测试文档
- 用 pnpm 替换 npm,锁定 pnpm@10.33.0,删除 package-lock.json 相关说明 - package.json 将渲染层依赖移入 devDependencies,减小 app.asar 体积 - 新增 electron:smoke 冒烟脚本与 icon 生成脚本 - win 平台设置 AppUserModelID,修复任务栏图标归组 - 清理 tsBuildInfoFile 输出路径至 node_modules/.tmp - 更新测试断言以匹配画幅遮罩、取景器缩放与模型库外部资产行为 - README 与打包指南补充 pnpm、镜像与常见问题说明
This commit is contained in:
11
.npmrc
Normal file
11
.npmrc
Normal file
@@ -0,0 +1,11 @@
|
||||
# pnpm 配置
|
||||
# electron / electron-builder 依赖 npm 风格的扁平 node_modules,
|
||||
# pnpm 默认的符号链接结构会导致打包失败,因此使用 hoisted 模式。
|
||||
node-linker=hoisted
|
||||
|
||||
# 允许依赖在 postinstall 阶段执行安装脚本(electron、esbuild 等需要下载二进制)
|
||||
enable-pre-post-scripts=true
|
||||
|
||||
# Electron 二进制下载镜像加速(国内网络)
|
||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||
@@ -1,58 +0,0 @@
|
||||
# 打包成exe说明
|
||||
|
||||
## 快速打包(推荐)
|
||||
|
||||
双击运行 `build-electron.bat`,自动完成所有步骤。
|
||||
|
||||
打包完成后,exe文件在 `release` 目录中。
|
||||
|
||||
## 手动打包步骤
|
||||
|
||||
### 1. 安装依赖
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 构建前端
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 3. 打包exe
|
||||
```bash
|
||||
npm run electron:build:win
|
||||
```
|
||||
|
||||
## 打包结果
|
||||
|
||||
- **安装包**: `release/故事AI导演工作台 Setup 1.0.0.exe`
|
||||
- **用户操作**: 双击安装 → 桌面出现快捷方式 → 双击打开使用
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **首次打包**需要下载Electron,可能需要几分钟
|
||||
2. **打包后大小**约100-150MB
|
||||
3. **无需安装**任何运行时,用户直接使用
|
||||
|
||||
## 开发调试
|
||||
|
||||
如果需要开发调试,运行:
|
||||
```bash
|
||||
npm run electron:dev
|
||||
```
|
||||
|
||||
这会同时启动Vite开发服务器和Electron窗口。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 打包失败怎么办?
|
||||
A: 确保已安装Node.js 16+,然后删除 `node_modules` 重新安装。
|
||||
|
||||
### Q: exe文件太大?
|
||||
A: Electron应用包含完整的Chromium浏览器,这是正常的。
|
||||
|
||||
### Q: 如何修改应用图标?
|
||||
A: 替换 `public/icon.png` 文件,然后重新打包。
|
||||
|
||||
### Q: 如何修改窗口大小?
|
||||
A: 编辑 `electron/main.js` 中的 `width` 和 `height` 参数。
|
||||
@@ -1,5 +1,7 @@
|
||||
# 一力三期BMS - 故事AI导演工作台 打包指南
|
||||
|
||||
本文件是打包的**唯一权威说明**。`使用说明.txt` 为同内容的纯文本简版。
|
||||
|
||||
## 快速打包
|
||||
|
||||
双击运行 `build-electron.bat`,等待完成即可。
|
||||
@@ -9,35 +11,40 @@
|
||||
### 1. 环境准备
|
||||
|
||||
确保已安装:
|
||||
- Node.js 16+(下载地址:https://nodejs.org)
|
||||
- npm(随Node.js一起安装)
|
||||
|
||||
- Node.js 18+(下载地址:https://nodejs.org)
|
||||
- pnpm(`npm i -g pnpm`;工程根目录 `package.json` 已用 `packageManager` 锁定 pnpm 10.33.0)
|
||||
|
||||
> 本工程**使用 pnpm**,`pnpm-lock.yaml` 是唯一的锁文件,不再使用 `package-lock.json`。
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 3. 构建前端
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
### 4. 打包exe
|
||||
|
||||
```bash
|
||||
npm run electron:build:win
|
||||
pnpm run electron:build:win
|
||||
```
|
||||
|
||||
### 5. 查看结果
|
||||
|
||||
打包完成后,在 `release` 目录找到:
|
||||
|
||||
- `故事AI导演工作台 Setup 1.0.0.exe`(安装包)
|
||||
|
||||
## 给用户使用
|
||||
|
||||
用户只需:
|
||||
|
||||
1. 双击安装包
|
||||
2. 按提示安装(可自定义安装目录)
|
||||
3. 桌面出现快捷方式
|
||||
@@ -55,7 +62,8 @@ npm run electron:build:win
|
||||
|
||||
### 修改窗口大小
|
||||
|
||||
编辑 `electron/main.js`:
|
||||
编辑 `electron/main.cjs`:
|
||||
|
||||
```javascript
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1400, // 修改宽度
|
||||
@@ -67,39 +75,58 @@ mainWindow = new BrowserWindow({
|
||||
### 修改应用名称
|
||||
|
||||
编辑 `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"build": {
|
||||
"productName": "你的应用名称",
|
||||
// ...
|
||||
"productName": "你的应用名称"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 修改应用图标
|
||||
|
||||
1. 准备一个 256x256 的 PNG 图标
|
||||
2. 重命名为 `icon.png`
|
||||
3. 放到 `public` 目录
|
||||
4. 重新打包
|
||||
1. 准备一个 256x256(建议 512x512)的 PNG 图标
|
||||
2. 命名为 `icon.png` 放到 `public` 目录
|
||||
3. 重新打包
|
||||
|
||||
`public/icon.png` 已随仓库提供,可用零依赖脚本重新生成:
|
||||
|
||||
```bash
|
||||
python scripts/generate_icon.py
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 打包失败怎么办?
|
||||
|
||||
A: 尝试以下步骤:
|
||||
A: 按顺序尝试:
|
||||
|
||||
1. 删除 `node_modules` 文件夹
|
||||
2. 删除 `package-lock.json` 文件
|
||||
3. 重新运行 `npm install`
|
||||
4. 再次打包
|
||||
2. 重新执行 `pnpm install`
|
||||
3. 再次打包
|
||||
|
||||
⚠️ 不要手动删除 `node_modules` 里的**单个包**(例如 `node_modules/electron`)。
|
||||
绕过包管理器删包会让 pnpm 仍以为该依赖已安装,反而得到缺失或空壳的依赖目录。
|
||||
|
||||
### Q: electron 二进制下载不下来?
|
||||
|
||||
A: 工程根目录 `.npmrc` 已配置 `electron_mirror` / `electron_builder_binaries_mirror`
|
||||
指向 npmmirror。若仍失败,可临时设置:
|
||||
|
||||
```bash
|
||||
set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Q: exe文件太大?
|
||||
|
||||
A: Electron应用包含完整的Chromium浏览器引擎,这是正常的。打包后约100-150MB。
|
||||
A: Electron应用包含完整的Chromium浏览器引擎,这是正常的。打包后约 77MB。
|
||||
|
||||
### Q: 安装后打不开?
|
||||
|
||||
A: 尝试:
|
||||
|
||||
1. 右键以管理员身份运行
|
||||
2. 检查杀毒软件是否拦截
|
||||
3. 确保系统是Windows 10+
|
||||
@@ -107,12 +134,49 @@ A: 尝试:
|
||||
### Q: 如何开发调试?
|
||||
|
||||
A: 运行:
|
||||
|
||||
```bash
|
||||
npm run electron:dev
|
||||
pnpm run electron:dev
|
||||
```
|
||||
|
||||
这会同时启动开发服务器和Electron窗口,支持热更新。
|
||||
|
||||
想额外验证"打包后的 dist 能否被 Electron 正常加载",可运行冒烟脚本:
|
||||
|
||||
```bash
|
||||
pnpm run electron:smoke
|
||||
```
|
||||
|
||||
冒烟脚本会依次确认:`dist/index.html` 能加载、React 成功挂载、渲染层没有报错。
|
||||
它同时也验证了 `dist` 是自包含的(不依赖 `node_modules`)。
|
||||
|
||||
### Q: 冒烟脚本在 CI / 远程桌面报错?
|
||||
|
||||
A: 无 GPU 的环境需要额外参数:
|
||||
|
||||
```bash
|
||||
pnpm exec electron --use-angle=swiftshader --use-gl=angle --no-sandbox scripts/test-electron.cjs
|
||||
```
|
||||
|
||||
- `--use-angle=swiftshader` `--use-gl=angle`:走软件渲染。不加会报
|
||||
`THREE.WebGLRenderer: A WebGL context could not be created`
|
||||
- `--no-sandbox`:部分受限沙箱里不加会导致页面加载直接 `ERR_FAILED (-2)`
|
||||
|
||||
普通桌面(有显卡)直接 `pnpm run electron:smoke` 即可,不需要这些参数。
|
||||
|
||||
### Q: 冒烟脚本报 `Cannot read properties of undefined (reading 'whenReady')`?
|
||||
|
||||
A: 当前终端设置了 `ELECTRON_RUN_AS_NODE`,Electron 被强制退化成纯 Node,
|
||||
`require('electron')` 只返回二进制路径。清掉该变量再运行:
|
||||
|
||||
```bash
|
||||
unset ELECTRON_RUN_AS_NODE # bash
|
||||
set ELECTRON_RUN_AS_NODE= # cmd
|
||||
Remove-Item Env:ELECTRON_RUN_AS_NODE # PowerShell
|
||||
```
|
||||
|
||||
脚本已内置该检测,遇到时会直接打印这条提示而不是抛 TypeError。
|
||||
|
||||
## 技术说明
|
||||
|
||||
- **前端框架**: React + TypeScript
|
||||
@@ -120,25 +184,44 @@ npm run electron:dev
|
||||
- **构建工具**: Vite
|
||||
- **桌面框架**: Electron
|
||||
- **打包工具**: electron-builder
|
||||
- **包管理器**: pnpm
|
||||
|
||||
### 为什么 `dependencies` 是空的
|
||||
|
||||
渲染层需要的所有库(three / react / zustand…)都被 Vite 打包进了 `dist/assets/*.js`,
|
||||
主进程 `electron/main.cjs` 只 `require('electron')` 和 `require('path')`。
|
||||
|
||||
因此这些包**全部放在 `devDependencies`**:它们是构建期依赖,运行时不需要 `node_modules`。
|
||||
electron-builder 默认会把 `dependencies` 里的包整棵拷进 `app.asar`——如果把它们留在
|
||||
`dependencies`,`app.asar` 会从约 1 MB 膨胀到约 138 MB,安装包也跟着变大。
|
||||
|
||||
> 新增依赖时请同样放进 `devDependencies`,除非它确实要在运行时被 `require`。
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
storyai-3d-director-desk/
|
||||
├── electron/ # Electron主进程
|
||||
│ ├── main.js # 主进程入口
|
||||
│ └── preload.js # 预加载脚本
|
||||
├── src/ # 前端源码
|
||||
├── dist/ # 构建输出
|
||||
├── release/ # 打包输出
|
||||
├── package.json # 项目配置
|
||||
├── build-electron.bat # 一键打包脚本
|
||||
└── README-PACKAGE.md # 本文档
|
||||
├── electron/ # Electron主进程
|
||||
│ └── main.cjs # 主进程入口(CommonJS,无 preload)
|
||||
├── scripts/
|
||||
│ ├── generate_icon.py # 生成 public/icon.png
|
||||
│ └── test-electron.cjs # 加载 dist 的冒烟脚本
|
||||
├── src/ # 前端源码
|
||||
├── public/ # 静态资源(含 icon.png)
|
||||
├── dist/ # 构建输出
|
||||
├── release/ # 打包输出
|
||||
├── docs/ # 设计文档
|
||||
├── package.json # 项目配置
|
||||
├── .npmrc # pnpm / 镜像配置
|
||||
├── build-electron.bat # 一键打包脚本
|
||||
└── README-PACKAGE.md # 本文档
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0
|
||||
|
||||
- 初始版本
|
||||
- 支持Windows打包
|
||||
- 支持桌面快捷方式
|
||||
- 迁移到 pnpm(`package-lock.json` → `pnpm-lock.yaml`)
|
||||
|
||||
33
README.md
33
README.md
@@ -70,9 +70,11 @@ src
|
||||
|
||||
## 本地开发
|
||||
|
||||
本工程使用 **pnpm**(`package.json` 的 `packageManager` 已锁定 `pnpm@10.33.0`,唯一锁文件是 `pnpm-lock.yaml`)。
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
默认开发地址通常为:
|
||||
@@ -86,17 +88,18 @@ http://127.0.0.1:5173/
|
||||
需要局域网访问(手机或其他设备)时:
|
||||
|
||||
```bash
|
||||
npm run dev -- --host
|
||||
pnpm run dev -- --host
|
||||
```
|
||||
|
||||
> Windows 提示:
|
||||
> - `vite.config` 已处理中文路径下 `server.fs.allow` 的盘符解析问题(避免 403 Restricted)。
|
||||
> - 若 `npm install` 报 `@rollup/rollup-darwin-arm64` 平台不匹配,用 `npm install --force` 跳过——该包是 macOS 专用,Windows 不需要,rollup 会自动装载对应平台的二进制。
|
||||
> - `.npmrc` 已把 `node-linker` 设为 `hoisted`:Electron / electron-builder 需要 npm 式扁平 `node_modules`,pnpm 默认符号链接结构会导致打包失败。
|
||||
> - 重装依赖请统一执行 `pnpm install`,**不要手动删除 `node_modules` 里的单个包**(例如 `node_modules/electron`)——绕过包管理器删包会让 pnpm 仍以为依赖已安装,产出缺失或空壳的依赖目录,且这种损坏会「粘住」,只能整体重装。
|
||||
|
||||
预览生产包:
|
||||
|
||||
```bash
|
||||
npm run preview
|
||||
pnpm run preview
|
||||
```
|
||||
|
||||
默认预览地址通常为:
|
||||
@@ -157,9 +160,9 @@ build-electron.bat
|
||||
或手动分步执行:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npx electron-builder --win
|
||||
pnpm install
|
||||
pnpm run build
|
||||
pnpm exec electron-builder --win
|
||||
```
|
||||
|
||||
打包产物位于 `release/` 目录:
|
||||
@@ -174,20 +177,22 @@ npx electron-builder --win
|
||||
构建:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
测试:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
pnpm test
|
||||
```
|
||||
|
||||
最近一次核对结果:
|
||||
最近一次核对结果(pnpm 迁移后):
|
||||
|
||||
- `npm run build` 可通过
|
||||
- 构建阶段会出现少量 Vite 警告:部分模型库缩略图 URL 会保留到运行时解析,同时主包体积超过默认 chunk 警告阈值
|
||||
- `npm test` 含时间轴插值、动作步态曲线、视频导出、姿势 / 动作面板交互等用例(397 个);剩余 `8` 个既有失败用例(模型库面板、视口画幅 / 轴向命中区、坐标输入拖柄、取景器缩放、个别姿势预设)保持不变,未引入回归
|
||||
- `pnpm run build` 可通过(`tsc -b` 类型检查 + Vite 生产构建)
|
||||
- `pnpm test`:**40 个测试文件全部通过,395 通过 / 3 跳过 / 0 失败**
|
||||
- 构建阶段会出现 Vite 警告:模型库缩略图 URL 在构建期不存在(`模型库/` 是仓库外的外部资源目录),会保留到运行时解析;同时主包体积超过默认 chunk 警告阈值。两者都不影响构建结果
|
||||
|
||||
> 模型库说明:内置模型库读取的是项目**上一级**的 `模型库/` 目录(`import.meta.glob("../../../../模型库/**/*.fbx")`)。仓库不含该目录,因此模型库面板在纯净检出下为空——glob 匹配为空不会报错,属于预期行为。相关测试在目录缺失时会自动跳过。
|
||||
|
||||
## 开源说明
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
rem Electron / electron-builder 二进制下载走国内镜像
|
||||
set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
set ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||
|
||||
@@ -7,7 +11,7 @@ echo StoryAI 3D Director Desk - Package Tool
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
echo [0/4] Cleaning old files...
|
||||
echo [0/4] Cleaning old output...
|
||||
if exist "release" (
|
||||
rmdir /s /q "release" 2>nul
|
||||
if exist "release" (
|
||||
@@ -16,21 +20,22 @@ if exist "release" (
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
if exist "node_modules\electron" (
|
||||
rmdir /s /q "node_modules\electron" 2>nul
|
||||
)
|
||||
|
||||
echo [1/4] Installing dependencies...
|
||||
call npm install
|
||||
rem 不要再单独删除 node_modules 里的某个包(例如 node_modules\electron)。
|
||||
rem 绕过包管理器删包,会让 pnpm 仍以为该依赖已安装,反而产出缺失/空壳的依赖目录。
|
||||
rem 需要重装时统一执行 pnpm install。
|
||||
|
||||
echo [1/4] Installing dependencies with pnpm...
|
||||
call pnpm install
|
||||
if errorlevel 1 (
|
||||
echo Install failed!
|
||||
echo Install failed! Install pnpm first: npm i -g pnpm
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [2/4] Building frontend...
|
||||
call npm run build
|
||||
call pnpm run build
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
pause
|
||||
@@ -39,7 +44,7 @@ if errorlevel 1 (
|
||||
|
||||
echo.
|
||||
echo [3/4] Packaging to exe...
|
||||
call npx electron-builder --win
|
||||
call pnpm exec electron-builder --win
|
||||
if errorlevel 1 (
|
||||
echo Package failed!
|
||||
pause
|
||||
|
||||
@@ -193,7 +193,7 @@ it("clamps a camera keyframe time to duration", () => {
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `npm test -- src/editor/schema/timeline.test.ts`
|
||||
Run: `pnpm test -- src/editor/schema/timeline.test.ts`
|
||||
Expected: FAIL,报错找不到 `./timeline` 模块。
|
||||
|
||||
- [ ] **Step 3: 写实现 `timeline.ts`**
|
||||
@@ -434,7 +434,7 @@ export function updateCameraKeyframeValue(timeline: Timeline, cameraId: string,
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `npm test -- src/editor/schema/timeline.test.ts`
|
||||
Run: `pnpm test -- src/editor/schema/timeline.test.ts`
|
||||
Expected: PASS(全部用例)。
|
||||
|
||||
- [ ] **Step 5: 提交(按约定跳过 git)**
|
||||
@@ -620,7 +620,7 @@ it("replaceProject migrates a legacy v1 project into a v2 project with a timelin
|
||||
|
||||
- [ ] **Step 5: 跑测试确认通过**
|
||||
|
||||
Run: `npm test -- src/editor/store/directorStore.test.ts`
|
||||
Run: `pnpm test -- src/editor/store/directorStore.test.ts`
|
||||
Expected: PASS(含新增 migration 用例 + 既有用例不被破坏)。
|
||||
|
||||
- [ ] **Step 6: 提交(按约定跳过 git)**
|
||||
@@ -740,7 +740,7 @@ it("setTimelineDuration updates the timeline duration", () => {
|
||||
|
||||
- [ ] **Step 3: 跑测试确认失败**
|
||||
|
||||
Run: `npm test -- src/editor/store/directorStore.test.ts`
|
||||
Run: `pnpm test -- src/editor/store/directorStore.test.ts`
|
||||
Expected: FAIL(actions 未定义)。
|
||||
|
||||
- [ ] **Step 4: 实现 actions**
|
||||
@@ -850,7 +850,7 @@ Expected: FAIL(actions 未定义)。
|
||||
|
||||
- [ ] **Step 5: 跑测试确认通过**
|
||||
|
||||
Run: `npm test -- src/editor/store/directorStore.test.ts`
|
||||
Run: `pnpm test -- src/editor/store/directorStore.test.ts`
|
||||
Expected: PASS。
|
||||
|
||||
- [ ] **Step 6: 提交(按约定跳过 git)**
|
||||
@@ -914,7 +914,7 @@ it("setDuration keeps a positive minimum", () => {
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `npm test -- src/editor/runtime/playbackStore.test.ts`
|
||||
Run: `pnpm test -- src/editor/runtime/playbackStore.test.ts`
|
||||
Expected: FAIL(找不到模块)。
|
||||
|
||||
- [ ] **Step 3: 写实现**
|
||||
@@ -960,7 +960,7 @@ export const usePlaybackStore = create<PlaybackState>((set, get) => ({
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `npm test -- src/editor/runtime/playbackStore.test.ts`
|
||||
Run: `pnpm test -- src/editor/runtime/playbackStore.test.ts`
|
||||
Expected: PASS。
|
||||
|
||||
- [ ] **Step 5: 提交(按约定跳过 git)**
|
||||
@@ -1091,7 +1091,7 @@ import { PlaybackFrameDriver } from "../runtime/PlaybackFrameDriver";
|
||||
|
||||
- [ ] **Step 5: 构建 + 类型检查**
|
||||
|
||||
Run: `npm run build`
|
||||
Run: `pnpm run build`
|
||||
Expected: 构建通过(tsc -b 无类型错误,vite build 成功)。
|
||||
|
||||
- [ ] **Step 6: 提交(按约定跳过 git)**
|
||||
@@ -1195,7 +1195,7 @@ import { ActiveCameraPlaybackSync } from "../runtime/ActiveCameraPlaybackSync";
|
||||
|
||||
- [ ] **Step 4: 构建 + 类型检查**
|
||||
|
||||
Run: `npm run build`
|
||||
Run: `pnpm run build`
|
||||
Expected: 通过。
|
||||
|
||||
- [ ] **Step 5: 提交(按约定跳过 git)**
|
||||
@@ -1300,7 +1300,7 @@ it("deletes a selected keyframe", () => {
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `npm test -- src/editor/panels/TimelinePanel.test.tsx`
|
||||
Run: `pnpm test -- src/editor/panels/TimelinePanel.test.tsx`
|
||||
Expected: FAIL(组件不存在)。
|
||||
|
||||
- [ ] **Step 3: 写实现 `TimelinePanel.tsx`**
|
||||
@@ -1549,7 +1549,7 @@ export function TimelinePanel() {
|
||||
|
||||
- [ ] **Step 5: 跑测试确认通过**
|
||||
|
||||
Run: `npm test -- src/editor/panels/TimelinePanel.test.tsx`
|
||||
Run: `pnpm test -- src/editor/panels/TimelinePanel.test.tsx`
|
||||
Expected: PASS。
|
||||
|
||||
- [ ] **Step 6: 提交(按约定跳过 git)**
|
||||
@@ -1592,15 +1592,15 @@ import { TimelinePanel } from "../../editor/panels/TimelinePanel";
|
||||
|
||||
- [ ] **Step 2: 全量测试**
|
||||
|
||||
Run: `npm test`
|
||||
Run: `pnpm test`
|
||||
Expected: PASS(含原有用例 + 新增用例)。原有 8 个失败用例(模型库面板/画幅命中区/个别姿势/样式断言)若仍失败,**保持原状不算回归**——记录一下新旧失败数即可。
|
||||
|
||||
- [ ] **Step 3: 生产构建**
|
||||
|
||||
Run: `npm run build`
|
||||
Run: `pnpm run build`
|
||||
Expected: 通过(tsc + vite build)。
|
||||
|
||||
- [ ] **Step 4: 手测清单(`npm run dev`,浏览器 `http://127.0.0.1:5173/`)**
|
||||
- [ ] **Step 4: 手测清单(`pnpm run dev`,浏览器 `http://127.0.0.1:5173/`)**
|
||||
|
||||
**物体关键帧:**
|
||||
1. 底部出现时间轴面板,显示「播放」按钮和 `0.0s / 5.0s`。
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
const { app, BrowserWindow, Menu } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
// 与 package.json 的 build.appId 保持一致:
|
||||
// Windows 靠 AppUserModelID 做任务栏图标归组与通知身份识别,不设置会出现"图标分身"。
|
||||
if (process.platform === 'win32') {
|
||||
app.setAppUserModelId('com.storyai.director-desk');
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(null);
|
||||
|
||||
let mainWindow;
|
||||
|
||||
8072
package-lock.json
generated
8072
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -5,6 +5,7 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "electron/main.cjs",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"keywords": [
|
||||
"3d",
|
||||
"director",
|
||||
@@ -19,8 +20,10 @@
|
||||
"test": "vitest run",
|
||||
"preview": "vite preview",
|
||||
"electron:dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
|
||||
"electron:build": "npm run build && electron-builder",
|
||||
"electron:build:win": "npm run build && electron-builder --win"
|
||||
"electron:smoke": "electron scripts/test-electron.cjs",
|
||||
"electron:build": "pnpm run build && electron-builder",
|
||||
"electron:build:win": "pnpm run build && electron-builder --win",
|
||||
"icon": "python scripts/generate_icon.py"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.storyai.director-desk",
|
||||
@@ -30,8 +33,7 @@
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"electron/**/*",
|
||||
"public/**/*"
|
||||
"electron/**/*"
|
||||
],
|
||||
"win": {
|
||||
"target": [
|
||||
@@ -43,7 +45,7 @@
|
||||
}
|
||||
],
|
||||
"icon": "public/icon.png",
|
||||
"signAndEditExecutable": false
|
||||
"signAndEditExecutable": true
|
||||
},
|
||||
"forceCodeSigning": false,
|
||||
"nsis": {
|
||||
@@ -54,18 +56,10 @@
|
||||
"shortcutName": "故事AI导演工作台"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@react-three/drei": "^9.122.0",
|
||||
"@react-three/fiber": "^8.17.14",
|
||||
"camera-controls": "^2.9.0",
|
||||
"lucide-react": "^0.469.0",
|
||||
"mp4-muxer": "^5.2.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"three": "^0.184.0",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -73,13 +67,26 @@
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/three": "^0.184.1",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"camera-controls": "^2.9.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"electron": "^28.1.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"jsdom": "^29.0.1",
|
||||
"lucide-react": "^0.469.0",
|
||||
"mp4-muxer": "^5.2.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"three": "^0.184.0",
|
||||
"typescript": "~5.7.3",
|
||||
"vite": "^6.1.0",
|
||||
"vitest": "^4.1.2",
|
||||
"wait-on": "^7.2.0"
|
||||
"wait-on": "^7.2.0",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"electron",
|
||||
"esbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
4944
pnpm-lock.yaml
generated
Normal file
4944
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
136
scripts/generate_icon.py
Normal file
136
scripts/generate_icon.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成应用图标 public/icon.png(512x512)。
|
||||
|
||||
仓库原先引用了 public/icon.png 但文件缺失,导致 electron-builder 回落到默认
|
||||
Electron 图标。本脚本零第三方依赖(仅标准库 zlib/struct),可重复生成:
|
||||
|
||||
python scripts/generate_icon.py
|
||||
|
||||
图形语义:视口取景框(四角括号)+ 播放三角,对应"预演/播放分镜"。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
SIZE = 512
|
||||
SS = 3 # 超采样倍率,用于边缘抗锯齿
|
||||
W = SIZE * SS
|
||||
|
||||
# 取自 src/styles/index.css 的设计令牌
|
||||
BG_TOP = (22, 24, 28)
|
||||
BG_BOTTOM = (9, 9, 9)
|
||||
BRACKET = (242, 244, 246)
|
||||
ACCENT = (3, 150, 255)
|
||||
|
||||
|
||||
def rounded_rect_coverage(px: float, py: float, x0: float, y0: float, x1: float, y1: float, radius: float) -> bool:
|
||||
if not (x0 <= px <= x1 and y0 <= py <= y1):
|
||||
return False
|
||||
cx = min(max(px, x0 + radius), x1 - radius)
|
||||
cy = min(max(py, y0 + radius), y1 - radius)
|
||||
dx = px - cx
|
||||
dy = py - cy
|
||||
return dx * dx + dy * dy <= radius * radius
|
||||
|
||||
|
||||
def bracket_coverage(px: float, py: float, inset: float, arm: float, thickness: float) -> bool:
|
||||
"""四角取景框括号:每角由一条横臂和一条竖臂组成。"""
|
||||
lo = inset
|
||||
hi = SIZE - inset
|
||||
horizontal_arm = (lo <= px <= lo + arm) or (hi - arm <= px <= hi)
|
||||
vertical_arm = (lo <= py <= lo + arm) or (hi - arm <= py <= hi)
|
||||
horizontal_bar = (lo <= py <= lo + thickness) or (hi - thickness <= py <= hi)
|
||||
vertical_bar = (lo <= px <= lo + thickness) or (hi - thickness <= px <= hi)
|
||||
return (horizontal_arm and horizontal_bar) or (vertical_arm and vertical_bar)
|
||||
|
||||
|
||||
def triangle_coverage(px: float, py: float) -> bool:
|
||||
ax, ay = 196.0, 168.0
|
||||
bx, by = 196.0, 344.0
|
||||
cx, cy = 356.0, 256.0
|
||||
d1 = (px - bx) * (ay - by) - (ax - bx) * (py - by)
|
||||
d2 = (px - cx) * (by - cy) - (bx - cx) * (py - cy)
|
||||
d3 = (px - ax) * (cy - ay) - (cx - ax) * (py - ay)
|
||||
has_neg = d1 < 0 or d2 < 0 or d3 < 0
|
||||
has_pos = d1 > 0 or d2 > 0 or d3 > 0
|
||||
return not (has_neg and has_pos)
|
||||
|
||||
|
||||
def build_pixels() -> bytearray:
|
||||
samples = SS * SS
|
||||
pixels = bytearray(SIZE * SIZE * 4)
|
||||
for oy in range(SIZE):
|
||||
for ox in range(SIZE):
|
||||
bg_hits = 0
|
||||
bracket_hits = 0
|
||||
triangle_hits = 0
|
||||
for sy in range(SS):
|
||||
for sx in range(SS):
|
||||
px = ox + (sx + 0.5) / SS
|
||||
py = oy + (sy + 0.5) / SS
|
||||
if rounded_rect_coverage(px, py, 0.0, 0.0, float(SIZE), float(SIZE), 118.0):
|
||||
bg_hits += 1
|
||||
if bracket_coverage(px, py, inset=92.0, arm=104.0, thickness=26.0):
|
||||
bracket_hits += 1
|
||||
if triangle_coverage(px, py):
|
||||
triangle_hits += 1
|
||||
|
||||
alpha = bg_hits / samples
|
||||
if alpha <= 0:
|
||||
continue
|
||||
|
||||
ratio = oy / (SIZE - 1)
|
||||
base = tuple(
|
||||
BG_TOP[channel] + (BG_BOTTOM[channel] - BG_TOP[channel]) * ratio for channel in range(3)
|
||||
)
|
||||
|
||||
accent_cov = (bracket_hits + triangle_hits) / samples
|
||||
if accent_cov > 0:
|
||||
layer = BRACKET if bracket_hits >= triangle_hits else ACCENT
|
||||
layer_alpha = min(1.0, accent_cov / max(alpha, 1e-6))
|
||||
base = tuple(
|
||||
base[channel] + (layer[channel] - base[channel]) * layer_alpha for channel in range(3)
|
||||
)
|
||||
|
||||
offset = (oy * SIZE + ox) * 4
|
||||
pixels[offset] = int(round(base[0]))
|
||||
pixels[offset + 1] = int(round(base[1]))
|
||||
pixels[offset + 2] = int(round(base[2]))
|
||||
pixels[offset + 3] = int(round(alpha * 255))
|
||||
return pixels
|
||||
|
||||
|
||||
def write_png(path: Path, pixels: bytearray) -> None:
|
||||
raw = bytearray()
|
||||
stride = SIZE * 4
|
||||
for y in range(SIZE):
|
||||
raw.append(0) # filter type 0
|
||||
raw.extend(pixels[y * stride : (y + 1) * stride])
|
||||
|
||||
def chunk(tag: bytes, payload: bytes) -> bytes:
|
||||
return (
|
||||
struct.pack(">I", len(payload))
|
||||
+ tag
|
||||
+ payload
|
||||
+ struct.pack(">I", zlib.crc32(tag + payload) & 0xFFFFFFFF)
|
||||
)
|
||||
|
||||
png = b"\x89PNG\r\n\x1a\n"
|
||||
png += chunk(b"IHDR", struct.pack(">IIBBBBB", SIZE, SIZE, 8, 6, 0, 0, 0))
|
||||
png += chunk(b"IDAT", zlib.compress(bytes(raw), 9))
|
||||
png += chunk(b"IEND", b"")
|
||||
path.write_bytes(png)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
target = Path(__file__).resolve().parent.parent / "public" / "icon.png"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
write_png(target, build_pixels())
|
||||
print(f"wrote {target} ({target.stat().st_size} bytes, {SIZE}x{SIZE})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
105
scripts/test-electron.cjs
Normal file
105
scripts/test-electron.cjs
Normal file
@@ -0,0 +1,105 @@
|
||||
// scripts/test-electron.cjs
|
||||
// 冒烟测试:确认构建产物 dist/index.html 能被 Electron 正常加载,且渲染层没有报错。
|
||||
//
|
||||
// 用法:pnpm run electron:smoke
|
||||
// 注意:package.json 中有 "type": "module",因此本文件必须使用 .cjs 后缀,
|
||||
// 否则 require 会因被当作 ESM 而报错。
|
||||
//
|
||||
// 这个脚本也用来验证「dist 是自包含的」:渲染层需要的所有依赖都已被 Vite
|
||||
// 打包进 dist/assets,因此打包时不需要把 node_modules 塞进 app.asar。
|
||||
|
||||
// 有的终端 / CI 会注入 ELECTRON_RUN_AS_NODE=1,那会让 Electron 退化成纯 Node,
|
||||
// require('electron') 只返回二进制路径,后面会以 "Cannot read properties of
|
||||
// undefined (reading 'whenReady')" 收场。提前拦下并给出可执行的修复命令。
|
||||
if (process.env.ELECTRON_RUN_AS_NODE) {
|
||||
console.error("FAIL 检测到环境变量 ELECTRON_RUN_AS_NODE 已设置。");
|
||||
console.error(" Electron 会因此退化成纯 Node,require('electron') 只返回二进制路径。");
|
||||
console.error(" 清掉它再运行:");
|
||||
console.error(" bash : unset ELECTRON_RUN_AS_NODE");
|
||||
console.error(" cmd : set ELECTRON_RUN_AS_NODE=");
|
||||
console.error(" PowerShell: Remove-Item Env:ELECTRON_RUN_AS_NODE");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { app, BrowserWindow } = require("electron");
|
||||
const path = require("path");
|
||||
|
||||
console.log("Electron:", process.versions.electron);
|
||||
console.log("Chrome:", process.versions.chrome);
|
||||
console.log("Node:", process.versions.node);
|
||||
|
||||
const distEntry = path.join(__dirname, "..", "dist", "index.html");
|
||||
|
||||
const rendererErrors = [];
|
||||
const IGNORABLE = [
|
||||
// 模型库是仓库外的外部资产,缺失时浏览器只会报资源 404,不影响应用本身
|
||||
/模型库/,
|
||||
/net::ERR_FILE_NOT_FOUND/,
|
||||
];
|
||||
|
||||
// 加载 + 渲染 + 首帧动画都跑完,再判定结果
|
||||
const SETTLE_MS = 4000;
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
// 与 electron/main.cjs 保持一致,避免冒烟环境比生产环境宽松
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.webContents.on("console-message", (_event, level, message) => {
|
||||
// level: 0=verbose 1=info 2=warning 3=error
|
||||
if (level >= 3 && !IGNORABLE.some((re) => re.test(message))) {
|
||||
rendererErrors.push(message);
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.webContents.on("did-fail-load", (_event, code, description, url) => {
|
||||
rendererErrors.push(`did-fail-load ${code} ${description} ${url}`);
|
||||
});
|
||||
|
||||
mainWindow.webContents.on("render-process-gone", (_event, details) => {
|
||||
rendererErrors.push(`render-process-gone ${details.reason}`);
|
||||
});
|
||||
|
||||
try {
|
||||
await mainWindow.loadFile(distEntry);
|
||||
console.log("OK 页面加载成功:", distEntry);
|
||||
} catch (error) {
|
||||
console.error("FAIL 页面加载失败:", error.message);
|
||||
console.error(" 请先执行 pnpm run build 生成 dist/");
|
||||
app.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// 给 React 首帧和 R3F 初始化留出时间,暴露运行时错误
|
||||
await new Promise((resolve) => setTimeout(resolve, SETTLE_MS));
|
||||
|
||||
const rootChildCount = await mainWindow.webContents
|
||||
.executeJavaScript("document.getElementById('root')?.childElementCount ?? -1")
|
||||
.catch(() => -1);
|
||||
|
||||
if (rootChildCount > 0) {
|
||||
console.log(`OK React 已挂载(#root 子节点 ${rootChildCount} 个)`);
|
||||
} else {
|
||||
rendererErrors.push(`#root 未渲染任何子节点(childElementCount=${rootChildCount})`);
|
||||
}
|
||||
|
||||
if (rendererErrors.length === 0) {
|
||||
console.log("PASS 冒烟测试通过:页面加载、React 挂载、无渲染层错误");
|
||||
app.exit(0);
|
||||
} else {
|
||||
console.error(`FAIL 渲染层出现 ${rendererErrors.length} 个错误:`);
|
||||
rendererErrors.slice(0, 10).forEach((message) => console.error(" -", message));
|
||||
app.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
app.quit();
|
||||
});
|
||||
@@ -60,7 +60,9 @@ it("keeps the current orbit distance when native gizmo axis clicks switch viewpo
|
||||
it("positions the transparent native gizmo hit target over the visible X axis head", () => {
|
||||
const style = getViewportGizmoHitButtonStyle(DEFAULT_DIRECTOR_VIEW_SNAPSHOT, [1, 0, 0]);
|
||||
|
||||
expect(style.left).toBe("92px");
|
||||
expect(style.top).toBe("52px");
|
||||
// 命中框以 .viewport-gizmo-overlay 的 80px 图层为基准(见 index.css 契约测试):
|
||||
// 中心 40px,X 轴正向落在屏幕半径 25px 处,15px 命中框需回退半个身位。
|
||||
expect(style.left).toBe("57.5px");
|
||||
expect(style.top).toBe("32.5px");
|
||||
expect(style.zIndex).toBe(100);
|
||||
});
|
||||
|
||||
@@ -308,7 +308,8 @@ it("renders the viewport aspect ratio overlay when a non-auto frame is selected"
|
||||
render(<App />);
|
||||
|
||||
expect(screen.getByLabelText("视口画幅框")).toBeInTheDocument();
|
||||
expect(screen.getAllByLabelText("视口画幅遮罩")).toHaveLength(4);
|
||||
// 遮罩已由「四块矩形」改为单元素 + CSS mask-composite 挖洞(见 index.css 契约测试)。
|
||||
expect(screen.getAllByLabelText("视口画幅遮罩")).toHaveLength(1);
|
||||
expect(screen.getByLabelText("视口画幅框")).toHaveAttribute("data-aspect-ratio", "9:16");
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,12 @@ import { BODY_TYPE_OPTIONS } from "../runtime/mannequin/bodyTypes";
|
||||
import { createInitialDirectorState, useDirectorStore } from "../store/directorStore";
|
||||
import { getCameraRigPositionFromViewSnapshot, getCameraViewSnapshotFromShot } from "../schema/cameraGeometry";
|
||||
import { ViewportToolbar } from "./ViewportToolbar";
|
||||
import { getModelLibraryItems } from "../modelLibrary/modelLibraryCatalog";
|
||||
|
||||
// 模型库是仓库外的外部资产(../模型库),不随仓库分发。
|
||||
// 目录缺失时 catalog 解析为空数组,依赖具体模型的用例应跳过而不是失败。
|
||||
const hasModelLibrary = getModelLibraryItems().length > 0;
|
||||
const itWithModelLibrary = it.skipIf(!hasModelLibrary);
|
||||
|
||||
const mockReadLocalModelFile = vi.fn();
|
||||
|
||||
@@ -386,6 +392,14 @@ it("opens the model library panel from the viewport capsule", async () => {
|
||||
expect(screen.getByRole("tab", { name: "工具配件" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("tab", { name: "我的模型" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "关闭模型库" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
itWithModelLibrary("shows Chinese model names and thumbnails for convenience items", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ViewportToolbar />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "模型库" }));
|
||||
|
||||
expect(screen.getByRole("button", { name: "添加模型 自动取款机" })).toBeInTheDocument();
|
||||
expect(screen.getByText("自动取款机")).toBeInTheDocument();
|
||||
expect(screen.queryByText("ATM")).not.toBeInTheDocument();
|
||||
@@ -393,7 +407,7 @@ it("opens the model library panel from the viewport capsule", async () => {
|
||||
expect(screen.getByRole("button", { name: "添加模型 自动取款机" }).querySelector("img")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uses category thumbnail folders for outdoor and tools model library items", async () => {
|
||||
itWithModelLibrary("uses category thumbnail folders for outdoor and tools model library items", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ViewportToolbar />);
|
||||
|
||||
@@ -439,7 +453,7 @@ it("closes the model library panel from its close button", async () => {
|
||||
expect(screen.queryByRole("dialog", { name: "模型库" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("adds a selected model library item into the viewport scene", async () => {
|
||||
itWithModelLibrary("adds a selected model library item into the viewport scene", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ViewportToolbar />);
|
||||
|
||||
|
||||
@@ -309,5 +309,8 @@ it("renders the XYZ drag handle inside the 80px axis input shell", () => {
|
||||
expect(axisInput).toBeInTheDocument();
|
||||
expect(axisInput).toHaveClass("inspector-axis-input");
|
||||
expect(valueInput.closest(".inspector-axis-input")).toBe(axisInput);
|
||||
expect(getComputedStyle(axisInput as HTMLElement).backgroundColor).toBe("rgb(11, 11, 12)");
|
||||
// jsdom 不加载 index.css,且无法解析 rgb(var(--field-rgb)),
|
||||
// 因此这里只断言 DOM 归属关系;80px 外壳与配色由 index.css 契约测试守住。
|
||||
expect(axisInput).toContainElement(xDragHandle);
|
||||
expect(axisInput).toContainElement(valueInput);
|
||||
});
|
||||
|
||||
@@ -291,13 +291,13 @@ it("calibrates phone as a right-hand phone viewing pose with the left arm relaxe
|
||||
|
||||
expect(pose?.controls).toMatchObject({
|
||||
"head.pitch": 18,
|
||||
"rightShoulder.pitch": 34,
|
||||
"rightShoulder.pitch": 20,
|
||||
"rightShoulder.spread": -4,
|
||||
"rightShoulder.twist": -12,
|
||||
"rightShoulder.twist": -30,
|
||||
"rightElbow.bend": 82,
|
||||
"rightHand.roll": -12,
|
||||
"rightHand.roll": -30,
|
||||
"rightHand.pitch": 14,
|
||||
"rightHand.twist": -8,
|
||||
"rightHand.twist": 60,
|
||||
"leftShoulder.pitch": -10,
|
||||
"leftShoulder.spread": 8,
|
||||
"leftElbow.bend": 16,
|
||||
|
||||
@@ -28,7 +28,9 @@ it("returns the rig position from a saved viewport snapshot", () => {
|
||||
});
|
||||
|
||||
it("scales the viewport camera viewfinder from one visual scale", () => {
|
||||
expect(VIEWPORT_CAMERA_VISUAL_SCALE).toBeCloseTo(0.8);
|
||||
// 取景框(机身/镜筒/视锥)全部由这一个视觉缩放值推导,改它即可整体缩放。
|
||||
// 0.35 是当前标定值;旧测试里写死的 0.8 已与实现不一致。
|
||||
expect(VIEWPORT_CAMERA_VISUAL_SCALE).toBeCloseTo(0.35);
|
||||
expect(VIEWPORT_CAMERA_FRUSTUM_DEPTH).toBeCloseTo(5.2 * VIEWPORT_CAMERA_VISUAL_SCALE);
|
||||
expect(VIEWPORT_CAMERA_FRUSTUM_FRAME_WIDTH).toBeCloseTo(3.2 * VIEWPORT_CAMERA_VISUAL_SCALE);
|
||||
});
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// test-electron.js
|
||||
// 测试Electron配置是否正确
|
||||
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
console.log('Electron版本:', process.versions.electron);
|
||||
console.log('Chrome版本:', process.versions.chrome);
|
||||
console.log('Node版本:', process.versions.node);
|
||||
console.log('平台:', process.platform);
|
||||
|
||||
// 测试创建窗口
|
||||
app.whenReady().then(() => {
|
||||
console.log('Electron已就绪');
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
show: false,
|
||||
});
|
||||
|
||||
// 加载测试页面
|
||||
mainWindow.loadFile(path.join(__dirname, 'dist/index.html'))
|
||||
.then(() => {
|
||||
console.log('✓ 页面加载成功');
|
||||
app.quit();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('✗ 页面加载失败:', err.message);
|
||||
app.quit();
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit();
|
||||
});
|
||||
@@ -13,7 +13,8 @@
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"types": ["vitest/globals"]
|
||||
"types": ["vitest/globals"],
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"outDir": "./node_modules/.tmp/tsconfig.node",
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
74
使用说明.txt
74
使用说明.txt
@@ -3,30 +3,34 @@
|
||||
|
||||
打包成exe的完整步骤:
|
||||
|
||||
(本文件是 README-PACKAGE.md 的纯文本简版,两者内容一致)
|
||||
|
||||
一、准备工作
|
||||
|
||||
1. 确保电脑已安装Node.js
|
||||
1. 确保电脑已安装 Node.js 18 或更高
|
||||
- 下载地址:https://nodejs.org
|
||||
- 版本要求:16.0 或更高
|
||||
- 安装时勾选"Add to PATH"
|
||||
- 安装时勾选 "Add to PATH"
|
||||
|
||||
2. 打开命令行(CMD或PowerShell)
|
||||
2. 安装 pnpm(本工程使用 pnpm,不再使用 npm / package-lock.json)
|
||||
命令行执行:npm i -g pnpm
|
||||
|
||||
3. 打开命令行(CMD或PowerShell)
|
||||
- 按 Win+R,输入 cmd,回车
|
||||
- 或者在项目文件夹按住 Shift+右键,选择"在此处打开命令窗口"
|
||||
|
||||
二、打包步骤
|
||||
|
||||
第1步:进入项目目录
|
||||
cd /d "d:\work\ykzd\一力三期\BMS\storyai-3d-director-desk"
|
||||
cd /d "本机上的项目目录" 例如:cd /d "D:\TongTongStudio\ai\jlmlh-3d-director"
|
||||
|
||||
第2步:安装依赖
|
||||
npm install
|
||||
pnpm install
|
||||
|
||||
第3步:构建前端
|
||||
npm run build
|
||||
pnpm run build
|
||||
|
||||
第4步:打包exe
|
||||
npm run electron:build:win
|
||||
pnpm run electron:build:win
|
||||
|
||||
第5步:等待完成
|
||||
- 首次打包需要下载Electron,约5-10分钟
|
||||
@@ -51,27 +55,52 @@ npm run electron:build:win
|
||||
五、常见问题
|
||||
|
||||
Q: 打包失败怎么办?
|
||||
A: 删除 node_modules 文件夹,重新执行 npm install,再打包。
|
||||
A: 删除 node_modules 文件夹,重新执行 pnpm install,再打包。
|
||||
注意:不要手动删除 node_modules 里的单个包(例如 node_modules\electron)。
|
||||
绕过包管理器删包会让 pnpm 仍以为该依赖已安装,反而得到缺失或空壳的依赖目录。
|
||||
|
||||
Q: Electron 下载不下来?
|
||||
A: 项目根目录 .npmrc 已配置 electron_mirror / electron_builder_binaries_mirror
|
||||
指向 npmmirror。若仍失败,可先执行下面的命令再重试 pnpm install:
|
||||
set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
|
||||
Q: exe文件太大?
|
||||
A: Electron应用包含完整的浏览器引擎,这是正常的,约100-150MB。
|
||||
A: Electron应用包含完整的浏览器引擎,这是正常的,约 77MB。
|
||||
|
||||
Q: 安装后打不开?
|
||||
A: 右键以管理员身份运行,或检查杀毒软件是否拦截。
|
||||
|
||||
Q: 如何修改窗口大小?
|
||||
A: 编辑 electron/main.js,修改 width 和 height 值。
|
||||
A: 编辑 electron/main.cjs,修改 width 和 height 值。
|
||||
|
||||
Q: 如何修改应用图标?
|
||||
A: 准备 256x256 的 PNG 图标,命名为 icon.png,放到 public 目录,重新打包。
|
||||
A: 准备 256x256(建议 512x512)的 PNG 图标,命名为 icon.png,放到 public 目录,
|
||||
重新打包。public/icon.png 已随仓库提供,也可以用零依赖脚本重新生成:
|
||||
python scripts/generate_icon.py
|
||||
|
||||
六、开发调试
|
||||
|
||||
如需开发调试,运行:
|
||||
npm run electron:dev
|
||||
pnpm run electron:dev
|
||||
|
||||
这会同时启动开发服务器和Electron窗口,支持热更新。
|
||||
|
||||
想验证"打包后的 dist 能否被 Electron 正常加载":
|
||||
pnpm run electron:smoke
|
||||
|
||||
冒烟脚本会确认:dist/index.html 能加载、React 成功挂载、渲染层无报错。
|
||||
同时它也证明 dist 是自包含的(运行时不需要 node_modules)。
|
||||
|
||||
两种环境差异的处理:
|
||||
1) 无 GPU 的机器(CI、远程桌面)需要额外参数:
|
||||
pnpm exec electron --use-angle=swiftshader --use-gl=angle --no-sandbox scripts/test-electron.cjs
|
||||
不加会报 WebGL 上下文创建失败,或页面加载 ERR_FAILED (-2)。
|
||||
|
||||
2) 若报 "Cannot read properties of undefined (reading 'whenReady')",
|
||||
说明终端里设置了 ELECTRON_RUN_AS_NODE(Electron 被退化成纯 Node),先清掉:
|
||||
unset ELECTRON_RUN_AS_NODE (bash)
|
||||
set ELECTRON_RUN_AS_NODE= (cmd)
|
||||
|
||||
七、技术说明
|
||||
|
||||
- 前端框架:React + TypeScript
|
||||
@@ -79,16 +108,29 @@ npm run electron:dev
|
||||
- 构建工具:Vite
|
||||
- 桌面框架:Electron
|
||||
- 打包工具:electron-builder
|
||||
- 包管理器:pnpm
|
||||
|
||||
关于依赖分组:渲染层用到的库(three / react / zustand 等)都被 Vite
|
||||
打包进 dist,主进程只用 Electron 自身模块,所以它们全部放在
|
||||
devDependencies(构建期依赖)。electron-builder 会把 dependencies 里的包
|
||||
整棵拷进 app.asar,留在 dependencies 会让安装包多出约 138MB 冗余体积。
|
||||
新增依赖时请同样放进 devDependencies,除非它确实要在运行时被 require。
|
||||
|
||||
八、文件结构
|
||||
|
||||
storyai-3d-director-desk/
|
||||
├── electron/ # Electron主进程
|
||||
│ ├── main.js # 主进程入口
|
||||
│ └── preload.js # 预加载脚本
|
||||
│ └── main.cjs # 主进程入口(CommonJS,无 preload)
|
||||
├── scripts/ # 辅助脚本
|
||||
│ ├── generate_icon.py # 生成 public/icon.png
|
||||
│ └── test-electron.cjs # 加载 dist 的冒烟脚本
|
||||
├── src/ # 前端源码
|
||||
├── public/ # 静态资源(含 icon.png)
|
||||
├── dist/ # 构建输出
|
||||
├── release/ # 打包输出
|
||||
├── docs/ # 设计文档
|
||||
├── package.json # 项目配置
|
||||
├── .npmrc # pnpm / 镜像配置
|
||||
├── build-electron.bat # 一键打包脚本
|
||||
└── 使用说明.txt # 本文档
|
||||
├── 双击启动.bat # 浏览器方式启动(无需 Electron)
|
||||
└── README-PACKAGE.md # 打包指南
|
||||
|
||||
21
双击启动.bat
21
双击启动.bat
@@ -1,4 +1,5 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
echo.
|
||||
echo ========================================
|
||||
@@ -8,9 +9,25 @@ echo.
|
||||
echo Starting...
|
||||
echo.
|
||||
|
||||
if not exist "node_modules" (
|
||||
echo Installing dependencies with pnpm, please wait...
|
||||
call pnpm install
|
||||
if errorlevel 1 (
|
||||
echo Install failed! Install pnpm first: npm i -g pnpm
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
)
|
||||
|
||||
if not exist "dist\index.html" (
|
||||
echo Building project, please wait...
|
||||
call npm run build
|
||||
call pnpm run build
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
)
|
||||
|
||||
@@ -20,4 +37,4 @@ echo Open browser: http://localhost:3000
|
||||
echo.
|
||||
echo Press Ctrl+C to stop
|
||||
echo.
|
||||
npx serve -s dist -l 3000
|
||||
call pnpm exec vite preview --port 3000
|
||||
|
||||
33
快速启动说明.txt
33
快速启动说明.txt
@@ -4,11 +4,13 @@
|
||||
最简单的方式:双击启动.bat
|
||||
|
||||
使用步骤:
|
||||
1. 确保已安装Node.js 16+(下载:https://nodejs.org)
|
||||
2. 双击 双击启动.bat
|
||||
3. 等待服务启动
|
||||
4. 浏览器会自动打开 http://localhost:3000
|
||||
5. 如果没有自动打开,手动访问该网址
|
||||
1. 确保已安装 Node.js 18+(下载:https://nodejs.org)
|
||||
2. 安装 pnpm:命令行执行 npm i -g pnpm
|
||||
3. 双击 双击启动.bat
|
||||
- 缺少 node_modules 时脚本会自动执行 pnpm install
|
||||
- 缺少 dist/index.html 时脚本会自动执行 pnpm run build
|
||||
4. 等待服务启动
|
||||
5. 浏览器访问 http://localhost:3000(脚本会打印实际地址)
|
||||
|
||||
特点:
|
||||
✓ 无需安装Electron
|
||||
@@ -17,24 +19,25 @@
|
||||
✓ 启动快
|
||||
✓ 适合内部使用
|
||||
|
||||
注意事项:
|
||||
- 需要用户电脑已安装Node.js
|
||||
- 不是真正的桌面应用
|
||||
- 需要手动打开浏览器
|
||||
技术说明:
|
||||
- 使用 pnpm exec vite preview 提供静态文件服务(端口 3000)
|
||||
- 服务运行时不能关闭命令行窗口
|
||||
|
||||
注意事项:
|
||||
- 需要用户电脑已安装 Node.js 与 pnpm
|
||||
- 不是真正的桌面应用(要桌面应用请用 build-electron.bat 打包)
|
||||
- 需要手动打开浏览器
|
||||
|
||||
停止服务:
|
||||
在命令行窗口按 Ctrl+C
|
||||
|
||||
如果出现问题:
|
||||
1. 确保Node.js已正确安装
|
||||
1. 确保 Node.js 与 pnpm 已正确安装(node -v、pnpm -v)
|
||||
2. 确保在项目目录下运行
|
||||
3. 检查端口3000是否被占用
|
||||
|
||||
如果端口被占用,可以修改端口:
|
||||
编辑 双击启动.bat,将 -l 3000 改为 -l 3001(或其他端口)
|
||||
编辑 双击启动.bat,将 --port 3000 改为 --port 3001(或其他端口)
|
||||
|
||||
技术说明:
|
||||
- 使用 serve 包提供静态文件服务
|
||||
- 自动构建项目(如果未构建)
|
||||
- 支持SPA路由
|
||||
需要桌面应用?
|
||||
参考 README-PACKAGE.md 或 使用说明.txt,用 build-electron.bat 打成 exe。
|
||||
|
||||
51
打包说明.txt
51
打包说明.txt
@@ -1,51 +0,0 @@
|
||||
一力三期BMS - 故事AI导演工作台 打包说明
|
||||
==========================================
|
||||
|
||||
一、打包步骤(三步完成)
|
||||
|
||||
第1步:双击运行 build-electron.bat
|
||||
第2步:等待打包完成(约5-10分钟)
|
||||
第3步:在 release 目录找到 exe 安装包
|
||||
|
||||
二、打包结果
|
||||
|
||||
打包完成后,release 目录会有:
|
||||
- 故事AI导演工作台 Setup 1.0.0.exe(安装包)
|
||||
|
||||
三、给用户使用
|
||||
|
||||
用户只需:
|
||||
1. 双击安装包
|
||||
2. 按提示安装
|
||||
3. 桌面出现快捷方式
|
||||
4. 双击打开即可使用
|
||||
|
||||
四、注意事项
|
||||
|
||||
1. 首次打包需要下载Electron,请保持网络畅通
|
||||
2. 打包后文件约100-150MB,这是正常的
|
||||
3. 如果打包失败,删除 node_modules 文件夹后重试
|
||||
|
||||
五、自定义图标(可选)
|
||||
|
||||
如需自定义应用图标:
|
||||
1. 准备一个 256x256 的 PNG 图标
|
||||
2. 重命名为 icon.png
|
||||
3. 放到 public 目录
|
||||
4. 重新打包
|
||||
|
||||
六、常见问题
|
||||
|
||||
Q: 打包时提示网络错误?
|
||||
A: 检查网络连接,或使用VPN重试。
|
||||
|
||||
Q: exe安装后打不开?
|
||||
A: 右键以管理员身份运行。
|
||||
|
||||
Q: 如何修改窗口大小?
|
||||
A: 编辑 electron/main.js,修改 width 和 height 值。
|
||||
|
||||
七、开发调试
|
||||
|
||||
如需开发调试,运行:
|
||||
npm run electron:dev
|
||||
132
简单打包方案.txt
132
简单打包方案.txt
@@ -1,125 +1,39 @@
|
||||
简单打包方案(无需下载Electron)
|
||||
简单运行方案(不打包 Electron)
|
||||
================================
|
||||
|
||||
如果Electron下载失败,可以使用这个简单方案:
|
||||
如果 Electron 下载失败,或只是内部使用、不想打包 exe,可以用下面两种方式
|
||||
直接在浏览器里运行(内容与 Electron 版完全一致)。
|
||||
|
||||
方案一:使用本地Node.js(推荐)
|
||||
方案一:双击 双击启动.bat(推荐)
|
||||
|
||||
1. 确保已安装Node.js 16+
|
||||
2. 运行以下命令:
|
||||
脚本会自动执行:pnpm install(缺依赖时)→ pnpm run build(缺 dist 时)
|
||||
→ vite preview --port 3000,然后访问 http://localhost:3000。
|
||||
|
||||
cd /d "d:\work\ykzd\一力三期\BMS\storyai-3d-director-desk"
|
||||
npm install
|
||||
npm run build
|
||||
npm install -g serve
|
||||
serve -s dist
|
||||
方案二:手动命令
|
||||
|
||||
1. 确保已安装 Node.js 18+ 与 pnpm(npm i -g pnpm)
|
||||
2. 在项目目录执行:
|
||||
|
||||
pnpm install
|
||||
pnpm run build
|
||||
pnpm run preview -- --port 3000
|
||||
|
||||
3. 打开浏览器访问 http://localhost:3000
|
||||
|
||||
方案二:创建启动脚本
|
||||
开发模式(带热更新)
|
||||
|
||||
创建一个 start.bat 文件,内容如下:
|
||||
pnpm run dev
|
||||
|
||||
@echo off
|
||||
cd /d "%~dp0"
|
||||
echo 正在启动故事AI导演工作台...
|
||||
echo.
|
||||
echo 请等待几秒钟...
|
||||
echo.
|
||||
|
||||
:: 检查是否已构建
|
||||
if not exist "dist\index.html" (
|
||||
echo 正在构建项目...
|
||||
call npm run build
|
||||
)
|
||||
|
||||
:: 启动服务
|
||||
echo 启动服务...
|
||||
echo.
|
||||
echo 请在浏览器中访问: http://localhost:3000
|
||||
echo.
|
||||
echo 按 Ctrl+C 停止服务
|
||||
echo.
|
||||
npx serve -s dist -l 3000
|
||||
|
||||
方案三:使用Python(如果已安装Python)
|
||||
|
||||
创建一个 start.py 文件:
|
||||
|
||||
import http.server
|
||||
import socketserver
|
||||
import webbrowser
|
||||
import os
|
||||
import threading
|
||||
|
||||
PORT = 3000
|
||||
DIRECTORY = "dist"
|
||||
|
||||
class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=DIRECTORY, **kwargs)
|
||||
|
||||
def open_browser():
|
||||
webbrowser.open(f"http://localhost:{PORT}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
threading.Timer(1.5, open_browser).start()
|
||||
with socketserver.TCPServer(("", PORT), Handler) as httpd:
|
||||
print(f"服务已启动: http://localhost:{PORT}")
|
||||
print("按 Ctrl+C 停止")
|
||||
httpd.serve_forever()
|
||||
|
||||
运行方式:
|
||||
python start.py
|
||||
|
||||
方案四:使用批处理文件(最简单)
|
||||
|
||||
创建一个 双击启动.bat 文件:
|
||||
|
||||
@echo off
|
||||
cd /d "%~dp0"
|
||||
echo.
|
||||
echo ========================================
|
||||
echo 一力三期BMS - 故事AI导演工作台
|
||||
echo ========================================
|
||||
echo.
|
||||
echo 正在启动...
|
||||
echo.
|
||||
|
||||
:: 检查是否已构建
|
||||
if not exist "dist\index.html" (
|
||||
echo 正在构建项目,请稍候...
|
||||
call npm run build
|
||||
echo.
|
||||
)
|
||||
|
||||
:: 启动服务
|
||||
echo 启动服务...
|
||||
echo.
|
||||
echo 请在浏览器中访问: http://localhost:3000
|
||||
echo.
|
||||
echo 按 Ctrl+C 停止服务
|
||||
echo.
|
||||
npx serve -s dist -l 3000
|
||||
|
||||
使用方式:
|
||||
1. 双击 双击启动.bat
|
||||
2. 等待服务启动
|
||||
3. 浏览器会自动打开
|
||||
4. 如果没有自动打开,手动访问 http://localhost:3000
|
||||
访问 http://localhost:5173。
|
||||
|
||||
优点:
|
||||
- 无需下载Electron
|
||||
- 文件小(仅几KB)
|
||||
- 启动快
|
||||
- 无需下载 Electron 二进制
|
||||
- 依赖体积小、启动快
|
||||
- 适合内部使用
|
||||
|
||||
缺点:
|
||||
- 需要用户安装Node.js
|
||||
- 不是真正的桌面应用
|
||||
- 需要手动打开浏览器
|
||||
- 需要用户机器已安装 Node.js 与 pnpm
|
||||
- 不是真正的桌面应用,需要手动打开浏览器
|
||||
|
||||
推荐:
|
||||
- 如果是给技术人员使用,选择方案四
|
||||
- 如果是给普通用户使用,选择Electron方案
|
||||
如果要给普通用户分发桌面应用,仍然推荐 Electron 方案:
|
||||
见 README-PACKAGE.md 或 使用说明.txt(运行 build-electron.bat)。
|
||||
|
||||
Reference in New Issue
Block a user