# premise

这是我在展会看到的友商上位机 UI（`assets/paxini.jpg`）的风格参考，希望做一份相近气质的前端界面。前端由你负责实现与迭代。

## 约束（长期有效）

- 使用 `Svelte + TypeScript`
- 页面拆分成 `HudPanel`、`CenterStage`、`SignalChart`
- 不引入大型 UI 库
- 主色调：近黑背景 + 青蓝 / 荧光绿 / 橙红
- 主界面整体是“一整块 board”，通过渐变过渡，不做强分块
- 不做明显流光/扫光动画（可有静态纹理层次）
- 样式集中在组件内或 `theme` 文件
- 布局优先 `grid/flex`，仅在必要位置使用绝对定位
- 后续对接 Tauri，组件层不要把数据源写死

---

# 当前实现基线（已完成）

## Step1：主背景

- 已完成近黑全屏背景、弱渐变、暗角和轻噪声层
- `:root/html/body` 背景已与主界面统一，避免拖动窗口出现黑边断层

## Step2：TitleBar + ControlBar + 主布局

- 已完成窗口按钮（最小化 / 最大化切换 / 关闭）
- 已完成 ControlBar：
  - 配置菜单组（打开/关闭/校准/参数）
  - 连接状态卡片
  - 串口下拉框
  - 中英文切换
  - 设备信息行（设备/采样率/通道）
- 页面主结构已固定为：`TitleBar + ControlBar + WebGL2 Area`

## Step2-1：Tauri 交互与窗口体验

- TitleBar 已支持拖动（`data-tauri-drag-region`）
- 默认窗口大小自动尝试调整到屏幕约 `3/4`
- 已连接 Tauri window commands：

```ts
invoke("win_minimize");
invoke("win_toggle_maximize");
invoke("win_close");
```

## Step3：HUD 面板与数据演示

- 左右两侧悬浮曲线面板已完成
- 有数据时入场并渲染，无数据时退场（当前 demo：5s 开/关周期）
- 折线、渐变填充、边框与深色半透明底已完成
- 底部量程条已完成

---

# 当前前端结构（请按此扩展）

## 1) `src/routes/+page.svelte`

- 页面编排与状态管理
- 维护 `signalPanels`、语言、连接状态、串口、配置菜单选中态
- 将左右面板分流后传给 `CenterStage`
- 处理 Tauri invoke（窗口控制）

## 2) `src/lib/components/HudPanel.svelte`

- 只负责顶部控制区 UI 与交互事件分发
- 通过事件向外抛出：
  - `windowcontrol`
  - `localechange`
  - `configlink`
  - `portchange`

## 3) `src/lib/components/CenterStage.svelte`

- 承载 WebGL2 主区域和左右悬浮 rail
- 接收 `leftPanels/rightPanels` 并在 rail 内渲染 `SignalChart`
- 通过 `ResizeObserver` 动态计算：
  - `panel-zone` 顶部起始位置
  - 左右 rail 缩放比例（低高度窗口可自适应）
- 侧边定位策略：贴边但保留 `edge inset` 安全距离

## 4) `src/lib/components/SignalChart.svelte`

- 单个曲线面板渲染组件
- 仅消费结构化数据，不关心数据来源
- 支持 `side`（left/right）与 `active`（入场/退场）状态

---

# 关键布局规则（避免回归）

- WebGL2 区域占据下半主容器
- HUD 曲线面板浮在 WebGL2 上方，但贴近左右边缘，不遮挡中间核心区域
- 左右面板必须挂在 `CenterStage` 的 rail 容器内
- 不再使用命名 slot 传曲线面板（曾导致面板落到错误容器）
- 调试用彩色边框已移除，保持正式视觉

---

# 数据接入约定（统一结构）

```ts
type SignalTone = "cyan" | "lime" | "orange";
type SignalPanelSide = "left" | "right";

interface HudSignalSeries {
  id: string;
  tone: SignalTone;
  points: number[];
}

interface HudSignalIcon {
  id: string;
  label: string;
  tone: SignalTone;
}

interface HudSignalPanel {
  id: string;
  code: string;
  title: string;
  side: SignalPanelSide;
  active: boolean;
  series: HudSignalSeries[];
  icons: HudSignalIcon[];
}

interface HudPacket {
  ts: number;
  panels: HudSignalPanel[];
}
```

统一入口：

```ts
let signalPanels: HudSignalPanel[] = [];

function applyPacket(packet: HudPacket) {
  signalPanels = packet.panels;
}
```

---

# Step3 数据源 Demo（可替换）

## Demo A：本地 Mock

```ts
function startMockFeed(push: (packet: HudPacket) => void): () => void {
  let hasData = false;
  const cycle = setInterval(() => {
    hasData = !hasData;
    push({
      ts: Date.now(),
      panels: hasData ? buildRandomPanels() : buildInactivePanels()
    });
  }, 5000);

  const tick = setInterval(() => {
    if (hasData) {
      push({
        ts: Date.now(),
        panels: evolvePanels()
      });
    }
  }, 1200);

  return () => {
    clearInterval(cycle);
    clearInterval(tick);
  };
}
```

## Demo B：Tauri invoke 轮询

```ts
import { invoke } from "@tauri-apps/api/core";

async function fetchHudPacket(): Promise<HudPacket> {
  return await invoke<HudPacket>("hud_get_packet");
}
```

## Demo C：Tauri 事件推送

```ts
import { listen, type UnlistenFn } from "@tauri-apps/api/event";

async function startTauriStream(push: (packet: HudPacket) => void): Promise<() => void> {
  const unlisten: UnlistenFn = await listen<HudPacket>("hud_stream", (event) => {
    push(event.payload);
  });
  return () => unlisten();
}
```

## Demo D：WebSocket

```ts
function startWebSocket(push: (packet: HudPacket) => void): () => void {
  const ws = new WebSocket("ws://127.0.0.1:9001/hud");
  ws.onmessage = (e) => push(JSON.parse(e.data) as HudPacket);
  return () => ws.close();
}
```

---

# 其他接口建议（主控区联动）

```ts
invoke("sensor_connect", { port: "COM3" });
invoke("sensor_disconnect");

invoke("sensor_set_sample_rate", { hz: 120 });
invoke("sensor_set_channels", { channels: [1, 2, 3, 4] });

invoke("sensor_start_stream");
invoke("sensor_stop_stream");

invoke("sensor_get_status");
```

---

# step3 前端细节调整

1. 添加对串口连接失败的提醒
2. 连接成功后，连接按钮变成断开按钮点击就断开连接

# step 4 webgl2

1. 黑色背景，科技感 UI 容器
2. 一个 64x64 的压力矩阵
3. 用 InstancedMesh 渲染 4096 个小圆柱或小圆环
4. 每个实例根据 value 更新位置、缩放、颜色
5. 支持 OrbitControls 旋转缩放
6. 内置一个假数据生成器，能模拟脚印从无到有再消失
7. 数据更新用平滑插值，不要跳变
8. 左侧显示总压力、最大值、平均值
9. 支持 2D/3D 模式切换，但第一版 2D 可以只是俯视正交投影

# step4 config panel

我需要一个panel来进行参数配置，有以下配置（后面会加）

1. 点阵数量，目前默认是64*64，我希望可以自定义
2. range上下限，我希望可以自定义max和min来映射颜色
