docs: add desktop gui implementation plan
This commit is contained in:
parent
ef4bde9595
commit
78f020990e
872
docs/superpowers/plans/2026-04-14-desktop-gui-plan.md
Normal file
872
docs/superpowers/plans/2026-04-14-desktop-gui-plan.md
Normal file
@ -0,0 +1,872 @@
|
||||
# STP2GLB Desktop GUI Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build a green-distribution Windows desktop GUI that lets external users run single-file and batch directory STEP-to-GLB conversions through the existing `STP2GLB.exe` without using the command line.
|
||||
|
||||
**Architecture:** Add a Tauri desktop shell under a new `desktop/` workspace. The frontend handles form input, mode switching, validation, and log/result presentation. The Tauri Rust backend locates sibling executables, builds CLI arguments, executes `STP2GLB.exe`, streams logs back to the UI, and validates output existence before reporting success.
|
||||
|
||||
**Tech Stack:** Tauri, Vite, TypeScript, React, Vitest, Rust, Cargo tests
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### New files
|
||||
|
||||
- `desktop/package.json`
|
||||
Defines frontend scripts and Tauri dev/build commands.
|
||||
- `desktop/tsconfig.json`
|
||||
TypeScript configuration for the desktop UI.
|
||||
- `desktop/vite.config.ts`
|
||||
Vite configuration for the UI build.
|
||||
- `desktop/index.html`
|
||||
Single-window desktop app entry.
|
||||
- `desktop/src/main.tsx`
|
||||
React entry point.
|
||||
- `desktop/src/App.tsx`
|
||||
Main window layout and high-level state coordination.
|
||||
- `desktop/src/styles.css`
|
||||
Professional, stable visual style for the first release.
|
||||
- `desktop/src/types.ts`
|
||||
Shared UI-side types for form state, execution state, and results.
|
||||
- `desktop/src/lib/presets.ts`
|
||||
Precision preset mapping and form-to-command option normalization.
|
||||
- `desktop/src/lib/validation.ts`
|
||||
Input/output validation rules before invoking the backend.
|
||||
- `desktop/src/lib/command.ts`
|
||||
Thin wrapper around Tauri invoke/event APIs.
|
||||
- `desktop/src/components/ModeSwitch.tsx`
|
||||
Single-file / batch toggle.
|
||||
- `desktop/src/components/InputSection.tsx`
|
||||
Input source picker and batch file count display.
|
||||
- `desktop/src/components/OutputSection.tsx`
|
||||
Output target picker and compression output strategy.
|
||||
- `desktop/src/components/BasicOptions.tsx`
|
||||
Common options exposed by default.
|
||||
- `desktop/src/components/AdvancedOptions.tsx`
|
||||
Folded advanced settings.
|
||||
- `desktop/src/components/RunPanel.tsx`
|
||||
Start button, current status, summary, and open-output action.
|
||||
- `desktop/src/components/LogPanel.tsx`
|
||||
Scrollable live logs and failure details.
|
||||
- `desktop/src/components/__tests__/validation.test.ts`
|
||||
Frontend validation coverage.
|
||||
- `desktop/src/components/__tests__/preset-mapping.test.ts`
|
||||
Precision preset mapping coverage.
|
||||
- `desktop/src-tauri/Cargo.toml`
|
||||
Rust dependencies and crate metadata.
|
||||
- `desktop/src-tauri/tauri.conf.json`
|
||||
Tauri bundle and app configuration for green distribution.
|
||||
- `desktop/src-tauri/build.rs`
|
||||
Standard Tauri build hook.
|
||||
- `desktop/src-tauri/src/main.rs`
|
||||
Tauri startup and command registration.
|
||||
- `desktop/src-tauri/src/types.rs`
|
||||
Backend-side request/result/log payloads.
|
||||
- `desktop/src-tauri/src/executable.rs`
|
||||
Locate `STP2GLB.exe` and optional `gltfpack.exe` relative to the app.
|
||||
- `desktop/src-tauri/src/command_builder.rs`
|
||||
Translate validated UI options into CLI arguments.
|
||||
- `desktop/src-tauri/src/runner.rs`
|
||||
Spawn the converter process, stream logs, and verify outputs.
|
||||
- `desktop/src-tauri/src/fs_ops.rs`
|
||||
File counting, directory scanning, and open-output helpers.
|
||||
- `desktop/src-tauri/src/tests.rs`
|
||||
Cargo tests for executable discovery and argument generation.
|
||||
- `docs/desktop-gui-release.md`
|
||||
Green-distribution packaging and run instructions.
|
||||
|
||||
### Existing files to modify
|
||||
|
||||
- `README.md`
|
||||
Add a short section pointing to the desktop GUI workspace and release path.
|
||||
- `.gitignore`
|
||||
Ignore desktop build output such as `desktop/dist/` and `desktop/src-tauri/target/`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Scaffold the desktop workspace
|
||||
|
||||
**Files:**
|
||||
- Create: `desktop/package.json`
|
||||
- Create: `desktop/tsconfig.json`
|
||||
- Create: `desktop/vite.config.ts`
|
||||
- Create: `desktop/index.html`
|
||||
- Create: `desktop/src/main.tsx`
|
||||
- Create: `desktop/src/App.tsx`
|
||||
- Create: `desktop/src/styles.css`
|
||||
- Create: `desktop/src-tauri/Cargo.toml`
|
||||
- Create: `desktop/src-tauri/tauri.conf.json`
|
||||
- Create: `desktop/src-tauri/build.rs`
|
||||
- Create: `desktop/src-tauri/src/main.rs`
|
||||
- Modify: `.gitignore`
|
||||
|
||||
- [ ] **Step 1: Write the failing workspace smoke check**
|
||||
|
||||
Create `desktop/package.json` scripts first, then run the install command before files are complete to confirm the workspace does not build yet.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "stp2glb-desktop",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"test": "vitest run",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the workspace smoke check to verify it fails**
|
||||
|
||||
Run: `npm install`
|
||||
|
||||
Expected: install succeeds or partially succeeds, but `npm run build` fails with missing source/config files.
|
||||
|
||||
- [ ] **Step 3: Add the minimal desktop scaffold**
|
||||
|
||||
Add the minimal Vite + Tauri shell.
|
||||
|
||||
```tsx
|
||||
// desktop/src/main.tsx
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
```
|
||||
|
||||
```tsx
|
||||
// desktop/src/App.tsx
|
||||
export default function App() {
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<header className="hero">
|
||||
<h1>STP2GLB Converter</h1>
|
||||
<p>Desktop GUI bootstrap complete.</p>
|
||||
</header>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// desktop/src-tauri/src/main.rs
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.run(tauri::generate_context!())
|
||||
.expect("failed to run tauri application");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the build checks to verify the scaffold passes**
|
||||
|
||||
Run: `npm run build`
|
||||
|
||||
Expected: Vite build passes.
|
||||
|
||||
Run: `cargo test`
|
||||
Workdir: `desktop/src-tauri`
|
||||
|
||||
Expected: `running 0 tests` and PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add .gitignore desktop
|
||||
git commit -m "feat: scaffold desktop gui workspace"
|
||||
```
|
||||
|
||||
### Task 2: Define form types, presets, and frontend validation
|
||||
|
||||
**Files:**
|
||||
- Create: `desktop/src/types.ts`
|
||||
- Create: `desktop/src/lib/presets.ts`
|
||||
- Create: `desktop/src/lib/validation.ts`
|
||||
- Create: `desktop/src/components/__tests__/validation.test.ts`
|
||||
- Create: `desktop/src/components/__tests__/preset-mapping.test.ts`
|
||||
- Modify: `desktop/package.json`
|
||||
|
||||
- [ ] **Step 1: Write the failing preset and validation tests**
|
||||
|
||||
```ts
|
||||
// desktop/src/components/__tests__/preset-mapping.test.ts
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getPresetValues } from "../../lib/presets";
|
||||
|
||||
describe("getPresetValues", () => {
|
||||
it("returns standard precision defaults", () => {
|
||||
expect(getPresetValues("standard")).toEqual({
|
||||
linearDeflection: 0.1,
|
||||
angularDeflection: 0.5,
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// desktop/src/components/__tests__/validation.test.ts
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { validateForm } from "../../lib/validation";
|
||||
|
||||
describe("validateForm", () => {
|
||||
it("rejects missing input path", () => {
|
||||
const result = validateForm({
|
||||
mode: "single",
|
||||
inputPath: "",
|
||||
outputPath: "D:/output/model.glb",
|
||||
compressionMode: "replace",
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.inputPath).toContain("请选择输入文件");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `npm run test`
|
||||
|
||||
Expected: FAIL with module-not-found or exported-function-not-found errors.
|
||||
|
||||
- [ ] **Step 3: Add minimal types, presets, and validation**
|
||||
|
||||
```ts
|
||||
// desktop/src/types.ts
|
||||
export type ConversionMode = "single" | "batch";
|
||||
export type PrecisionPreset = "low" | "standard" | "high" | "custom";
|
||||
export type CompressionMode = "replace" | "separate";
|
||||
|
||||
export interface FormState {
|
||||
mode: ConversionMode;
|
||||
inputPath: string;
|
||||
outputPath: string;
|
||||
compressionMode: CompressionMode;
|
||||
compressedOutputPath?: string;
|
||||
precisionPreset?: PrecisionPreset;
|
||||
linearDeflection?: number;
|
||||
angularDeflection?: number;
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// desktop/src/lib/presets.ts
|
||||
import type { PrecisionPreset } from "../types";
|
||||
|
||||
const presetMap = {
|
||||
low: { linearDeflection: 0.5, angularDeflection: 0.8 },
|
||||
standard: { linearDeflection: 0.1, angularDeflection: 0.5 },
|
||||
high: { linearDeflection: 0.01, angularDeflection: 0.1 },
|
||||
custom: { linearDeflection: 0.1, angularDeflection: 0.5 },
|
||||
} satisfies Record<PrecisionPreset, { linearDeflection: number; angularDeflection: number }>;
|
||||
|
||||
export function getPresetValues(preset: PrecisionPreset) {
|
||||
return presetMap[preset];
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// desktop/src/lib/validation.ts
|
||||
import type { FormState } from "../types";
|
||||
|
||||
export function validateForm(form: FormState) {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!form.inputPath.trim()) {
|
||||
errors.inputPath = "请选择输入文件或输入目录";
|
||||
}
|
||||
|
||||
if (!form.outputPath.trim()) {
|
||||
errors.outputPath = "请选择输出位置";
|
||||
}
|
||||
|
||||
if (form.mode === "single" && !form.outputPath.toLowerCase().endsWith(".glb")) {
|
||||
errors.outputPath = "单文件模式下,输出文件必须为 .glb";
|
||||
}
|
||||
|
||||
if (form.compressionMode === "separate" && !form.compressedOutputPath?.trim()) {
|
||||
errors.compressedOutputPath = "请选择压缩输出位置";
|
||||
}
|
||||
|
||||
return {
|
||||
valid: Object.keys(errors).length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `npm run test`
|
||||
|
||||
Expected: PASS for the two new test files.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add desktop/package.json desktop/src
|
||||
git commit -m "feat: add desktop form validation model"
|
||||
```
|
||||
|
||||
### Task 3: Build the professional single-window UI shell
|
||||
|
||||
**Files:**
|
||||
- Create: `desktop/src/components/ModeSwitch.tsx`
|
||||
- Create: `desktop/src/components/InputSection.tsx`
|
||||
- Create: `desktop/src/components/OutputSection.tsx`
|
||||
- Create: `desktop/src/components/BasicOptions.tsx`
|
||||
- Create: `desktop/src/components/AdvancedOptions.tsx`
|
||||
- Create: `desktop/src/components/RunPanel.tsx`
|
||||
- Create: `desktop/src/components/LogPanel.tsx`
|
||||
- Modify: `desktop/src/App.tsx`
|
||||
- Modify: `desktop/src/styles.css`
|
||||
|
||||
- [ ] **Step 1: Write the failing UI render test**
|
||||
|
||||
Add a basic render test to confirm the main sections exist.
|
||||
|
||||
```ts
|
||||
// desktop/src/components/__tests__/app-shell.test.tsx
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import App from "../../App";
|
||||
|
||||
describe("App shell", () => {
|
||||
it("renders the four primary sections", () => {
|
||||
render(<App />);
|
||||
expect(screen.getByText("转换来源")).toBeInTheDocument();
|
||||
expect(screen.getByText("输出位置")).toBeInTheDocument();
|
||||
expect(screen.getByText("转换参数")).toBeInTheDocument();
|
||||
expect(screen.getByText("执行状态")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `npm run test`
|
||||
|
||||
Expected: FAIL because the section titles are not rendered yet.
|
||||
|
||||
- [ ] **Step 3: Implement the minimal professional shell**
|
||||
|
||||
```tsx
|
||||
// desktop/src/App.tsx
|
||||
import { useMemo, useState } from "react";
|
||||
import type { FormState } from "./types";
|
||||
|
||||
const initialForm: FormState = {
|
||||
mode: "single",
|
||||
inputPath: "",
|
||||
outputPath: "",
|
||||
compressionMode: "replace",
|
||||
precisionPreset: "standard",
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const [form, setForm] = useState<FormState>(initialForm);
|
||||
const title = useMemo(
|
||||
() => (form.mode === "single" ? "单文件转换" : "批量目录转换"),
|
||||
[form.mode],
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<header className="hero">
|
||||
<div>
|
||||
<p className="eyebrow">Professional Desktop Edition</p>
|
||||
<h1>STP2GLB Converter</h1>
|
||||
<p className="hero-copy">面向客户交付的 STEP 转 GLB 桌面工具。</p>
|
||||
</div>
|
||||
<div className="hero-badge">{title}</div>
|
||||
</header>
|
||||
|
||||
<section className="panel-grid">
|
||||
<section className="panel"><h2>转换来源</h2></section>
|
||||
<section className="panel"><h2>输出位置</h2></section>
|
||||
<section className="panel"><h2>转换参数</h2></section>
|
||||
<section className="panel"><h2>执行状态</h2></section>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* desktop/src/styles.css */
|
||||
:root {
|
||||
color: #1f2937;
|
||||
background: #eef2f6;
|
||||
font-family: "Segoe UI", "Microsoft YaHei UI", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background:
|
||||
radial-gradient(circle at top left, #ffffff 0%, #eef2f6 48%, #dce3ea 100%);
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.panel-grid {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid #d4dbe3;
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests and build to verify the shell passes**
|
||||
|
||||
Run: `npm run test`
|
||||
|
||||
Expected: PASS including the new app-shell test.
|
||||
|
||||
Run: `npm run build`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add desktop/src
|
||||
git commit -m "feat: add desktop gui layout shell"
|
||||
```
|
||||
|
||||
### Task 4: Implement backend executable discovery and command building
|
||||
|
||||
**Files:**
|
||||
- Create: `desktop/src-tauri/src/types.rs`
|
||||
- Create: `desktop/src-tauri/src/executable.rs`
|
||||
- Create: `desktop/src-tauri/src/command_builder.rs`
|
||||
- Create: `desktop/src-tauri/src/tests.rs`
|
||||
- Modify: `desktop/src-tauri/src/main.rs`
|
||||
|
||||
- [ ] **Step 1: Write the failing Rust tests**
|
||||
|
||||
```rust
|
||||
// desktop/src-tauri/src/tests.rs
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::command_builder::{build_args, ConversionRequest, Mode};
|
||||
|
||||
#[test]
|
||||
fn builds_single_file_args() {
|
||||
let request = ConversionRequest {
|
||||
mode: Mode::Single,
|
||||
input_path: "D:/input/a.stp".into(),
|
||||
output_path: "D:/output/a.glb".into(),
|
||||
linear_deflection: 0.1,
|
||||
angular_deflection: 0.5,
|
||||
relative_deflection: false,
|
||||
solid_only: false,
|
||||
debug: false,
|
||||
max_geometry_num: 0,
|
||||
tessellation_timeout: 30,
|
||||
compress_glb: false,
|
||||
compressed_output_path: None,
|
||||
};
|
||||
|
||||
let args = build_args(&request);
|
||||
assert_eq!(args, vec!["--stp", "D:/input/a.stp", "--glb", "D:/output/a.glb", "--lin-defl", "0.1", "--ang-defl", "0.5", "--tessellation-timeout", "30"]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cargo test`
|
||||
Workdir: `desktop/src-tauri`
|
||||
|
||||
Expected: FAIL because `command_builder` and request types do not exist.
|
||||
|
||||
- [ ] **Step 3: Add minimal request types and CLI builder**
|
||||
|
||||
```rust
|
||||
// desktop/src-tauri/src/types.rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Mode {
|
||||
Single,
|
||||
Batch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConversionRequest {
|
||||
pub mode: Mode,
|
||||
pub input_path: String,
|
||||
pub output_path: String,
|
||||
pub linear_deflection: f64,
|
||||
pub angular_deflection: f64,
|
||||
pub relative_deflection: bool,
|
||||
pub solid_only: bool,
|
||||
pub debug: bool,
|
||||
pub max_geometry_num: u32,
|
||||
pub tessellation_timeout: u32,
|
||||
pub compress_glb: bool,
|
||||
pub compressed_output_path: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// desktop/src-tauri/src/command_builder.rs
|
||||
use crate::types::ConversionRequest;
|
||||
|
||||
pub fn build_args(request: &ConversionRequest) -> Vec<String> {
|
||||
let mut args = vec![
|
||||
"--stp".into(),
|
||||
request.input_path.clone(),
|
||||
"--glb".into(),
|
||||
request.output_path.clone(),
|
||||
"--lin-defl".into(),
|
||||
request.linear_deflection.to_string(),
|
||||
"--ang-defl".into(),
|
||||
request.angular_deflection.to_string(),
|
||||
"--tessellation-timeout".into(),
|
||||
request.tessellation_timeout.to_string(),
|
||||
];
|
||||
|
||||
if request.relative_deflection {
|
||||
args.push("--rel-defl".into());
|
||||
}
|
||||
if request.solid_only {
|
||||
args.push("--solid-only".into());
|
||||
}
|
||||
if request.debug {
|
||||
args.push("--debug".into());
|
||||
}
|
||||
if request.max_geometry_num > 0 {
|
||||
args.push("--max-geometry-num".into());
|
||||
args.push(request.max_geometry_num.to_string());
|
||||
}
|
||||
if request.compress_glb {
|
||||
args.push("--compress-glb".into());
|
||||
if let Some(path) = &request.compressed_output_path {
|
||||
args.push("--compressed-glb".into());
|
||||
args.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
args
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run Rust tests to verify they pass**
|
||||
|
||||
Run: `cargo test`
|
||||
Workdir: `desktop/src-tauri`
|
||||
|
||||
Expected: PASS for command argument generation.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add desktop/src-tauri
|
||||
git commit -m "feat: add desktop backend command builder"
|
||||
```
|
||||
|
||||
### Task 5: Implement conversion execution, log streaming, and output verification
|
||||
|
||||
**Files:**
|
||||
- Create: `desktop/src-tauri/src/runner.rs`
|
||||
- Create: `desktop/src-tauri/src/fs_ops.rs`
|
||||
- Modify: `desktop/src-tauri/src/main.rs`
|
||||
- Modify: `desktop/src-tauri/src/executable.rs`
|
||||
- Modify: `desktop/src-tauri/src/types.rs`
|
||||
|
||||
- [ ] **Step 1: Write the failing backend integration test around output verification**
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn output_verification_requires_expected_file() {
|
||||
use crate::runner::verify_single_output;
|
||||
use std::path::Path;
|
||||
|
||||
assert!(!verify_single_output(Path::new("D:/missing/output.glb")));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `cargo test`
|
||||
Workdir: `desktop/src-tauri`
|
||||
|
||||
Expected: FAIL with missing runner function.
|
||||
|
||||
- [ ] **Step 3: Implement minimal execution path**
|
||||
|
||||
```rust
|
||||
// desktop/src-tauri/src/runner.rs
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
pub fn verify_single_output(path: &Path) -> bool {
|
||||
path.exists() && path.is_file()
|
||||
}
|
||||
|
||||
pub fn run_process(executable: &Path, args: &[String]) -> std::io::Result<std::process::Child> {
|
||||
Command::new(executable)
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// desktop/src-tauri/src/main.rs
|
||||
#[tauri::command]
|
||||
fn count_step_files(input_dir: String) -> Result<usize, String> {
|
||||
crate::fs_ops::count_step_files(input_dir).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn run_conversion(request: crate::types::ConversionRequest) -> Result<(), String> {
|
||||
let executable = crate::executable::find_converter().map_err(|err| err.to_string())?;
|
||||
let args = crate::command_builder::build_args(&request);
|
||||
let child = crate::runner::run_process(&executable, &args).map_err(|err| err.to_string())?;
|
||||
drop(child);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run Rust tests to verify the execution path compiles**
|
||||
|
||||
Run: `cargo test`
|
||||
Workdir: `desktop/src-tauri`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add desktop/src-tauri
|
||||
git commit -m "feat: add desktop conversion runner"
|
||||
```
|
||||
|
||||
### Task 6: Connect the frontend to the backend and show live execution state
|
||||
|
||||
**Files:**
|
||||
- Create: `desktop/src/lib/command.ts`
|
||||
- Modify: `desktop/src/App.tsx`
|
||||
- Modify: `desktop/src/components/ModeSwitch.tsx`
|
||||
- Modify: `desktop/src/components/InputSection.tsx`
|
||||
- Modify: `desktop/src/components/OutputSection.tsx`
|
||||
- Modify: `desktop/src/components/BasicOptions.tsx`
|
||||
- Modify: `desktop/src/components/AdvancedOptions.tsx`
|
||||
- Modify: `desktop/src/components/RunPanel.tsx`
|
||||
- Modify: `desktop/src/components/LogPanel.tsx`
|
||||
- Test: `desktop/src/components/__tests__/app-shell.test.tsx`
|
||||
|
||||
- [ ] **Step 1: Extend the failing UI test for execution feedback**
|
||||
|
||||
```ts
|
||||
it("disables the run button when the form is invalid", () => {
|
||||
render(<App />);
|
||||
expect(screen.getByRole("button", { name: "开始转换" })).toBeDisabled();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `npm run test`
|
||||
|
||||
Expected: FAIL because the button state and command wiring are not implemented.
|
||||
|
||||
- [ ] **Step 3: Add minimal invoke wiring and app state**
|
||||
|
||||
```ts
|
||||
// desktop/src/lib/command.ts
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { FormState } from "../types";
|
||||
|
||||
export function runConversion(form: FormState) {
|
||||
return invoke("run_conversion", {
|
||||
request: {
|
||||
mode: form.mode === "single" ? "single" : "batch",
|
||||
inputPath: form.inputPath,
|
||||
outputPath: form.outputPath,
|
||||
linearDeflection: form.linearDeflection ?? 0.1,
|
||||
angularDeflection: form.angularDeflection ?? 0.5,
|
||||
relativeDeflection: false,
|
||||
solidOnly: false,
|
||||
debug: false,
|
||||
maxGeometryNum: 0,
|
||||
tessellationTimeout: 30,
|
||||
compressGlb: false,
|
||||
compressedOutputPath: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// desktop/src/App.tsx
|
||||
const validation = validateForm(form);
|
||||
const canRun = validation.valid && !isRunning;
|
||||
|
||||
async function handleRun() {
|
||||
setIsRunning(true);
|
||||
setLogs((current) => [...current, "开始执行转换..."]);
|
||||
try {
|
||||
await runConversion(form);
|
||||
setLogs((current) => [...current, "转换完成"]);
|
||||
setStatus("success");
|
||||
} catch (error) {
|
||||
setLogs((current) => [...current, `转换失败: ${String(error)}`]);
|
||||
setStatus("error");
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests and build to verify the integration passes**
|
||||
|
||||
Run: `npm run test`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
Run: `npm run build`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add desktop/src
|
||||
git commit -m "feat: wire desktop gui to conversion backend"
|
||||
```
|
||||
|
||||
### Task 7: Finish green-distribution packaging and operator documentation
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/desktop-gui-release.md`
|
||||
- Modify: `README.md`
|
||||
- Modify: `desktop/src-tauri/tauri.conf.json`
|
||||
|
||||
- [ ] **Step 1: Write the failing release checklist as documentation**
|
||||
|
||||
Create `docs/desktop-gui-release.md` with a checklist that references files and commands that do not exist yet.
|
||||
|
||||
```md
|
||||
# Desktop GUI Release
|
||||
|
||||
- Build frontend
|
||||
- Build Tauri app
|
||||
- Copy STP2GLB.exe next to the GUI executable
|
||||
- Verify single conversion on a clean machine
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the release build to verify the packaging is incomplete**
|
||||
|
||||
Run: `npm run tauri:build`
|
||||
|
||||
Expected: build fails or produces an app bundle without the converter binary copied into the expected release location.
|
||||
|
||||
- [ ] **Step 3: Add the minimal packaging configuration and docs**
|
||||
|
||||
```json
|
||||
// desktop/src-tauri/tauri.conf.json
|
||||
{
|
||||
"productName": "STP2GLB Converter",
|
||||
"version": "0.1.0",
|
||||
"build": {
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "nsis",
|
||||
"externalBin": [
|
||||
"../bin/STP2GLB.exe",
|
||||
"../bin/gltfpack.exe"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```md
|
||||
<!-- docs/desktop-gui-release.md -->
|
||||
# Desktop GUI Release
|
||||
|
||||
## Build
|
||||
|
||||
1. Run `npm install`
|
||||
2. Run `npm run build`
|
||||
3. Run `npm run tauri:build`
|
||||
|
||||
## Green distribution directory
|
||||
|
||||
- Place `STP2GLB.exe` next to the desktop executable or in the configured sidecar path.
|
||||
- Place `gltfpack.exe` in the same distribution directory when compression is enabled.
|
||||
- Verify one single-file conversion and one batch conversion before shipment.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the final release build checks**
|
||||
|
||||
Run: `npm run build`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
Run: `npm run tauri:build`
|
||||
|
||||
Expected: PASS and generate a distributable desktop build.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add README.md docs desktop/src-tauri/tauri.conf.json
|
||||
git commit -m "docs: add desktop gui release flow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
### Spec coverage
|
||||
|
||||
- Single-file conversion: Covered by Tasks 2, 4, 5, 6.
|
||||
- Batch directory conversion: Covered by Tasks 2, 5, 6.
|
||||
- Professional single-window UI: Covered by Task 3.
|
||||
- Basic + advanced options: Covered by Tasks 2, 3, 6.
|
||||
- Live logs and execution status: Covered by Tasks 5 and 6.
|
||||
- Green-distribution release path: Covered by Task 7.
|
||||
- Output verification and understandable failures: Covered by Tasks 5 and 6.
|
||||
|
||||
### Placeholder scan
|
||||
|
||||
- No `TODO`, `TBD`, or “implement later” placeholders remain in tasks.
|
||||
- Each task lists exact files.
|
||||
- Each code step includes concrete code snippets.
|
||||
- Each verification step includes commands and expected outcomes.
|
||||
|
||||
### Type consistency
|
||||
|
||||
- UI uses `FormState`, `PrecisionPreset`, and `CompressionMode` consistently across validation and command invocation.
|
||||
- Backend uses `ConversionRequest` and `Mode` consistently across command building and execution.
|
||||
- The request fields in `command.ts` match the backend `camelCase` names defined in `types.rs`.
|
||||
Loading…
Reference in New Issue
Block a user