first commit
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/tauri-demo.iml" filepath="$PROJECT_DIR$/.idea/tauri-demo.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
11
.idea/tauri-demo.iml
generated
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="EMPTY_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src-tauri/src" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/src-tauri/target" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"svelte.svelte-vscode",
|
||||||
|
"tauri-apps.tauri-vscode",
|
||||||
|
"rust-lang.rust-analyzer"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"svelte.enable-ts-plugin": true
|
||||||
|
}
|
||||||
7
README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Tauri + SvelteKit + TypeScript
|
||||||
|
|
||||||
|
This template should help get you started developing with Tauri, SvelteKit and TypeScript in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
|
||||||
BIN
assest/paxini.jpg
Normal file
|
After Width: | Height: | Size: 7.9 MiB |
1
flowus_tools.json
Normal file
254
frontend_prompt.md
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# 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来映射颜色
|
||||||
1957
package-lock.json
generated
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "tauri-demo",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"three": "^0.183.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-static": "^3.0.6",
|
||||||
|
"@sveltejs/kit": "^2.9.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
"@tauri-apps/cli": "^2",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
||||||
5664
src-tauri/Cargo.lock
generated
Normal file
34
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[package]
|
||||||
|
name = "tauri-demo"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
|
name = "tauri_demo_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = ["tray-icon"] }
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
anyhow = "1.0.102"
|
||||||
|
tokio-serial = { version = "5.4.5" }
|
||||||
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
tokio-util = "0.7.18"
|
||||||
|
serde_json = "1"
|
||||||
|
fern = { version = "0.7.1", features = ["colored", "date-based"] }
|
||||||
|
log = "0.4.29"
|
||||||
|
humantime = "2.3.0"
|
||||||
|
csv = "1.4.0"
|
||||||
|
chrono = "0.4.44"
|
||||||
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
15
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Capability for the main window",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"core:window:allow-center",
|
||||||
|
"core:window:allow-current-monitor",
|
||||||
|
"core:window:allow-inner-size",
|
||||||
|
"core:window:allow-set-size",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
|
"opener:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 906 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 906 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 906 KiB |
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 906 KiB |
361
src-tauri/recording_replay_debug_20260330.csv
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
channel1,channel2,channel3,channel4,channel5,channel6,channel7,channel8,channel9,channel10,channel11,channel12,channel13,channel14,channel15,channel16,channel17,channel18,channel19,channel20,channel21,channel22,channel23,channel24,channel25,channel26,channel27,channel28,channel29,channel30,channel31,channel32,channel33,channel34,channel35,channel36,channel37,channel38,channel39,channel40,channel41,channel42,channel43,channel44,channel45,channel46,channel47,channel48,channel49,channel50,channel51,channel52,channel53,channel54,channel55,channel56,channel57,channel58,channel59,channel60,channel61,channel62,channel63,channel64,channel65,channel66,channel67,channel68,channel69,channel70,channel71,channel72,channel73,channel74,channel75,channel76,channel77,channel78,channel79,channel80,channel81,channel82,channel83,channel84,dts
|
||||||
|
3748,4447,2765,2814,3154,4834,4314,3093,1325,177,1870,2839,2522,3465,3098,905,4761,2290,1908,4899,3021,4467,3824,4920,5000,4420,2633,4187,1887,2670,4990,4196,532,4176,1400,288,4006,218,4744,539,5000,5000,5000,4841,1946,1611,1224,2173,4118,5000,1975,1763,4747,1611,2866,4954,3566,2973,1434,1225,3192,4116,5000,152,3274,1919,1059,2380,1456,4029,1642,4279,426,3722,3423,4743,1150,5000,3017,1363,2352,5000,3010,2711,0
|
||||||
|
3778,4415,2733,2781,3119,4795,4271,3109,1338,218,1912,2872,2553,3492,3120,921,4710,2234,1894,4885,3004,4443,3789,4941,5000,4427,2653,4220,1920,2703,5000,4223,492,4135,1356,242,3982,195,4781,572,5000,5000,5000,4845,1958,1653,1266,2151,4091,5000,1931,1723,4705,1566,2902,4985,3597,3003,1461,1247,3210,4129,5000,134,3256,1898,1029,2345,1415,4044,1651,4284,475,3772,3469,4782,1177,5000,2978,1321,2309,5000,2983,2683,35
|
||||||
|
3808,4445,2343,2810,3145,4818,4290,3062,1288,198,1891,2843,2522,3456,3141,936,4720,2241,1941,4932,3047,4479,3816,4899,5000,4371,2612,4191,1892,2735,5000,4249,513,4156,1374,257,4020,233,4755,543,5000,5000,4976,4787,1908,1695,1308,2191,4126,5000,1948,1745,4724,1583,2877,4955,3567,2970,1425,1208,3228,4143,5000,177,3299,1939,1059,2372,1436,3997,1598,4227,463,3759,3452,4757,1203,5000,3000,1341,2327,5000,2599,2717,71
|
||||||
|
3776,4414,2311,2776,3109,4841,4309,3077,1300,239,1932,2876,2552,3482,3100,889,4668,2185,1927,4917,3090,4514,3842,4919,5000,4377,2632,4223,1925,2705,5000,4212,471,4114,1330,210,4059,272,4791,995,5000,5000,4986,4791,1920,1675,1288,2168,4097,5000,1902,1766,4743,1600,2913,4987,3598,2999,1451,1229,3183,4094,4975,159,3280,1916,1028,2398,1456,4011,1607,4232,512,3807,3496,4792,1167,5000,2961,1719,2283,5000,2634,2750,108
|
||||||
|
3806,4444,2341,2804,3134,4801,4265,3030,1251,218,1910,2909,2582,3507,3120,904,4677,2191,1974,4963,3069,4485,3806,4876,5000,4321,2590,4256,1958,2737,5000,4237,491,4135,1347,226,4035,248,4765,963,5000,5000,4996,4793,1931,1717,1330,2207,4130,5000,1917,1726,4700,1555,2887,4957,3567,2966,1476,1251,3200,4106,4985,202,3323,1955,1058,2362,1414,3962,1553,4175,499,3855,3538,4826,1192,5000,2983,1739,2302,5000,2607,2722,146
|
||||||
|
3773,3992,2370,2832,3160,4822,4283,3044,1263,258,1949,2879,2549,3470,3077,855,4625,2136,2021,5000,3109,4516,3831,4894,5000,4327,2611,4227,1928,2706,5000,4199,448,4155,1364,241,4073,285,4800,994,5000,5000,4942,4734,1881,1697,1309,2182,4100,5000,1931,1747,4718,1572,2923,4988,3597,2995,1439,1210,3154,4057,4933,183,3365,1992,1088,2387,1433,3975,1561,4179,548,3840,3518,4796,1155,5000,2943,1697,2320,4998,2641,2754,185
|
||||||
|
3803,4022,2338,2798,3122,4781,4238,2996,1214,299,1989,2911,2578,3495,3096,869,4634,2142,2006,4991,3086,4484,3793,4850,5000,4332,2631,4259,1961,2737,5000,4223,466,4113,1319,194,4048,260,5000,961,5000,5000,4949,4736,1893,1738,1350,2220,4131,5000,1881,1705,4675,1527,2897,5000,3628,3023,1464,1230,3170,4069,4943,226,3344,1967,1056,2349,1390,3926,1506,4184,596,3886,3558,4827,1179,5000,3385,1717,2276,4971,2613,2725,225
|
||||||
|
3833,4052,2367,2825,3147,4802,4255,3011,1226,277,1965,2881,2545,3457,3053,882,4642,2149,2052,5000,3123,4512,3817,4867,5000,4275,2589,4230,1931,2706,5000,4245,484,4132,1337,209,4086,297,5000,989,5000,5000,4894,4675,1843,1718,1391,2256,4161,5000,1893,1726,4693,1544,2933,4989,3596,2989,1426,1188,3124,4019,4954,269,3384,2002,1085,2373,1408,3938,1514,4127,582,3869,3535,4794,1141,5000,3407,1737,2295,5000,2647,2756,259
|
||||||
|
3381,4020,2334,2790,3109,4761,4272,3025,1239,317,2003,2912,2573,3480,3071,833,4589,2093,2036,5000,3098,4477,3841,4884,5000,4279,2610,4262,1963,2736,5000,4205,439,4090,1291,163,4062,334,5000,1017,4673,5000,4900,4677,1854,1759,1369,2230,4128,5000,1841,1685,4650,1562,2968,5000,3626,3016,1450,1208,3139,4031,4902,249,3362,1974,1051,2335,1426,3950,1521,4132,630,3914,3572,4821,1164,5000,3366,1695,1831,4978,2619,2726,294
|
||||||
|
3411,4050,2363,2817,3133,4781,4227,2977,1190,295,1978,2882,2539,3503,3089,845,4597,2100,2082,5000,3133,4502,3802,4838,5000,4222,2568,4232,1995,2765,5000,4227,455,4110,1308,178,4099,728,5000,982,4630,5000,4844,4616,1866,1801,1409,2266,4156,5000,1850,1705,4668,1517,2942,4989,3594,2981,1412,1227,3154,4043,4913,291,3401,2007,1080,2358,1381,3899,1466,4075,615,3896,3547,4847,1188,5000,3388,1714,1850,5000,2653,2756,330
|
||||||
|
3378,4018,2329,2782,3156,4801,4243,2990,1203,334,2014,2913,2566,3464,3044,795,4543,2045,2065,5000,3166,4525,3824,4854,5000,4226,2589,4265,1964,2733,5000,4186,409,4067,1263,193,4137,764,5000,1008,4649,5000,4849,4618,1816,1780,1386,2238,4121,5000,1859,1725,4687,1535,2976,5000,3623,3008,1435,1184,3107,3993,4861,270,3377,1977,1108,2380,1397,3910,1473,4080,662,3939,3581,4808,1148,5000,3347,1672,1806,5000,2686,2786,367
|
||||||
|
3408,4047,2358,2808,3117,4759,4198,2942,1154,311,2049,2943,2593,3486,3060,807,4552,2052,2110,5000,3136,4485,3784,4807,5000,4168,2609,4297,1995,2761,5000,4206,424,4086,1280,147,4112,737,5000,551,4605,5000,4854,4619,1828,1821,1425,2272,4147,5000,1805,1683,4643,1490,2949,4989,3590,3035,1458,1202,3121,4004,4872,311,3414,2008,1073,2341,1352,3859,1417,4024,709,3980,3615,4831,1171,5000,3368,1271,1825,5000,2656,2754,405
|
||||||
|
3376,4077,2386,2834,3140,4778,4214,2956,1168,349,2022,2912,2558,3446,3015,756,4498,2060,2154,5000,3167,4505,3806,4821,5000,4172,2568,4267,1964,2728,5000,4163,439,4105,1297,162,4569,772,5000,576,4622,5000,4796,4558,1778,1800,1401,2243,4172,5000,1811,1703,4661,1508,2983,5000,3620,2999,1418,1158,3073,3954,4822,352,3450,2037,1100,2362,1367,3869,1424,4030,693,3959,3585,4789,1551,5000,3328,1291,1843,5000,2689,2783,444
|
||||||
|
3405,4044,2352,2798,3101,4735,4168,2908,1181,387,2056,2942,2584,3467,3030,767,4506,2006,2135,5000,3135,4462,3765,4835,5000,4176,2588,4298,1994,2755,5000,4182,391,4062,1251,116,4543,745,5000,599,4639,5000,4800,4560,1790,1840,1439,2275,4135,5000,1755,1661,4618,1464,3017,5000,3648,3025,1440,1176,3087,3966,4833,330,3424,2003,1065,2321,1320,3817,1431,4036,738,3999,3615,4808,1573,5000,3349,1248,1800,5000,2659,2750,484
|
||||||
|
3435,4073,2380,2823,3123,4754,4183,2922,1134,362,2026,2910,2548,3426,3046,778,4514,2014,2178,5000,3162,4478,3785,4787,5000,4118,2547,4268,1963,2721,5000,4201,405,4081,1268,132,4580,778,4839,560,4592,5000,4741,4500,1740,1880,1476,2307,4158,5000,1760,1681,4636,1483,2988,5000,3615,2988,1400,1131,3039,3977,4845,369,3458,2030,1091,2342,1335,3827,1375,3981,720,3975,3582,4763,1594,5000,2950,1268,1819,5000,2690,2778,518
|
||||||
|
3402,4040,2346,2786,3083,4772,4199,2936,1148,398,2058,2939,2573,3446,2999,726,4460,1961,2158,5000,3127,4494,3805,4800,5000,4122,2568,4299,1992,2748,5000,4157,357,4037,1222,506,4616,812,4867,582,4607,5000,4745,4503,1752,1858,1451,2276,4118,5000,1703,1638,4654,1501,3021,5000,3643,3014,1421,1148,3053,3927,4796,346,3430,1994,1055,2362,1350,3836,1382,3988,764,4013,3610,5000,1553,5000,2909,1225,1776,5000,2660,2806,553
|
||||||
|
3432,4069,2373,2811,3104,4728,4152,2888,1101,372,2027,2906,2598,3466,3013,737,4469,1971,2199,5000,3152,4446,3763,4751,5000,4064,2527,4330,2022,2774,5000,4174,370,4056,1239,522,4590,783,4833,541,4559,5000,4686,4505,1765,1898,1488,2306,4140,5000,1706,1658,4611,1459,2991,5000,3609,2977,1442,1165,3066,3939,4809,384,3462,2018,1080,2320,1302,3784,1327,3934,745,3987,3636,5000,1574,5000,2930,1245,1795,5000,2691,2771,589
|
||||||
|
3399,4036,2338,2836,3126,4746,4168,2903,1117,407,2057,2935,2561,3423,2966,685,4415,1918,2240,5000,3176,4458,3782,4764,5000,4069,2548,4299,1989,2738,4999,4129,321,4012,1255,538,4625,395,4860,562,4572,5000,4690,4447,1716,1875,1461,2273,4099,5000,1710,1677,4630,1478,3023,5000,3637,3001,1401,1120,3018,3889,4760,359,3431,2041,1105,2340,1315,3793,1334,3942,788,4022,3598,5000,1532,5000,2889,1202,1814,5000,2721,2797,626
|
||||||
|
3428,4065,2365,2798,3085,4702,4121,2855,1071,442,2086,2964,2585,3442,2979,695,4424,1929,2217,5000,3136,4407,3738,4713,4977,4073,2569,4330,2018,2763,5000,4146,334,4030,1630,493,4599,365,4825,519,4584,5000,4693,4451,1728,1914,1497,2301,4118,5000,1650,1635,4587,1436,2993,5000,3665,3026,1421,1136,3031,3902,4774,395,3462,2001,1068,2297,1267,3740,1279,3950,829,4056,4042,5000,1553,5000,2909,1222,1771,5000,2689,2761,664
|
||||||
|
3458,4093,2392,2822,3106,4720,4136,2870,1088,414,2052,2930,2547,3399,2931,643,4433,1940,2256,5000,3157,4417,3757,4725,4984,4015,2529,4299,1984,2726,4979,4162,346,4049,1647,509,4634,397,4851,538,4534,4968,4635,4394,1679,1891,1469,2329,4137,5000,1652,1654,4606,1456,3024,5000,3630,2987,1379,1090,2982,3852,4788,431,3491,2022,1092,2315,1280,3748,1286,3898,808,4026,4001,5000,1511,5000,2930,1241,1791,5000,2719,2786,703
|
||||||
|
3425,4059,2357,2784,3064,4675,4090,2885,1105,447,2079,2958,2570,3417,2944,653,4380,1890,2232,5000,3114,4363,3774,4736,4990,4020,2550,4329,2012,2751,4999,4116,297,4005,1601,464,4186,366,4876,556,4546,4974,4638,4399,1692,1930,1503,2294,4093,5000,1592,1612,4563,1476,3054,5000,3657,3011,1398,1106,2996,3865,4741,404,3457,1979,1054,2271,1230,3757,1293,3907,848,4057,4021,5000,1110,5000,2889,1199,1749,5000,2686,2749,743
|
||||||
|
3454,4088,2383,2807,3084,4692,4105,2838,1060,417,2043,2924,2532,3435,2958,663,4389,1902,2269,5000,3132,4371,3729,4685,4934,3963,2510,4297,1978,2775,5000,4131,309,4443,1618,481,4221,396,4838,512,4494,4918,4580,4344,1706,1968,1536,2320,4110,5000,1593,1632,4582,1435,3022,5000,3622,2972,1356,1060,3009,3878,4757,438,3484,1997,1077,2289,1243,3704,1239,3856,824,4446,3978,5000,1130,5000,2909,1219,1768,5000,2714,2773,777
|
||||||
|
3421,4054,2347,2769,3104,4709,4120,2854,1079,448,2068,2951,2554,3391,2908,611,4337,1853,2243,5000,3148,4376,3746,4696,4940,3968,2532,4327,2005,2737,4976,4084,260,4399,1573,499,4254,426,4862,528,4504,4923,4585,4351,1657,1943,1507,2283,4065,5000,1532,1651,4602,1456,3051,5000,3649,2995,1375,1075,2960,3829,4711,409,3448,1952,1101,2306,1255,3712,1247,3867,862,4474,3996,5000,1087,5000,2868,1176,1727,5000,2743,2797,812
|
||||||
|
3450,4082,2373,2792,3062,4664,4074,2808,1035,417,2030,2978,2576,3408,2921,621,4347,1867,2278,5000,3102,4319,3701,4644,4885,3911,2554,4356,2032,2760,4995,4099,272,4417,1590,34,4226,393,4823,482,4452,4867,4589,4359,1671,1980,1539,2308,4080,5000,1532,1609,4560,1416,3018,5000,3613,3018,1393,1090,2973,3843,4728,442,3473,1968,1061,2261,1204,3659,1193,3817,837,4502,4012,4666,1107,5000,2888,1196,1747,5000,2708,2758,848
|
||||||
|
3417,4047,2399,2814,3082,4681,4089,2824,1055,447,2053,2943,2536,3363,2871,569,4296,1881,2312,5000,3116,4323,3716,4654,4891,3917,2514,4323,1997,2720,4951,4052,642,4435,1607,52,4259,421,4845,498,4461,4872,4532,4306,1623,1955,1509,2269,4095,5000,1533,1629,4580,1438,3046,5000,3639,2979,1350,1044,2925,3795,4684,411,3496,1983,1083,2278,1216,3667,1202,3829,1293,4466,3964,4608,1064,5000,2847,1216,1767,5000,2736,2780,885
|
||||||
|
3445,4075,2362,2775,3039,4636,4043,2779,1075,476,2075,2969,2558,3379,2883,579,4306,1835,2283,5000,3066,4264,3670,4601,4897,3923,2536,4352,2023,2743,4969,4066,655,4391,1562,8,4230,387,4805,513,4470,4877,4538,4317,1638,1992,1540,2292,4047,5000,1471,1587,4538,1399,3011,5000,3665,3001,1368,1059,2938,3809,4702,442,3457,1935,1043,2232,1165,3614,1211,3843,1328,4491,3978,4610,1083,5000,2867,1174,1726,5000,2701,2740,923
|
||||||
|
3474,4102,2387,2797,3058,4653,4059,2796,1034,442,2034,2933,2517,3333,2834,589,4318,1851,2315,5000,3078,4266,3685,4611,4841,3867,2497,4319,1987,2702,4987,4080,667,4409,1159,26,4262,415,4826,465,4417,4821,4482,4266,1590,1966,1570,2314,4060,5000,1471,1607,4559,1422,3038,5000,3628,2961,1324,1011,2890,3824,4721,472,3479,1948,1065,2248,1176,3622,1159,3795,1300,4452,3508,4549,1040,5000,2888,1194,1747,5000,2727,2762,962
|
||||||
|
3441,4068,2350,2757,3015,4607,4075,2813,1055,470,2054,2959,2538,3349,2845,537,4268,1806,2284,5000,3026,4267,3700,4620,4847,3874,2519,4347,2012,2724,4942,4452,618,4364,1114,0,4232,442,4846,478,4425,4826,4489,4279,1605,2001,1537,2274,4010,5000,1409,1565,4580,1446,3064,5000,3653,2982,1342,1026,2904,3778,4678,438,3437,1898,1024,2201,1187,3631,1168,4230,1333,4475,3518,4550,1059,5000,2847,1152,1706,5000,2691,2783,1002
|
||||||
|
3469,4095,2375,2779,3034,4624,4029,2769,1015,434,2011,2922,2558,3364,2857,547,4280,1824,2313,5000,3036,4206,3652,4567,4791,3820,2480,4314,2037,2745,4959,4466,631,4382,1131,2,4263,406,4804,430,4371,4770,4435,4293,1621,2036,1566,2294,4022,5000,1409,1585,4539,1408,3028,5000,3616,2941,1297,1041,2918,3793,4699,466,3457,1909,1045,2217,1136,3578,1117,4184,1302,4434,3528,4549,1078,5000,2867,1172,1727,5000,2716,2741,1036
|
||||||
|
3435,4060,2338,2801,3053,4641,4045,2788,1038,460,2030,2947,2517,3318,2807,496,4231,1780,2280,5000,3044,4206,3666,4576,4797,3827,2504,4341,2000,2704,4914,4418,582,3918,1149,21,4294,432,4823,442,4378,4776,4443,4246,1574,2009,1532,2252,3971,4948,1409,1605,4561,1432,3053,5000,3641,2962,1315,994,2870,3748,4658,431,3413,1919,1065,2232,1147,3587,1128,4201,1333,4034,3474,4486,1034,5000,2826,1130,1687,5000,2741,2761,1071
|
||||||
|
3464,4087,2362,2760,3009,4595,4000,2745,1000,422,2047,2972,2536,3333,2818,507,4244,1800,2308,5000,2989,4143,3618,4522,4742,3836,2527,4369,2024,2724,5000,4432,596,3936,1104,0,4263,395,4780,392,4324,4782,4452,4263,1590,2043,1560,2271,3981,4951,1347,1564,4521,1395,3015,5000,3666,2983,1332,1008,2884,3764,4680,457,3431,1866,1023,2184,1095,3534,1497,4156,1362,4052,3481,4484,1053,5000,2846,1151,1709,5000,2704,2719,1107
|
||||||
|
3430,4113,2386,2781,3028,4612,4017,2764,1024,446,2002,2934,2494,3285,2768,456,4258,1821,2334,5000,2995,4142,3631,4531,4748,3783,2488,4334,1986,2682,5000,4384,610,3954,1122,0,4293,419,4798,404,4331,4726,4400,4219,1544,2015,1525,2289,3990,4954,1347,1585,4543,1421,3039,5000,3628,2942,1287,961,2837,3720,4641,482,3447,1875,1043,2198,1106,3543,1509,4175,1329,4007,3426,4420,1010,5000,2867,1171,1731,5000,2728,2738,1144
|
||||||
|
3458,4078,2348,2740,2984,4567,3972,2784,1049,469,2018,2958,2513,3300,2780,467,4211,1780,2297,5000,2938,4078,3582,4539,4755,3792,2512,4361,2009,2701,5000,4398,142,3910,1078,0,4260,381,4815,415,4338,4733,4411,4238,1561,2048,1551,2245,3937,4895,1286,1544,4504,1385,3062,5000,3652,2962,1304,976,2852,3738,4665,444,3401,1820,1000,2150,1054,3553,1522,4194,936,4023,3431,4417,1029,5000,2825,1130,1692,5000,2690,2695,1182
|
||||||
|
3486,4104,2372,2761,3002,4584,3989,2742,1013,430,1971,2920,2470,3252,2791,479,4226,1802,2322,5000,2943,4077,3595,4485,4700,3740,2474,4326,1971,3140,5000,4412,157,3927,1096,0,4290,404,4770,364,4284,4679,4361,4196,1515,2080,1576,2262,3946,4898,1287,1565,4527,1412,3022,5000,3614,2920,1258,929,2867,3756,4689,467,3416,1828,1019,2164,1064,3921,1473,4152,900,3976,3373,4352,1048,5000,2846,1151,1714,5000,2713,2713,1221
|
||||||
|
3451,4068,2333,2720,2958,4601,4007,2763,1040,451,1985,2943,2488,3266,2741,429,4180,1764,2283,5000,2947,4075,3608,4493,4707,3751,2499,4352,1993,3097,5000,4364,110,3883,1053,0,4318,427,4786,374,4291,4686,4374,4219,1533,2051,1539,2216,3892,4838,1226,1586,4551,1439,3044,5000,3637,2939,1275,943,2820,3713,4652,427,3367,1772,976,2178,1075,3931,1487,4173,925,3990,3376,4348,1005,5000,2805,1110,1675,5000,2735,2731,1261
|
||||||
|
3479,4094,2357,2740,2976,4556,3963,2723,1005,410,1936,2966,2506,3280,2752,441,4197,1788,2305,5000,2888,4011,3558,4439,4653,3700,2461,4378,2016,3116,5000,3958,126,3901,1071,0,4284,387,4739,322,4236,4633,4388,4242,1550,2082,1563,2231,3899,4840,1228,1546,4513,1404,3004,5000,3598,2897,1291,958,2836,3733,4678,448,3380,1778,994,2129,1023,3879,1439,3714,887,4003,3379,4344,1025,5000,2825,1131,1699,5000,2696,2686,1295
|
||||||
|
3445,4058,2380,2760,2994,4573,3982,2745,1033,429,1949,2927,2462,3232,2702,392,4152,1751,2326,5000,2890,4009,3570,4447,4660,3712,2486,4342,2396,3072,5000,3911,80,3919,1090,0,4312,409,4754,332,4243,4642,4341,4205,1506,2051,1525,2184,3844,4842,1230,1568,4537,1433,3025,5000,3621,2916,1246,911,2790,3691,4643,407,3393,1784,1012,2142,1453,3890,1454,3737,909,3952,3319,4278,982,4991,2784,1091,1722,5000,2718,2703,1330
|
||||||
|
3472,4083,2341,2718,2950,4528,3938,2706,999,448,1962,2950,2480,3246,2714,405,4171,1777,2284,5000,2830,3945,3519,4393,4669,3725,2511,4367,2418,3090,5000,3925,97,3876,1047,0,4277,368,4707,280,4251,4651,4357,4231,1525,2082,1548,2198,3851,4783,1171,1528,4500,1399,2983,5000,3644,2935,1262,926,2807,3712,4671,426,3342,1727,968,2093,1402,3839,1408,3761,931,3963,3320,4274,1002,5000,2805,1112,1684,5000,2677,2658,1366
|
||||||
|
3499,4109,2364,2738,2968,4545,3958,2729,1029,405,1911,2910,2435,3197,2664,419,4189,1804,2303,5000,2831,3943,3531,4401,4615,3676,2474,4330,2377,3046,4807,3940,115,3894,1066,0,4304,389,4721,289,4197,4600,4313,4197,1482,2050,1570,2212,3857,4785,1174,1551,4525,1428,3003,5000,3604,2892,1216,880,2762,3671,4699,445,3353,1732,985,2106,1412,3851,1005,3724,890,3910,3258,4209,960,5000,2826,1134,1708,5000,2698,2674,1403
|
||||||
|
3465,4072,2324,2696,2924,4501,3977,2753,1059,422,1923,2932,2452,3211,2676,371,4147,1770,2259,4981,2770,3880,3542,4410,4624,3690,2499,4775,2398,3064,4823,3894,71,3850,1024,0,4268,409,4735,298,4205,4611,4331,4227,1501,2079,1529,2163,3801,4725,1116,1511,4488,1458,3022,5000,3627,2911,1232,895,2779,3694,4666,401,3301,1675,940,2477,1422,3863,1022,3750,909,3919,3258,4205,980,4994,2785,1094,1671,5000,2657,2628,1441
|
||||||
|
3492,4097,2347,2716,2942,4519,3935,2716,1028,377,1871,2891,2407,3224,2688,385,4167,1799,2275,4988,2771,3879,3491,4356,4571,3643,2463,4737,2419,3081,4838,3909,90,3868,1044,0,4294,366,4686,246,4151,4561,4289,4195,1520,2108,1550,2175,3806,4727,1121,1534,4514,1426,2979,5000,3587,2868,1186,911,2797,3717,4696,418,3310,1680,957,2489,1371,3813,978,3715,865,3865,3196,4201,1000,5000,2806,1116,1696,5000,2677,2644,1480
|
||||||
|
3457,4060,2307,2674,2960,4537,3956,2741,1059,393,1882,2913,2424,3175,2638,338,4126,1767,2229,4995,2771,3879,3502,4364,4581,3659,2489,4761,2378,2616,4792,3863,47,3825,1001,0,4319,385,4699,255,4160,4573,4310,4227,1478,2074,1509,2125,3749,4668,1127,1558,4540,1456,2998,5000,3608,2886,1202,865,2754,3678,4665,372,3257,1623,974,2501,1382,3406,997,3743,883,3871,3195,4136,959,4975,2765,1077,1659,5000,2697,2659,1520
|
||||||
|
3484,4085,2329,2693,2916,4493,3915,2705,1029,347,1892,2934,2440,3188,2651,354,4148,1798,2244,4938,2710,3817,3450,4310,4529,3614,2935,4785,2398,2633,4807,3880,68,3843,1022,0,4282,341,4650,202,4107,4587,4332,4261,1499,2102,1528,2136,3754,4671,1071,1519,4505,1425,2954,5000,3568,2904,1218,881,2772,3702,4697,387,3266,1627,1348,2452,1331,3357,954,3710,899,3877,3194,4134,980,4996,2787,1099,1684,5000,2654,2613,1554
|
||||||
|
3448,4109,2351,2713,2934,4512,3936,2731,1062,362,1839,2892,2394,3139,2602,308,4109,1830,2258,4944,2710,3818,3461,4318,4539,3631,2900,4747,2356,2588,4761,3835,89,3862,1042,0,4306,359,4662,211,4116,4539,4293,4234,1458,2067,1485,2085,3759,4674,1078,1543,4532,1456,2972,5000,3589,2860,1172,835,2730,3665,4667,402,3274,1632,1364,2464,1342,3371,975,3740,852,3821,3130,4069,939,4956,2746,1122,1710,5000,2673,2628,1589
|
||||||
|
3475,4072,2311,2670,2890,4469,3896,2697,1095,376,1849,2913,2410,3152,2615,325,4133,1800,2209,4886,2648,3758,3409,4327,4550,3649,2926,4770,1956,2605,4776,3852,48,3819,1001,0,4268,315,4612,220,4126,4555,4318,4270,1479,2094,1503,2095,3701,4615,1024,1505,4498,1426,2989,5000,3611,2878,1189,851,2749,3691,4700,354,3219,1575,1318,2414,871,3323,996,3771,867,3825,3129,4068,960,4977,2767,1083,1674,5000,2630,2581,1625
|
||||||
|
3501,4096,2332,2689,2908,4488,3919,2724,1067,328,1796,2871,2364,3104,2628,342,4157,1834,2222,4891,2649,3761,3420,4274,4500,4026,2891,4731,1913,2560,4792,3870,71,3838,1022,0,4291,332,4624,167,4075,4509,4282,4246,1439,2120,1521,2104,3706,4619,1033,1530,4526,1458,2944,5000,3570,2834,1143,806,2708,3717,4734,367,3227,2000,1333,2426,882,3338,956,3741,818,3767,3065,4005,982,4999,2789,1106,1700,5000,2649,2596,1662
|
||||||
|
3466,4058,2292,2646,2865,4507,3941,2752,1102,341,1805,2891,2380,3117,2580,298,4120,1806,2171,4832,2587,3764,3430,4283,4512,4046,2918,4754,1932,2577,4747,3826,32,3795,981,0,4314,348,4636,176,4086,4526,4309,4284,1461,2083,1476,2052,3648,4561,981,1493,4554,1491,2961,5000,3590,2852,1159,823,2729,3682,4706,317,3172,1943,1287,2438,894,3353,979,3774,830,3770,3064,4005,942,4958,2749,1068,1665,5000,2605,2610,1700
|
||||||
|
3492,4082,2313,2666,2883,4465,3903,2719,1075,292,1752,2849,2395,3130,2594,316,4146,1842,2182,4836,2588,3707,3378,4230,4463,4005,2884,4356,1951,2594,4763,3845,56,3814,1003,0,4274,303,4585,123,4035,4483,4276,4324,1484,2108,1493,2061,3652,4566,991,1518,4521,1461,2915,5000,3549,2808,1175,841,2750,3710,4741,329,3178,1949,1302,1968,844,3307,940,3745,780,3711,3062,4006,965,4980,2771,1091,1692,5000,2623,2563,1739
|
||||||
|
3456,4044,2272,2685,2901,4485,3927,2749,1111,305,1761,2868,2349,3081,2546,273,4111,1816,2191,4839,2589,3712,3388,4239,4896,4026,2911,4316,1908,2548,4718,3802,19,3771,1025,0,4296,318,4596,133,4048,4502,4306,4303,1445,2071,1447,2007,3595,4571,1003,1543,4550,1494,2931,5000,3570,2825,1130,796,2710,3676,4715,279,3543,1955,1317,1980,856,3324,965,3780,791,3713,2999,3946,926,4939,2731,1053,1719,5000,2640,2577,1779
|
||||||
|
3482,4068,2293,2642,2858,4443,3889,2717,1085,317,1770,2887,2364,3094,2561,293,4138,1853,2139,4779,2529,3657,3336,4186,4848,4049,2939,4338,1927,2565,4735,3822,45,3791,985,0,4255,272,4545,81,4061,4523,4337,4346,1468,2095,1462,2015,3599,4515,954,1507,4517,1466,2886,5000,3590,2843,1146,815,2732,3705,4751,290,3550,1900,1270,1930,807,3279,928,3815,800,3715,2997,3948,949,4961,2753,1077,1685,5000,2596,2530,1813
|
||||||
|
3508,4091,2314,2661,2877,4464,3914,2748,1121,266,1717,2844,2317,3046,2514,251,4167,1890,2147,4782,2531,3665,3346,4196,4862,4010,2485,4298,1884,2520,4690,3843,72,3811,1007,0,4276,287,4556,91,4012,4482,4307,4327,1430,2056,1415,2023,3603,4521,968,1533,4547,1500,2901,5000,3548,2798,1101,771,2693,3673,4788,300,3556,1907,864,1942,820,3296,954,3790,747,3654,2935,3890,911,4921,2775,1102,1713,5000,2613,2544,1848
|
||||||
|
3471,4053,2273,2618,2834,4423,3877,2779,1158,278,1727,2863,2332,3059,2529,272,4134,1867,2093,4722,2471,3612,3356,4626,4877,4034,2514,4320,1902,2537,4707,3802,37,3769,968,0,4235,239,4567,101,4027,4505,4340,4371,1455,2079,1430,1969,3546,4466,922,1498,4515,1533,2917,5000,3568,2816,1117,790,2717,3704,4764,668,3501,1853,817,1892,771,3315,981,3827,756,3655,2934,3895,935,4943,2735,1064,1679,5000,2568,2496,1884
|
||||||
|
3497,4076,2294,2637,2853,4444,3903,2748,1134,227,1674,2819,2285,3073,2545,293,4164,1906,2100,4725,2474,3623,3303,4575,4831,3997,2480,4279,1859,2553,4725,3824,65,3789,991,0,4255,254,4515,50,3980,4466,4313,4355,1480,2101,1444,1976,3550,4473,938,1524,4546,1506,2871,5000,3526,2771,1072,747,2741,3735,4802,678,3507,1861,831,1904,784,3272,947,3803,701,3594,2872,3902,959,4964,2758,1089,1707,5000,2584,2511,1921
|
||||||
|
3461,4037,2253,2594,2872,4466,3929,2781,1172,238,1684,2838,2300,3025,2499,254,4133,1884,2045,4665,2478,3635,3313,4586,4847,3603,2509,4300,1877,2509,4682,3785,32,3747,953,0,4274,268,4526,61,3996,4491,4349,4401,1443,2060,1395,1921,3493,4420,893,1552,4577,1540,2886,5000,3545,2788,1089,767,2704,3704,4779,625,3452,1389,845,1916,798,3291,976,3841,708,3595,2873,3847,922,4924,2719,1052,1674,5000,2601,2525,1959
|
||||||
|
3486,4060,2273,2613,2830,4426,3894,2752,1148,187,1632,2856,2314,3039,2516,276,4164,1924,2051,4667,2421,3586,3681,4535,4802,3568,2538,4321,1895,2526,4700,3808,62,3767,977,0,4231,220,4475,10,3950,4455,4386,4448,1469,2081,1408,1928,3498,4429,912,1517,4546,1513,2840,5000,3503,2806,1107,786,2729,3737,5000,634,3459,1399,797,1866,751,3250,943,3819,652,3595,2874,3856,947,4946,2742,1078,1703,5000,2555,2478,1998
|
||||||
|
3449,4021,2294,2633,2849,4449,3922,2785,1187,198,1642,2812,2267,2991,2471,238,4135,1965,2057,4670,2426,3601,3691,4546,4819,3596,2505,4279,1851,2481,4658,3770,30,3788,1001,0,4250,233,4485,22,3968,4482,4362,4433,1433,2040,1358,1872,3503,4438,932,1545,4577,1547,2856,5000,3522,2761,1062,745,2693,3708,5000,581,3466,1410,811,1879,765,3271,973,3859,658,3534,2814,3804,911,4906,2703,1103,1733,5000,2571,2493,2038
|
||||||
|
3474,4043,2252,2590,2807,4410,3888,2757,1225,209,1653,2829,2282,3005,2489,262,4168,1945,2001,4611,2371,3555,3639,4496,4418,3624,2534,4300,1869,2499,4677,3794,61,3747,963,0,4207,184,4434,34,3986,4510,4401,4482,1460,2060,1370,1879,3447,4387,891,1511,4547,1520,2809,5000,3541,2779,1080,766,2720,3741,5000,590,2992,1360,763,1829,718,3230,1004,3900,663,3534,2816,3816,937,4928,2726,1067,1700,5000,2525,2445,2072
|
||||||
|
3499,4066,2273,2609,2828,4433,3916,2791,1202,157,1603,2785,2234,2958,2445,287,4202,1987,2006,4614,2378,3993,3650,4508,4375,3592,2502,4258,1825,2454,4697,3819,93,3768,988,0,4225,198,4445,0,3944,4477,4379,4468,1425,2018,1382,1885,3453,4398,914,1539,4579,1555,2825,5000,3499,2734,1036,725,2685,4195,5000,598,3000,1373,777,1842,734,3253,974,3880,606,3473,2758,3767,902,4950,2749,1093,1730,5000,2541,2460,2107
|
||||||
|
3462,4026,2231,2567,2786,4395,3945,2826,1242,168,1615,2802,2249,2972,2463,251,4174,1968,1949,4555,2325,4013,3660,4521,4395,3622,2532,4278,1843,2472,4656,3783,64,3727,951,0,4180,210,4456,0,3964,4508,4420,4518,1453,2036,1331,1830,3397,4349,875,1506,4611,1590,2841,5000,3518,2751,1054,747,2712,4168,5000,545,2946,1324,729,1793,750,3276,1007,3922,610,3474,2762,3782,929,4911,2711,1057,1698,5000,2495,2475,2143
|
||||||
|
3487,4049,2251,2586,2807,4419,3913,2799,1219,117,1565,2757,2263,2987,2482,277,4210,2011,1953,4559,2334,3972,3608,4052,4353,3591,2500,4236,1861,2490,4677,3810,97,3749,976,0,4198,161,4405,0,3923,4477,4400,4568,1481,2055,1342,1836,3404,4362,900,1535,4582,1563,2795,5000,3475,2707,1010,769,2740,4203,5000,133,2955,1339,742,1806,704,3238,978,3903,552,3413,2767,3799,956,4933,2734,1084,1729,5000,2511,2429,2180
|
||||||
|
3449,4009,2210,2606,2828,4444,3942,2834,1259,128,1579,2774,2216,2941,2440,242,4184,1993,1896,4563,2765,3995,3619,4065,4374,3624,2530,4256,1817,2447,4637,3775,69,3708,1002,0,4215,174,4417,0,3945,4509,4443,4556,1448,2011,1291,1780,3349,4314,926,1564,4614,1598,2811,5000,3494,2724,1029,729,3127,4177,5000,79,2903,1355,756,1819,721,3262,1012,3946,556,3414,2712,3755,922,4893,2696,1049,1697,5000,2527,2444,2218
|
||||||
|
3474,4031,2230,2563,2787,4407,3911,2808,1236,77,1592,2791,2231,2956,2460,270,4220,2037,1900,4506,2714,3957,3567,4017,4334,3657,2560,4275,1835,2465,4659,3803,104,3730,966,0,4169,124,4366,0,3906,4543,4486,4606,1477,2028,1301,1786,3357,4329,892,1532,4585,1571,2765,4995,3512,2742,1048,752,3157,4213,5000,88,2913,1310,707,1771,677,3225,985,3928,559,3416,2719,3774,950,4916,2720,1076,1728,5000,2481,2398,2257
|
||||||
|
3436,4053,2250,2583,2808,4432,3942,2844,1276,88,1545,2746,2183,2910,2419,237,4257,2082,1905,4512,2727,3983,3158,4032,4357,3628,2528,4233,1791,2423,4619,3769,139,3752,992,0,4186,137,4378,0,3930,4515,4469,4595,1445,1984,1249,1793,3365,4345,920,1561,4618,1606,2782,5000,3469,2698,1005,714,3125,4188,4896,96,2924,1329,721,1784,695,3251,1021,3973,500,3356,2665,3734,917,4876,2744,1103,1759,5000,2497,2415,2297
|
||||||
|
3460,4013,2208,2541,2768,4396,3911,2881,1315,100,1560,2763,2198,2926,2440,266,4233,2064,1847,4876,2680,3949,3107,4047,4380,3663,2559,4253,1810,2442,4643,3798,113,3713,957,0,4140,87,4390,0,3955,4551,4514,4645,1475,2000,1259,1737,3312,4300,888,1530,4589,1579,2799,5000,3488,2716,1025,1158,3155,4225,4938,43,2873,1286,673,1736,651,3278,1057,4018,503,3359,2675,3756,946,4899,2707,1069,1729,4999,2450,2369,2331
|
||||||
|
3484,4035,2228,2561,2790,4422,3943,2856,1293,50,1514,2717,2151,2881,2462,296,4272,2109,1851,4882,2695,3978,3118,4000,4342,3636,2528,4210,1766,2462,4666,3828,149,3735,984,3,4155,99,4341,0,3919,4525,4498,4634,1443,2016,1268,1744,3321,4319,919,1560,4623,1614,2754,4996,3444,2671,983,1121,3186,4262,4979,52,2885,1307,686,1751,670,3243,1032,4001,443,3300,2624,3719,975,4922,2731,1097,1760,5000,2466,2386,2366
|
||||||
|
3446,3995,2186,2519,2751,4449,3975,2893,1333,62,1531,2734,2166,2897,2422,264,4249,2093,1794,4828,2712,3589,3130,4016,4367,3673,2558,4229,1784,2420,4629,3797,124,3696,950,0,4171,112,4354,0,3946,4562,4544,4685,1474,1970,1215,1688,3269,4276,890,1590,4656,1648,2772,5000,3463,2689,1003,1146,3156,3818,4959,0,2836,1267,638,1765,690,3271,1070,4047,446,3304,2636,3745,943,4883,2694,1063,1730,4972,2482,2403,2402
|
||||||
|
3470,4016,2207,2540,2773,4414,3946,2868,1310,13,1487,2750,2181,2915,2445,295,4288,2138,2219,4836,2668,3560,3080,3970,4331,3648,2527,4249,1803,2440,4654,3828,162,3719,977,7,4124,62,4305,0,3912,4539,4591,4735,1505,1985,1224,1695,3280,4296,923,1559,4628,1621,2728,4976,3420,2646,1443,1171,3188,3856,5000,8,2850,1290,651,1718,648,3238,1046,4031,387,3308,2649,3773,973,4906,2719,1091,1762,4990,2436,2359,2439
|
||||||
|
3432,3976,2227,2560,2796,4442,3979,2906,1349,27,1506,2704,2134,2870,2406,265,4267,2122,2224,4845,2688,3594,3091,3988,4357,3685,2559,4206,1760,2399,4617,3798,137,3743,1005,40,4139,74,4319,0,3940,4578,4576,4723,1475,1938,1171,1640,3229,4317,958,1590,4661,1655,2746,4997,3438,2664,1402,1135,3158,3833,4981,0,2865,1314,665,1733,668,3267,1085,4077,390,3251,2602,3741,941,4867,2682,1057,1794,5000,2452,2377,2477
|
||||||
|
3456,3998,2185,2519,2758,4408,3950,2881,1327,40,1525,2720,2149,2888,2430,297,4307,2167,2167,4793,2227,3567,3042,3943,4385,3724,2590,4225,1779,2420,4643,3830,176,3704,972,12,4092,25,4271,0,3970,4618,4624,4773,1507,1952,1179,1647,3241,4278,932,1559,4633,1627,2703,5000,3457,2682,1423,1161,2771,3872,5000,0,2819,1278,617,1686,628,3235,1063,4124,392,3257,2618,3772,972,4890,2708,1086,1765,4963,2407,2333,2516
|
||||||
|
3479,4019,2205,2540,2782,4436,3984,2919,1366,0,1484,2675,2103,2844,2393,330,4348,2633,2172,4804,2250,3605,3054,3961,4351,3701,2559,4182,1736,2380,4608,3863,214,3728,1000,46,4107,37,4285,0,3939,4597,4610,4760,1478,1905,1188,1655,3254,4301,969,1591,4667,1661,2722,4976,3413,3059,1383,1126,2743,3849,5000,0,2835,1305,630,1702,649,3266,1103,4109,333,3202,2573,3743,941,4913,2733,1115,1797,4980,2423,2352,2556
|
||||||
|
3441,3978,2164,2499,2744,4403,4018,2957,1404,7,1505,2691,2118,2863,2418,302,4328,2617,2116,4754,2212,3581,3067,3980,4379,3741,2590,4201,1755,2402,4635,3834,191,3691,967,18,4059,50,4300,0,3970,4638,4658,4809,1511,1918,1134,1601,3205,4263,945,1560,4638,1694,2742,4997,3432,3078,1405,1154,2777,3889,5000,0,2791,1271,582,1656,672,3297,1144,4156,337,3209,2592,3778,972,4875,2697,1082,1768,4935,2378,2309,2590
|
||||||
|
3464,4000,2184,2520,2768,4432,3990,2933,1381,0,1466,2645,2072,2882,2444,336,4370,2662,2123,4348,2237,3621,3018,3937,4347,3720,2560,4159,1774,2424,4663,3868,230,3715,997,53,4073,0,4254,0,3941,4618,4645,4796,1544,1932,1142,1609,3219,4289,984,1592,4672,1666,2700,4955,3388,3035,1365,762,2811,3928,5000,0,2809,1300,596,1673,633,3267,1123,4141,278,3156,2550,3814,1004,4898,2723,1112,1801,4952,2394,2329,2625
|
||||||
|
3426,3959,2143,2480,2793,4462,4025,2971,1419,0,1489,2661,2088,2840,2408,309,4770,2646,2068,4362,2264,3663,3031,3957,4377,3761,2591,4178,1732,2385,4629,3841,207,3678,964,88,4088,13,4270,0,3974,4661,4693,4843,1516,1883,1089,1555,3173,4254,1025,1624,4706,1699,2721,4975,3827,3054,1388,728,2784,3906,5000,0,2767,1269,610,1689,656,3300,1165,4188,282,3165,2572,3790,974,4860,2687,1079,1772,4969,2411,2349,2661
|
||||||
|
3449,3980,2163,2501,2756,4430,3998,2948,1394,0,1514,2677,2104,2859,2435,345,4812,2691,2075,4315,2230,3644,2983,3916,4346,3741,2623,4197,1752,2408,4658,3876,247,3703,994,61,4040,0,4224,0,3946,4705,4742,4890,1550,1895,1097,1564,3188,4281,1004,1594,4677,1669,2681,4933,3784,3074,1411,757,2819,3946,5000,0,2787,1301,563,1644,618,3271,1145,4174,286,3175,2595,3829,1006,4884,2713,1109,1805,4925,2367,2307,2698
|
||||||
|
3410,4002,2184,2523,2781,4460,4033,2986,1431,0,1477,2631,2058,2818,2401,319,4793,2737,1663,4331,2260,3688,2997,3937,4377,3783,2592,4155,1710,2370,4625,3849,286,3728,1025,96,4054,0,4241,0,3981,4687,4729,4875,1523,1846,1043,1512,3205,4310,1047,1626,4711,1702,2702,4954,3802,3031,952,724,2793,3925,5000,0,2809,1334,577,1662,643,3305,1188,4221,228,3125,2558,3808,976,4846,2678,1139,1838,4942,2384,2329,2736
|
||||||
|
3433,3961,2142,2483,2745,4429,4007,2962,1468,0,1503,2647,2075,2839,2429,775,4836,2720,1610,4287,2229,3671,2949,3959,4410,3826,2624,4174,1731,2394,4655,3885,264,3691,993,69,4006,0,4197,0,4017,4731,4777,4920,1558,1858,1051,1522,3161,4278,1028,1596,4682,1672,2725,5000,3821,3051,976,754,2829,3965,5000,0,2770,1306,530,1618,606,3278,1231,4268,233,3137,2584,3850,1009,4870,2705,1107,1809,4897,2340,2288,2775
|
||||||
|
3456,3982,2163,2506,2771,4460,4043,3000,1443,0,1469,2601,2030,2798,2458,812,4880,2764,1619,4306,2261,3718,2964,3919,4381,3808,2594,4132,1690,2357,4685,3921,304,3717,1024,105,4019,0,4215,0,3992,4714,4763,4902,1531,1870,1059,1532,3179,4309,1072,1628,4715,1703,2686,5000,3778,3009,938,722,2803,4005,5000,0,2793,1341,544,1636,632,3313,1212,4253,177,3089,2550,3831,1041,4894,2732,1137,1842,4914,2358,2310,2815
|
||||||
|
3417,3941,2122,2466,2736,4492,4079,3038,1479,0,1497,2617,2047,2820,2425,788,4861,2327,1567,4264,2233,3765,2979,3942,4415,3852,2625,4151,1711,2382,4654,3896,281,3681,993,79,4033,0,4234,0,4029,4760,4811,4946,1567,1819,1006,1481,3136,4279,1055,1599,4749,1734,2710,5000,3797,2610,963,753,2840,3983,5000,0,2756,1316,497,1655,658,3349,1256,4300,183,3104,2579,3876,1012,4857,2697,1106,1814,4869,2314,2333,2849
|
||||||
|
3440,3963,2143,2489,2762,4461,4053,3014,1452,0,1465,2571,2064,2842,2875,826,4905,2371,1578,4285,2269,3752,2933,3903,4387,3835,2595,4171,1732,2408,4686,3933,321,3707,1024,115,3985,0,4192,0,4005,4744,4797,4988,1603,1831,1014,1492,3157,4312,1101,1631,4719,1703,3092,5000,3754,2568,988,784,2877,4024,5000,0,2782,1353,512,1612,624,3324,1238,4285,128,3058,2610,3922,1045,4881,2724,1137,1847,4886,2333,2294,2884
|
||||||
|
3400,3922,2102,2512,2790,4494,4089,3051,1487,0,1495,2587,2019,2802,2843,802,4886,2353,1590,4307,2305,3801,2949,3928,4422,3880,2627,4129,1692,2372,4656,3908,299,3672,1056,151,3999,0,4212,0,4044,4790,4845,4967,1577,1780,960,1442,3116,4346,1148,1664,4752,1734,3117,5000,3773,2589,951,754,2852,4002,5000,0,2747,1392,527,1632,651,3361,1283,4332,135,3075,2580,3908,1016,4844,2689,1106,1881,4904,2352,2318,2920
|
||||||
|
3423,3943,2123,2474,2755,4464,4063,3027,1459,0,1526,2603,2037,2825,2874,841,4510,2396,1540,4269,2281,3790,2903,3891,4396,3925,2658,4149,1714,2399,4688,3945,338,3699,1026,125,3951,0,4171,0,4084,4836,4892,5000,1614,1791,969,1454,3138,4320,1133,1634,4723,1701,3080,5000,3372,2610,977,786,2890,4042,5000,0,2775,1370,481,1590,617,3336,1265,4378,143,3093,2613,3956,1049,4868,2717,1137,1853,4859,2309,2281,2973
|
||||||
|
3446,3964,2145,2497,2783,4497,4100,3064,1493,0,1496,2557,1993,3207,2844,818,4554,2438,1554,4294,2320,3841,2920,3916,4432,3909,2628,4107,1675,2364,4659,3983,377,3726,1058,161,3965,0,4192,0,4062,4820,4876,4984,1589,1739,916,1467,3161,4356,1181,1667,4755,2151,3106,5000,3329,2570,941,756,2866,4020,5000,0,2805,1410,496,1611,646,3374,1310,4362,90,3051,2586,3944,1020,4831,2745,1168,1886,4877,2329,2306,3027
|
||||||
|
3406,3923,2104,2459,2749,4468,4074,3101,1526,0,1530,2574,2012,3231,2876,858,4536,2419,1506,4258,2299,3830,2937,3942,4469,3956,2660,4127,1697,2391,4692,3959,354,3691,1028,136,3917,0,4214,0,4103,4867,4922,5000,1626,1750,925,1419,3122,4331,1168,1637,4725,2180,3133,5000,3348,2591,968,789,2904,4060,5000,0,2773,1389,450,1570,614,3413,1355,4408,99,3072,2622,3994,1053,4856,2711,1137,1858,4832,2287,2269,3082
|
||||||
|
3429,3945,2126,2483,2778,4501,4111,3075,1496,0,1502,2528,1968,3256,2908,478,4580,2460,1521,4286,2340,3883,2893,3907,4445,3940,2629,4086,1659,2419,4726,3996,393,3719,1061,172,3931,0,4175,0,4082,4852,4905,4996,1663,1760,934,1433,3147,4369,1218,1669,4757,2146,3098,4884,3306,2551,933,760,2942,4100,5000,0,2804,1432,467,1591,644,3390,1338,4391,48,3032,2598,4046,1086,4882,2739,1169,1892,4850,2307,2295,3138
|
||||||
|
3389,3904,2085,2446,2806,4535,4148,3112,1528,0,1536,2545,2407,3219,2879,456,4562,2439,1476,4253,2383,3936,2911,3934,4483,3987,2661,4106,1682,2386,4698,3973,369,3685,1032,209,3945,0,4198,0,4124,4898,4950,5000,1639,1709,882,1386,3111,4346,1206,1702,5000,2174,3126,4903,3325,2574,960,793,2918,4077,5000,0,2775,1413,483,1613,674,3429,1384,4436,59,3055,2637,4036,1058,4845,2706,1138,1863,4806,2328,2322,3188
|
||||||
|
3411,3925,2107,2470,2774,4507,4123,3086,1497,0,1510,2561,2427,3244,2913,497,4605,2480,1493,4283,2364,3927,2868,3900,4460,3972,2692,4127,1706,2415,4732,4011,407,3713,1065,184,3897,0,4161,0,4104,4883,4993,5000,1677,1719,891,1401,3138,4386,1256,1672,5000,2140,3092,4861,3283,2596,988,827,2957,4117,5000,0,2808,1456,438,1573,643,3407,1367,4418,9,3080,2677,4089,1091,4871,2734,1170,1897,4824,2287,2287,3239
|
||||||
|
3372,3884,2129,2495,2803,4541,4159,3121,1527,0,1547,2516,2384,3208,2465,476,4587,2520,1511,4314,2409,3980,2887,3929,4500,4020,2661,4086,1668,2382,4705,3987,382,3741,1098,220,3911,0,4185,0,4147,4929,4974,5000,1654,1667,839,1355,3165,4426,1307,1704,5000,2167,2701,4881,3303,2557,954,800,2933,4094,5000,0,2842,1501,455,1596,675,3448,1412,4462,22,3044,2656,4081,1062,4834,2701,1202,1931,4843,2309,2315,3291
|
||||||
|
3394,3906,2090,2459,2771,4513,4134,3095,1557,0,1584,2953,2404,3235,2500,517,4630,2497,1468,4284,2393,3972,2844,3896,4540,4067,2693,4107,1693,2412,4740,4025,419,3708,1070,195,3864,0,4149,0,4191,4976,5000,5000,1692,1677,850,1372,3132,4406,1296,2094,5000,2131,2669,4900,3323,2580,983,834,2972,4132,5000,0,2816,1484,410,1558,645,3426,1458,4505,36,3071,2699,4135,1094,4860,2730,1172,1902,4799,2269,2281,3344
|
||||||
|
3416,3927,2112,2484,2801,4547,4170,3130,1524,0,1560,2908,2362,3200,2473,559,4673,2535,1488,4318,2440,4026,2864,3926,4518,4053,2662,4067,1656,2380,4776,4063,456,3737,1104,232,3879,0,4175,0,4172,4960,4995,5000,1669,1625,860,1390,3161,4448,1347,2126,5000,2157,2699,4858,3281,2542,950,807,2949,4171,5000,0,2852,1529,427,1582,678,3467,1441,4486,0,3038,2681,4128,1065,4886,2759,1204,1936,4818,2291,2310,3398
|
||||||
|
3377,3887,2073,2448,2769,4520,4207,3164,1552,0,1599,2925,2383,2807,2508,538,4654,2511,1448,4291,2425,4080,2885,3957,4559,4101,2693,4089,1681,2411,4749,4039,430,3704,1076,207,3832,0,4202,0,4216,5000,5000,5000,1708,1635,809,1346,3130,4430,1337,2096,5000,1762,2730,4878,3301,2565,979,842,2988,4147,5000,0,2827,1513,383,1544,711,3509,1486,4528,5,3068,2726,4183,1098,4850,2726,1174,1907,4775,2252,2339,3453
|
||||||
|
3399,3908,2095,2474,2800,4555,4181,3136,1518,0,1996,2880,2404,2835,2544,580,4697,2547,1470,4327,2473,4072,2844,3926,4539,4087,2662,4049,1707,2442,4785,4077,465,3733,1110,243,3847,0,4168,0,4198,4989,5000,5000,1747,1645,821,1365,3161,4473,1809,2128,5000,1725,2699,4835,3260,2528,947,878,3026,4185,5000,0,2865,1560,401,1569,683,3489,1469,4508,0,3037,2772,4239,1130,4876,2755,1206,1940,4794,2275,2307,3509
|
||||||
|
3359,3868,2056,2500,2831,4590,4217,3170,1545,0,2036,2898,2363,2801,2518,560,4677,2521,1432,4364,2522,4126,2865,3957,4581,4135,2692,4071,1671,2411,4759,4053,438,3701,1144,280,3862,0,4196,0,4243,5000,5000,5000,1724,1593,771,1323,3131,4456,1861,2159,5000,1750,2731,4855,3281,2552,977,851,3003,4160,5000,0,2842,1607,420,1594,717,3531,1514,4548,0,3070,2757,4233,1101,4841,2723,1177,1912,4814,2299,2337,3559
|
||||||
|
3381,3889,2079,2465,2800,4563,4192,3141,1509,0,2076,2915,1965,2830,2555,602,4719,2557,1456,4340,2510,4117,2825,3928,4561,4183,2723,4093,1698,2443,4796,4090,472,3731,1116,255,3816,0,4163,0,4225,5000,5000,5000,1764,1603,783,1344,3164,4501,1851,2129,4781,1712,2701,4813,3302,2577,1007,887,3042,4197,5000,0,2882,1592,376,1558,690,3511,1497,4527,0,3103,2805,4289,1133,4867,2752,1209,1945,4771,2261,2306,3610
|
||||||
|
3341,3911,2103,2492,2832,4598,4227,3173,1534,357,2055,2871,1925,2798,2530,582,4760,2591,1482,4379,2560,4170,2848,3961,4604,4169,2692,4054,1663,2413,4770,4065,506,3761,1151,291,3832,0,4193,0,4270,5000,5000,5000,1741,1551,733,1365,3198,4967,1903,2160,4810,1735,2734,4833,3261,2540,976,861,3018,4171,5000,0,2922,1639,396,1584,726,3554,1541,4566,0,3076,2791,4283,1103,4832,2782,1241,1978,4791,2286,2338,3662
|
||||||
|
3363,3871,2064,2457,2801,4571,4201,3205,1559,390,2097,2889,1948,2827,2567,624,4739,2562,1447,4358,2548,4161,2809,3994,4647,4217,2722,4077,1690,2446,4806,4102,477,3729,1124,266,3786,0,4223,38,4315,5000,5000,5000,1781,1561,747,1325,3171,4952,1893,2129,4776,1696,2767,4853,3282,2565,1007,898,3057,4207,5000,0,2902,1624,353,1549,699,3596,1586,4604,0,3112,2841,4339,1134,4859,2750,1212,1949,4750,2249,2308,3715
|
||||||
|
3385,3893,2088,2485,2833,4606,4237,3174,1521,363,2077,2425,1909,2796,2605,666,4780,2595,1475,4399,2599,4213,2833,3966,4629,4203,2690,4039,1656,2479,4843,4139,509,3759,1158,302,3802,0,4192,15,4297,5000,5000,5000,1759,1572,760,1348,3207,4999,1944,1740,4803,1719,2739,4811,3241,2529,977,872,3095,4242,5000,0,2943,1672,373,1576,736,3577,1568,4580,0,3087,2829,4332,1165,4886,2780,1244,1982,4770,2275,2340,3769
|
||||||
|
3345,3853,2050,2450,2803,4642,4272,3205,1964,398,2119,2444,1932,2826,2581,646,4758,2565,1442,4379,2650,4265,2857,4001,4673,4250,2720,4062,1684,2450,4818,4114,478,3728,1131,277,3819,0,4224,56,4342,5000,5000,5000,1799,1520,712,1310,3601,4984,1934,1770,4831,1741,2773,4831,3263,2555,1009,909,3072,4215,5000,0,2924,1657,331,1604,772,3620,1612,4617,0,3126,2880,4388,1134,4851,2748,1215,1952,4729,2301,2373,3824
|
||||||
|
3368,3875,2074,2478,2836,4615,4245,3174,1925,372,2099,2462,1956,2858,2620,688,4797,2595,1472,4422,2640,4253,2819,3974,4655,4236,2688,4086,1712,2483,4854,4150,509,3759,1166,313,3774,0,4195,34,4325,5000,5000,5000,1839,1530,727,1335,3639,5000,1986,1739,4795,1700,2746,4789,3223,2519,1041,946,3110,4249,5000,0,2967,1704,351,1571,747,3602,1593,4591,0,3165,2932,4443,1165,4878,2779,1247,1985,4751,2266,2344,3880
|
||||||
|
3328,3835,2098,2507,2869,4650,4279,3203,1946,408,1722,2419,1918,2827,2597,668,4774,2563,1503,4466,2692,4303,2844,4010,4700,4283,2718,4048,1679,2455,4829,4123,477,3790,1201,349,3792,0,4228,76,4369,5000,5000,5000,1817,1479,680,1298,3615,5000,1617,1769,4822,1721,2780,4809,3245,2546,1011,921,3085,4221,5000,0,3010,1751,372,1599,785,3645,1636,4626,0,3144,2922,4436,1133,4844,2747,1218,2017,4772,2293,2378,3930
|
||||||
|
3350,3857,2061,2473,2839,4624,4251,3590,1906,445,1765,2438,1942,2859,2636,710,4813,2592,1473,4449,2681,4290,2808,3984,4744,4330,2747,4073,1708,2489,4866,4158,506,3760,1175,323,3748,0,4199,55,4414,5000,5000,5000,1857,1490,695,1744,3654,5000,1605,1736,4786,1680,2754,4829,3268,2573,1044,958,3123,4254,5000,0,2992,1736,332,1567,761,3626,1616,4661,0,3186,2975,4490,1164,4871,2778,1250,1987,4732,2258,2350,3981
|
||||||
|
3372,3880,2086,2502,2872,4659,4285,3619,1926,421,1746,2396,1905,2830,2613,752,4851,2620,1506,4495,2733,4338,2834,4021,4727,4315,2714,4036,1676,2461,4840,4193,535,3791,1210,359,3766,0,4233,97,4396,5000,5000,4964,1835,1439,712,1771,3693,5000,1656,1766,4812,1701,2789,4788,3228,2539,1015,933,3098,4224,5000,0,3037,1783,353,1596,799,3670,1659,4632,0,3166,2965,4481,1131,4899,2808,1283,2019,4754,2286,2385,4033
|
||||||
|
3332,3840,2049,2469,2844,4632,4319,3647,1946,39,1789,2415,1930,2863,2652,732,4826,2585,1479,4479,2723,4324,2861,4058,4773,4362,2743,4061,1705,2496,4877,4165,500,3761,1183,332,3722,0,4268,139,4440,5000,5000,4976,1876,1450,666,1737,3672,4683,1644,1733,4775,1720,2825,4808,3251,2566,1048,970,3136,4256,5000,0,3019,1767,313,1565,838,3713,1700,4665,0,3210,3019,4534,1161,4865,2777,1254,1989,4715,2253,2357,4086
|
||||||
|
3354,3863,2074,2498,2877,4667,4710,3612,1903,15,1770,2373,1894,2896,2692,773,4863,2611,1514,4526,2774,4370,2826,4034,4756,4346,2710,4024,1736,2530,4913,4199,527,3793,1219,368,3741,0,4241,119,4422,5000,5000,4924,1916,1462,1104,1765,3713,4733,1693,1762,4800,1678,2798,4766,3212,2532,1020,1008,3172,4287,5000,0,3064,1812,336,1595,815,3695,1679,4634,0,3192,3010,4587,1190,4893,2808,1286,2020,4738,2282,2393,4140
|
||||||
|
3314,3824,2037,2466,2910,4702,4743,3639,1921,54,1812,2393,1920,2868,2670,752,4837,2574,1488,4573,2826,4414,2854,4072,4802,4392,2739,4050,1704,2503,4887,4170,491,3763,1192,403,3761,0,4277,162,4465,5000,5000,4934,1894,1411,1060,1733,3692,4720,1742,1791,4824,1697,2834,4787,3236,2561,1054,983,3147,4256,5000,0,3048,1796,358,1626,854,3738,1720,4665,0,3238,3064,4576,1156,4859,2777,1256,1989,4761,2311,2428,4195
|
||||||
|
3336,3847,2063,2496,2882,4675,4714,3603,1457,32,1855,2414,1947,2902,2710,793,4872,2598,1526,4559,2814,4396,2820,4048,4786,4375,2767,4076,1735,2538,4923,4203,516,3796,1227,376,3719,0,4252,143,4446,5000,5000,4942,1934,1423,1078,1763,3315,4770,1728,1757,4786,1654,2809,4746,3197,2589,1088,1020,3183,4286,5000,0,3093,1840,320,1596,832,3720,1698,4633,2,3284,3117,4626,1184,4887,2808,1289,2020,4723,2279,2402,4251
|
||||||
|
3297,3870,2089,2526,2916,5000,4746,3628,1475,72,1836,2372,1911,2874,2688,772,4844,2621,1564,4607,2865,4438,2849,4088,4831,4420,2734,4040,1704,2511,4897,4173,540,3828,1263,411,3739,0,4289,185,4488,5000,5000,4887,1913,1793,1036,1732,3357,4820,1775,1785,4810,1673,2845,4767,3221,2556,1060,995,3157,4253,5000,0,3138,1884,343,1628,872,3763,1738,4661,0,3269,3109,4613,1150,4853,2777,1321,2051,4747,2309,2438,4301
|
||||||
|
3319,3831,2053,2494,2887,5000,4715,3591,1491,112,1878,2393,1939,2909,2728,812,4878,2582,1541,4594,2853,4417,2816,4127,4877,4465,2762,4067,1735,2546,4933,4205,502,3799,1236,383,3697,0,4264,228,4530,5000,5000,4894,1953,1806,1055,1763,3339,4808,1760,1751,4771,1629,2881,4788,3245,2585,1095,1033,3192,4281,5000,0,3122,1865,305,1599,850,3744,1776,4689,8,3316,3163,4662,1177,4882,2808,1291,2020,4709,2277,2413,4352
|
||||||
|
3341,3854,2079,2524,2921,5000,4746,3195,1446,91,1858,2352,1904,2882,2769,852,4911,2603,1581,4643,2903,4456,2846,4105,4861,4447,2727,4032,1705,2519,4968,4235,525,3832,1272,417,3719,0,4302,209,4510,5000,5000,4837,1931,1819,1076,1376,3383,4858,1806,1779,4794,1647,2856,4747,3207,2553,1067,1008,3165,4309,5000,0,3167,1908,329,1632,890,3787,1753,4654,0,3303,3154,4647,1204,4910,2839,1324,2050,4734,2309,2449,4404
|
||||||
|
3302,3816,2044,2493,3313,5000,4777,3219,1461,131,1900,2374,1932,2918,2747,830,4882,2561,1559,4631,2889,4494,2876,4145,4907,4490,2755,4059,1737,2555,4941,4204,485,3802,1245,389,3740,0,4340,252,4551,5000,5000,4842,2391,1770,1035,1347,3366,4845,1789,1744,4816,1665,2892,4768,3232,2583,1102,1045,3199,4274,5000,0,3151,1887,292,1665,931,3830,1790,4680,18,3352,3207,4693,1169,4877,2809,1294,2018,4697,2278,2486,4457
|
||||||
|
3324,3839,2071,2524,3347,5000,4745,3180,1415,110,1879,2334,1961,2954,2787,869,4913,2581,1601,4680,2938,4469,2845,4124,4891,4471,2720,4087,1769,2590,4976,4233,506,3836,1280,423,3700,0,4317,233,4529,5000,5000,4846,2431,1784,1057,1381,3411,4895,1833,1770,4776,1621,2867,4728,3195,2551,1137,1082,3234,4301,5000,0,3196,1928,317,1637,910,3811,1765,4643,0,3339,3260,4737,1195,4906,2840,1326,2048,4723,2310,2461,4511
|
||||||
|
3284,3801,2036,2555,3381,5000,4355,3203,1430,151,1920,2356,1928,2928,2766,846,4882,2537,1643,4730,2985,4504,2877,4165,4937,4514,2747,4053,1740,2563,4948,4200,465,3807,1316,456,3723,0,4356,276,4568,5000,5000,4788,2409,1735,597,1353,3394,4944,1876,1797,4798,1638,2903,4749,3220,2581,1111,1057,3205,4265,5000,0,3179,1968,343,1671,951,3853,1801,4667,32,3389,3250,4718,1159,4873,2809,1296,2077,4749,2342,2498,4566
|
||||||
|
3307,3825,2063,2944,3353,5000,4322,3162,1382,193,1961,2378,1957,2964,2806,884,4912,2555,1624,4717,2970,4475,2846,4144,4921,4556,2774,4081,1772,2598,4982,4228,484,3840,1289,427,3684,0,4333,257,4607,5000,5000,5000,2449,1750,620,1389,3440,4932,1857,1761,4757,1594,2877,4709,3245,2612,1146,1094,3238,4290,5000,9,3224,1944,307,1643,930,3833,1775,4689,71,3439,3302,4760,1184,4902,2841,1327,2044,4713,2312,2894,4622
|
||||||
|
3329,3849,2090,2976,3387,5000,4351,3184,1396,172,1939,2339,1925,2939,2785,860,4941,2572,1668,4767,3015,4507,2879,4186,4967,4535,2739,4047,1743,2572,4954,4256,503,3873,1324,460,3707,0,4373,300,4584,5000,5000,5000,2426,1702,582,1425,3486,4981,1898,1787,4779,1611,2913,4731,3209,2581,1119,1068,3209,4252,5000,51,3268,1982,333,1678,972,3875,1810,4649,49,3428,3292,4739,1147,4869,2872,1359,2073,4740,2345,2931,4672
|
||||||
|
3290,3811,2056,2945,3359,4734,4317,3204,1410,213,1978,2362,1955,2976,2825,898,4908,2526,1650,4754,2998,4475,2911,4228,5000,4575,2765,4076,1776,2607,4987,4221,460,3845,1297,431,3669,0,4413,342,4621,5000,5000,5000,2466,1297,606,1399,3471,4967,1876,1750,4737,1628,2949,4753,3235,2612,1155,1105,3241,4276,5000,32,3250,1957,298,1652,951,3916,1844,4670,91,3479,3342,4777,1171,4898,2842,1329,2040,4705,2317,2907,4723
|
||||||
|
3313,3835,2504,2977,3394,4767,4345,3163,1361,192,1954,2323,1923,3013,2865,935,4935,2541,1695,4804,3042,4504,2883,4208,4996,4553,2729,4043,1748,2642,5000,4247,477,3879,1332,463,3694,0,4391,323,4596,5000,5000,5000,2505,1313,631,1437,3517,5000,1915,1775,4758,1583,2923,4713,3199,2582,1129,1079,3273,4299,5000,76,3294,1992,325,1687,993,3896,1815,4629,71,3468,3330,4815,1195,4928,2873,1360,2068,4733,2770,2944,4775
|
||||||
|
3274,3798,2470,2947,3428,4800,4372,3182,1374,234,1992,2347,1954,2989,2844,909,4900,2494,1678,4791,3084,4531,2916,4251,5000,4592,2755,4073,1781,2615,4990,4211,433,3850,1305,494,3719,0,4432,365,4632,5000,5000,5000,2482,1267,595,1413,3503,5000,1892,1800,4778,1600,2958,4735,3226,2614,1164,1115,3242,4260,5000,57,3275,1965,353,1724,1034,3936,1847,4648,114,3519,3380,4788,1157,4895,2843,1329,2034,4699,2804,2982,4828
|
||||||
|
3296,3823,2498,2979,2980,4770,4337,3139,1325,213,1967,2371,1985,3026,2884,945,4926,2508,1724,4840,3064,4494,2889,4232,5000,4569,2780,4102,1814,2650,5000,4235,449,3884,1340,464,3682,0,4411,344,4605,5000,5000,5000,2101,1283,621,1451,3550,5000,1929,1763,4736,1555,2932,4696,3190,2646,1200,1151,3273,4281,5000,101,3318,1998,319,1698,1014,3915,1817,4604,95,3571,3428,4823,1180,4925,2874,1360,2062,4727,2776,2958,4882
|
||||||
|
3257,4205,2526,3011,3013,4802,4363,3158,1338,254,2004,2333,1954,3002,2862,919,4889,2521,1770,4888,3104,4518,2923,4275,5000,4606,2744,4070,1786,2622,4991,4198,403,3918,1375,495,3708,0,4452,386,4639,5000,5000,5000,2078,1238,587,1429,3597,5000,1965,1787,4756,1572,2967,4719,3217,2616,1174,1124,3241,4240,5000,82,3359,2030,348,1735,1056,3955,1848,4621,139,3560,3414,4793,1141,4892,2844,1391,2089,5000,2811,2995,4937
|
||||||
|
3280,4230,2493,2981,2985,4771,4327,3114,1351,294,2039,2357,1986,3040,2901,954,4913,2471,1755,4875,3081,4478,2896,4256,5000,4643,2769,4100,1820,2657,5000,4221,418,3890,1347,464,3672,0,4431,427,4673,5000,5000,5000,2116,1255,614,1468,3583,5000,1937,1749,4713,1527,2939,4741,3244,2648,1210,1159,3270,4261,5000,126,3339,1999,314,1710,1035,3932,1878,4638,184,3612,3460,4824,1164,4922,2875,1360,2054,5000,2783,2971,4993
|
||||||
|
3303,4256,2522,2594,3019,4803,4352,3132,1301,272,2012,2320,1956,3017,2879,988,4937,2482,1802,4922,3119,4499,2932,4299,5000,4617,2732,4068,1792,2629,5000,4244,433,3924,1381,494,3699,0,4472,405,4643,5000,5000,4556,2092,1211,643,1509,3631,5000,1971,1772,4733,1544,2973,4703,3210,2619,1184,1132,3237,4281,5000,170,3379,2028,344,1747,1077,3971,1845,4591,168,3601,3444,4791,1124,4952,2907,1391,2081,5000,2818,2588,5043
|
||||||
|
3684,4219,2489,2564,2991,4772,4377,3149,1314,312,2046,2346,1989,3055,2918,960,4898,2431,1787,4907,3094,4517,2968,4343,5000,4653,2756,4099,1826,2663,5000,4204,386,3896,1354,462,3665,0,4513,445,5000,5000,5000,4558,2130,1229,610,1488,3616,5000,1941,1734,4752,1561,3007,4726,3238,2652,1220,1167,3265,4238,5000,151,3357,1995,311,1723,1119,4010,1874,4606,214,3652,3488,4819,1146,4920,2876,1359,2466,5000,2791,2625,5094
|
||||||
|
3708,4244,2518,2597,3024,4802,4339,3104,1264,290,2017,2309,2021,3093,2957,993,4919,2442,1834,4954,3129,4473,2942,4325,5000,4625,2719,4068,1860,2697,5000,4226,400,3930,1388,492,3693,0,4492,423,5000,5000,5000,4559,2168,1248,640,1529,3664,5000,1972,1757,4709,1517,2979,4687,3204,2623,1194,1201,3293,4256,5000,195,3396,2022,341,1760,1098,3986,1839,4558,199,3641,3532,4845,1167,4950,2907,1389,2492,5000,2827,2601,5146
|
||||||
|
3669,4208,2065,2630,3058,4832,4362,3120,1276,329,2049,2335,1993,3070,2934,963,4879,2390,1820,5000,3163,4488,2979,4368,5000,4659,2743,4099,1832,2668,5000,4185,352,3902,1422,521,3721,0,4534,462,5000,5000,4851,4500,2143,1205,608,1509,3650,5000,2002,1779,4727,1534,3012,4711,3232,2656,1230,1173,3258,4212,5000,176,3373,2048,372,1799,1140,4024,1866,4571,246,3692,3512,4807,1126,4918,2877,1357,2456,5000,2442,2638,5199
|
||||||
|
3692,4234,2095,2600,3029,4800,4323,3074,1227,306,2080,2361,2026,3109,2973,995,4899,2399,1867,4983,3133,4441,2954,4350,5000,4691,2767,4130,1866,2702,5000,4205,365,3937,1393,488,3688,0,4513,859,5000,5000,4856,4503,2181,1225,639,1551,3697,5000,1968,1740,4684,1490,2982,4673,3260,2689,1266,1207,3285,4230,5000,219,3410,2010,340,1775,1119,3999,1830,4522,294,3742,3553,4829,1147,4948,2908,1807,2481,5000,2416,2613,5253
|
||||||
|
3654,4260,2125,2633,3062,4830,4346,3090,1239,345,2048,2326,1998,3086,2949,964,4919,2408,1915,5000,3164,4453,2991,4394,5000,4661,2729,4099,1838,2673,5000,4163,378,3971,1427,516,3717,0,4554,897,5000,5000,4798,4444,2156,1183,609,1594,3745,5000,1996,1762,4702,1508,3014,4697,3227,2661,1239,1178,3249,4185,5000,262,3446,2034,371,1814,1161,4035,1855,4533,280,3730,3530,4788,1106,4916,2939,1837,2507,5000,2452,2649,5308
|
||||||
|
3678,3804,2093,2604,3033,4797,4306,3105,1252,382,2077,2353,2032,3124,2987,994,4875,2354,1900,5000,3132,4403,2967,4438,5000,4693,2752,4131,1872,2705,5000,4182,329,3943,1398,482,3685,0,4595,935,5000,5000,4802,4448,2192,1204,641,1575,3730,5000,1960,1722,4659,1464,3046,4721,3256,2695,1275,1211,3274,4201,5000,242,3420,1994,341,1790,1140,4071,1879,4544,328,3779,3569,4808,1126,4946,2909,1804,2470,4749,2426,2624,5364
|
||||||
|
3701,3831,2123,2637,3065,4825,4328,3058,1202,358,2044,2318,2004,3101,3025,1024,4893,2362,1948,5000,3160,4413,3005,4420,5000,4661,2713,4101,1843,2737,5000,4200,341,3977,1431,510,3715,0,4995,910,5000,5000,4744,4390,2166,1225,674,1619,3778,5000,1985,1743,4677,1482,3015,4683,3223,2667,1249,1181,3299,4217,5000,285,3455,2015,373,1829,1181,4045,1841,4493,315,3766,3544,4763,1146,4977,3360,1834,2494,4780,2462,2660,5414
|
||||||
|
3663,3795,2091,2608,3036,4853,4349,3073,1215,394,2071,2345,2039,3140,3000,991,4849,2308,1933,5000,3187,4421,3044,4464,5000,4690,2736,4133,1877,2707,5000,4157,292,3949,1402,475,3746,0,5000,946,5000,5000,4747,4396,2202,1185,646,1601,3763,5000,1946,1765,4696,1501,3045,4708,3253,2701,1284,1213,3262,4170,4970,264,3426,1973,343,1869,1221,4079,1863,4502,364,3814,3580,4779,1104,4945,3329,1801,2457,4750,2498,2696,5465
|
||||||
|
3267,3822,2121,2641,3068,4819,4308,3026,1166,368,2035,2373,2073,3179,3037,1020,4865,2315,1981,5000,3150,4367,3021,4446,5000,4657,2697,4165,1911,2739,5000,4174,304,3983,1435,502,3715,0,5000,919,4646,5000,4751,4402,2238,1208,680,1645,3809,5000,1969,1724,4652,1458,3013,4671,3220,2673,1320,1245,3285,4185,4980,306,3459,1991,375,1846,1200,4051,1823,4449,351,3861,3614,4794,1123,4975,3360,1830,2061,4783,2472,2669,5517
|
||||||
|
3229,3787,2152,2674,3100,4846,4328,3040,1180,404,2060,2339,2046,3156,3011,986,4819,2260,2028,5000,3174,4373,3060,4490,5000,4684,2720,4135,1883,2708,5000,4129,255,4018,1467,528,3747,415,5000,954,4669,5000,4692,4347,2211,1169,653,1628,3794,5000,1990,1745,4670,1477,3042,4696,3250,2707,1293,1214,3246,4138,4928,284,3490,2008,408,1886,1240,4085,1844,4458,400,3846,3585,4745,1081,5000,3329,1796,2084,4815,2508,2705,5570
|
||||||
|
3253,3814,2120,2644,3070,4811,4286,2993,1131,438,2084,2368,2082,3195,3047,1013,4835,2267,2012,5000,3135,4316,3037,4472,5000,4711,2742,4167,1916,2739,5000,4146,267,3990,1437,492,3717,392,5000,926,4691,5000,4696,4355,2246,1192,688,1673,3840,5000,1948,1704,4627,1435,3009,4721,3280,2741,1328,1245,3269,4152,4938,325,3459,1963,380,1863,1218,4056,1802,4466,450,3892,3616,4756,1100,5000,3360,1825,2046,4786,2482,2677,5624
|
||||||
|
3278,3841,2151,2677,3102,4838,4306,3007,1145,410,2046,2335,2055,3171,3021,1039,4849,2273,2059,5000,3156,4320,3077,4515,5000,4675,2703,4138,1888,2707,5000,4162,279,4024,1469,518,3749,431,5000,540,4651,5000,4637,4302,2219,1154,724,1718,3886,5000,1966,1724,4645,1455,3037,4685,3249,2714,1301,1213,3229,4104,4949,364,3488,1978,413,1903,1258,4088,1822,4411,437,3875,3584,4704,1057,5000,3390,1433,2069,4819,2519,2712,5679
|
||||||
|
3240,3806,2120,2648,3071,4802,4325,3021,1160,443,2068,2364,2091,3210,3056,1003,4801,2218,2043,5000,3113,4261,3117,4559,5000,4700,2725,4170,1921,2737,5000,4116,230,3996,1438,481,4140,470,5000,573,4671,5000,4641,4312,2253,1179,698,1701,3869,5000,1922,1683,4601,1475,3065,4710,3279,2748,1335,1243,3250,4117,4897,341,3455,1930,385,1881,1298,4120,1840,4418,486,3920,3613,4713,1496,5000,3359,1399,2030,4790,2493,2684,5735
|
||||||
|
3264,3834,2151,2681,3102,4828,4281,2973,1113,414,2027,2331,2065,3249,3091,1028,4815,2224,2088,5000,3132,4263,3095,4541,5000,4662,2685,4141,1954,2766,5000,4131,242,4030,1470,505,4174,447,5000,543,4628,5000,4583,4261,2287,1204,735,1746,3914,5000,1938,1703,4620,1434,3030,4674,3248,2721,1308,1272,3271,4130,4908,380,3482,1943,419,1921,1275,4088,1796,4363,474,3901,3578,4720,1515,5000,3389,1426,2053,4823,2529,2718,5785
|
||||||
|
3227,3799,2120,2652,3133,4853,4300,2987,1128,445,2048,2361,2101,3225,3063,990,4766,2168,2071,5000,3148,4265,3136,4584,5000,4685,2706,4173,1925,2733,5000,4085,193,4001,1439,530,4207,487,4710,574,4647,5000,4587,4274,2258,1168,711,1730,3896,5000,1953,1723,4639,1455,3056,4700,3279,2755,1342,1239,3230,4081,4857,356,3446,1893,454,1961,1314,4119,1813,4369,523,3944,3604,4664,1472,5000,2938,1392,2013,4857,2565,2751,5836
|
||||||
|
3252,3827,2151,2684,3101,4816,4256,2938,1082,414,2067,2391,2138,3264,3097,1014,4778,2175,2116,5000,3102,4203,3115,4565,5000,4645,2728,4206,1958,2762,5000,4099,206,4035,1470,912,4179,465,4686,542,4602,5000,4591,4287,2291,1194,749,1775,3940,5000,1905,1681,4595,1415,3020,4664,3248,2790,1376,1268,3250,4094,4868,393,3471,1904,426,1939,1290,4086,1768,4313,571,3985,3628,5000,1491,5000,2968,1419,2035,4829,2539,2722,5888
|
||||||
|
3215,3855,2183,2717,3132,4840,4273,2952,1098,444,2023,2359,2112,3240,3069,974,4728,2181,2159,5000,3116,4203,3156,4608,5000,4667,2687,4177,1928,2728,4987,4052,219,4069,1500,935,4214,504,4725,572,4619,4985,4535,4240,2261,1159,726,1759,3983,5000,1918,1701,4614,1437,3045,4691,3280,2763,1348,1234,3207,4044,4817,429,3495,1914,462,1979,1328,4115,1783,4318,558,3963,3588,5000,1448,5000,2936,1446,2057,4863,2575,2754,5941
|
||||||
|
3240,3821,2152,2687,3099,4802,4229,2904,1114,473,2041,2390,2149,3278,3101,997,4739,2125,2141,5000,3067,4141,3135,4651,5000,4687,2709,4210,1961,2756,5000,4066,170,4040,1468,897,4187,62,4700,601,4635,4991,4540,4257,2294,1187,765,1805,3964,5000,1868,1659,4571,1398,3069,4717,3311,2797,1381,1261,3226,4056,4829,402,3456,1862,435,1957,1304,4081,1798,4324,606,4003,3609,5000,1467,5000,2966,1410,2016,4835,2548,2724,5995
|
||||||
|
3265,3850,2184,2720,3129,4826,4246,2918,1070,440,1996,2359,2123,3254,3134,1018,4750,2132,2183,5000,3079,4140,3176,4631,5000,4645,2668,4180,1931,2721,5000,4081,184,4074,1918,919,4222,102,4738,566,4589,4935,4484,4212,2263,1215,804,1851,4006,5000,1879,1679,4591,1421,3031,4682,3281,2770,1353,1226,3182,4068,4841,437,3478,1870,470,1997,1342,4108,1751,4267,592,3979,3987,4972,1486,5000,2996,1437,2038,4869,2584,2756,6050
|
||||||
|
3228,3816,2153,2690,3096,4849,4262,2932,1088,467,2012,2390,2160,3292,3103,977,4699,2077,2163,5000,3028,4138,3217,4674,5000,4664,2689,4213,1963,2748,4985,4033,137,4045,1886,880,4257,141,4775,594,4603,4941,4491,4231,2295,1181,782,1834,3985,5000,1826,1636,4610,1444,3054,4709,3313,2805,1386,1253,3200,4018,4792,409,3436,1816,444,2037,1378,4135,1764,4272,640,4016,4006,4973,1443,5000,2964,1401,1997,4842,2557,2787,6106
|
||||||
|
3253,3845,2185,2722,3125,4809,4217,2885,1044,432,1965,2359,2197,3329,3135,997,4709,2084,2204,5000,3037,4075,3197,4654,5000,4620,2648,4246,1994,2774,5000,4047,151,4079,1915,902,3811,119,4749,558,4555,4884,4436,4252,2325,1211,822,1880,4026,5000,1835,1656,4568,1406,3015,4675,3283,2777,1418,1279,3218,4030,4805,442,3456,1824,480,2015,1353,4099,1715,4215,625,3990,4022,4973,1042,5000,2994,1427,2017,4876,2593,2755,6156
|
||||||
|
3217,3812,2154,2754,3154,4832,4233,2899,1063,458,1979,2391,2172,3304,3103,955,4657,2029,2244,5000,3045,4073,3239,4695,5000,4637,2669,4217,1963,2738,4960,3999,104,4470,1944,923,3847,159,4785,584,4568,4890,4444,4211,2294,1178,801,1864,4004,5000,1843,1675,4588,1431,3037,4702,3315,2812,1389,1243,3173,3979,4756,412,3413,1831,517,2055,1389,4124,1728,4219,672,4444,3976,4910,999,5000,2961,1391,2038,4911,2628,2785,6207
|
||||||
|
3242,3840,2186,2724,3120,4792,4187,2852,1021,483,1993,2423,2210,3341,3133,974,4666,2036,2222,5000,2991,4009,3218,4675,5000,4653,2689,4249,1994,2763,4979,4012,120,4503,1911,883,3821,136,4759,546,4580,4895,4453,4234,2324,1208,842,1909,4043,5000,1788,1633,4546,1394,2997,4668,3347,2846,1421,1268,3190,3991,4770,443,3431,1775,491,2032,1363,4087,1678,4224,718,4478,3990,4908,1018,5000,2991,1416,1996,4884,2600,2752,6259
|
||||||
|
3268,3869,2218,2756,3148,4813,4203,2866,1041,445,1944,2393,2185,3316,3101,930,4675,2044,2260,5000,2997,4007,3260,4716,5000,4607,2648,4220,1963,2726,4935,4026,136,4535,1939,484,3858,176,4794,570,4530,4838,4401,4197,2291,1177,821,1954,4082,5000,1794,1652,4567,1419,3018,4696,3318,2819,1390,1230,3144,3941,4785,473,3447,1781,528,2072,1398,4111,1689,4167,701,4448,3941,4424,976,4999,3020,1441,2017,4918,2635,2781,6312
|
||||||
|
3232,3837,2188,2726,3114,4772,4157,2882,1062,468,1957,2426,2222,3353,3130,948,4622,1991,2236,5000,2941,3943,3302,4756,5000,4622,2669,4253,1993,2750,4952,3978,511,4506,1905,442,3832,153,4828,593,4541,4844,4412,4223,2320,1209,863,1938,4058,5000,1738,1610,4525,1445,3038,4724,3350,2853,1422,1254,3160,3953,4738,441,3401,1724,503,2050,1370,4134,1700,4172,1166,4479,3953,4421,995,5000,2987,1404,1975,4891,2607,2747,6366
|
||||||
|
3258,3866,2219,2757,3141,4793,4173,2835,1022,428,1906,2396,2198,3389,3158,965,4631,1999,2272,5000,2945,3942,3282,4735,5000,4574,2627,4223,1961,2774,4969,3992,529,4539,1933,462,3869,192,4800,553,4489,4787,4362,4189,2349,1240,905,1982,4095,5000,1743,1630,4546,1410,2996,4690,3321,2825,1391,1216,3175,3964,4754,469,3416,1729,540,2089,1404,4094,1648,4114,1148,4447,3901,4418,1015,5000,3016,1429,1994,4926,2641,2775,6421
|
||||||
|
3222,3833,2189,2726,3168,4814,4188,2851,1044,450,1918,2429,2235,3363,3124,920,4577,1946,2246,5000,2949,3940,3324,4775,5000,4587,2648,4255,1991,2736,4924,3944,485,4509,1478,482,3907,231,4833,575,4499,4793,4374,4218,2315,1211,886,1965,4069,5000,1685,1649,4568,1437,3016,4719,3354,2860,1422,1239,3129,3914,4708,434,3368,1672,577,2129,1438,4116,1658,4120,1191,4475,3490,4352,973,5000,2982,1391,1952,4899,2675,2802,6477
|
||||||
|
3248,3863,2221,2757,3133,4772,4141,2805,1005,409,1867,2463,2272,3398,3151,936,4585,1956,2281,5000,2890,3877,3305,4752,5000,4538,2668,4288,2020,2759,4941,4379,503,4541,1505,440,3882,208,4803,534,4447,4737,4388,4248,2343,1244,928,2009,4104,5000,1688,1607,4528,1402,2973,4685,3325,2894,1452,1262,3144,3926,4725,461,3381,1677,553,2106,1409,4075,1605,4483,1172,4502,3498,4348,993,5000,3011,1416,1971,4934,2646,2767,6527
|
||||||
|
3213,3830,2253,2788,3159,4792,4157,2821,1029,429,1877,2434,2248,3372,3116,889,4532,1966,2314,5000,2893,3877,3347,4791,5000,4549,2626,4258,1987,2719,4895,4331,461,4573,1532,459,3920,246,4835,554,4456,4743,4341,4218,2308,1215,909,1992,4139,5000,1691,1627,4550,1430,2992,4714,3358,2866,1420,1222,3096,3877,4681,425,3393,1681,591,2145,1442,4095,1614,4488,1214,4466,3443,4282,952,5000,2977,1440,1991,4969,2679,2793,6578
|
||||||
|
3239,3860,2223,2757,3123,4749,4110,2776,1054,448,1887,2468,2285,3406,3142,904,4540,1915,2285,5000,2833,3815,3327,4768,5000,4561,2647,4290,2015,2741,4911,4346,481,4123,1496,416,3895,222,4804,573,4465,4750,4357,4251,2335,1249,952,2035,4111,5000,1631,1585,4510,1397,2948,4743,3391,2899,1450,1244,3111,3889,4700,449,3343,1624,566,2122,1412,4053,1623,4494,1255,4070,3449,4278,973,5000,3005,1401,1948,4942,2650,2756,6630
|
||||||
|
3266,3890,2254,2788,3148,4768,4125,2793,1017,404,1835,2439,2261,3379,3105,919,4548,1927,2316,5000,2834,3816,3369,4806,5000,4509,2605,4260,1982,2701,5000,4361,502,4155,1522,434,3933,260,4834,529,4411,4695,4312,4224,2299,1221,995,2079,4143,5000,1633,1605,4533,1426,2966,4711,3362,2871,1417,1203,3063,3902,4719,473,3354,1629,604,2160,1443,4072,1989,4438,1233,4031,3392,4212,932,5000,3033,1425,1967,4976,2682,2781,6683
|
||||||
|
3231,3858,2224,2756,3112,4725,4141,2811,1043,422,1844,2473,2298,3413,3130,871,4494,1877,2285,5000,2773,3817,3411,4844,5000,4519,2625,4291,2009,2722,5000,4314,461,4124,1486,391,3909,298,4864,547,4419,4702,4330,4260,2325,1256,977,2060,4113,5000,1572,1563,4556,1455,2983,4740,3395,2905,1446,1224,3077,3853,4677,434,3302,1571,580,2136,1474,4091,1997,4444,1272,4053,3396,4209,953,5000,2999,1386,1924,4949,2652,2806,6737
|
||||||
|
3258,3888,2256,2786,3137,4744,4094,2767,1008,377,1792,2446,2335,3447,3155,885,4502,1890,2314,5000,2774,3758,3391,4819,5000,4467,2584,4261,2037,2743,5000,4329,64,4155,1511,409,3947,273,4831,502,4365,4648,4288,4297,2350,1292,1020,2102,4144,5000,1573,1583,4518,1423,2938,4707,3366,2876,1412,1245,3091,3866,4697,456,3312,1576,618,2175,1442,4047,1942,4389,828,4012,3399,4205,975,5000,3027,1409,1942,4984,2684,2768,6792
|
||||||
|
3223,3856,2226,2816,3161,4762,4109,2785,1035,393,1801,2480,2310,3418,3116,836,4449,1841,2281,5000,2775,3761,3433,4855,5000,4475,2604,4292,2002,3122,5000,4283,25,4124,1536,427,3985,310,4860,518,4372,4657,4309,4273,2313,1265,1002,2082,4112,5000,1573,1604,4542,1453,2955,4737,3400,2909,1440,1203,3042,3817,4657,415,3259,1582,657,2212,1472,4484,1950,4396,865,4031,3339,4140,935,5000,2992,1370,1899,5000,2715,2791,6848
|
||||||
|
3250,3886,2257,2783,3123,4719,4063,2742,1002,347,1810,2515,2347,3451,3140,848,4457,1855,2308,5000,2713,3703,3412,4830,5000,4484,2624,4323,2029,3142,5000,4300,49,4155,1499,382,3961,285,4826,472,4318,4666,4331,4313,2338,1301,1046,2124,4141,5000,1512,1562,4504,1422,2909,4705,3433,2942,1468,1222,3056,3831,4679,435,3267,1525,633,2188,1439,4438,1895,4342,902,4049,3341,4137,957,5000,3019,1393,1918,4991,2684,2752,6898
|
||||||
|
3215,3917,2289,2813,3147,4736,4079,2762,1030,362,1757,2487,2322,3422,3100,799,4465,1870,2334,5000,2713,3709,3454,4865,5000,4429,2583,4292,1993,3100,5000,3834,73,4186,1523,399,4000,321,4853,487,4325,4614,4292,4292,2299,1276,1027,2165,4169,5000,1512,1583,4529,1453,2926,4735,3405,2913,1433,1180,3007,3784,4640,454,3275,1531,671,2225,1468,4454,1902,3930,875,4004,3279,4073,918,5000,3047,1415,1936,5000,2714,2774,6949
|
||||||
|
3242,3885,2258,2780,3109,4692,4032,2782,1060,377,1766,2522,2359,3454,3122,811,4412,1824,2297,4997,2651,3653,3434,4900,5000,4437,2603,4323,2439,3119,5000,3852,37,4154,1485,354,3976,295,4879,501,4332,4625,4316,4334,2323,1313,1071,2144,4135,5000,1450,1542,4492,1423,2942,4765,3438,2946,1460,1198,3020,3798,4664,410,3221,1476,648,2200,1854,4470,1909,3939,909,4020,3280,4071,941,5000,3011,1376,1893,4997,2682,2733,7001
|
||||||
|
3270,3915,2290,2808,3132,4710,4048,2741,1028,328,1713,2495,2334,3424,3144,822,4421,1841,2321,5000,2652,3661,3475,4873,5000,4381,2562,4291,2402,3138,5000,3869,63,4184,1509,371,4014,330,4843,453,4278,4574,4280,4315,2284,1351,1115,2184,4161,5000,1450,1564,4518,1455,2896,4734,3409,2916,1425,1155,3034,3813,4689,428,3228,1483,686,2237,1881,4423,1854,3886,880,3973,3218,4008,965,5000,3038,1398,1911,5000,2711,2755,7054
|
||||||
|
3235,3884,2259,2775,3093,4727,4064,2762,1059,342,1722,2531,2371,3455,3103,772,4368,1797,2282,4950,2652,3670,3516,4907,5000,4388,2582,4322,2428,3095,4803,3826,28,4152,1470,326,4052,365,4868,466,4285,4586,4307,4359,2307,1326,1097,2161,4124,5000,1388,1585,4544,1487,2911,4764,3443,2948,1451,1173,2985,3767,4652,382,3173,1429,662,2273,1908,4437,1440,3896,912,3986,3217,4008,927,5000,3003,1357,1867,5000,2740,2775,7108
|
||||||
|
3263,3914,2290,2803,3115,4682,4019,2722,1029,293,1670,2566,2407,3486,3123,783,4378,1815,2303,4956,2591,3618,3495,4878,5000,4332,2541,4772,2452,3113,4818,3844,56,4182,1493,342,4028,338,4830,417,4230,4537,4335,4404,2329,1365,1141,2200,4149,5000,1388,1545,4509,1458,2865,4733,3414,2918,1477,1191,2998,3783,4678,398,3180,1437,701,2667,1872,4389,1385,3845,881,3998,3216,4009,951,5000,3029,1379,1886,5000,2707,2733,7163
|
||||||
|
3229,3883,2321,2831,3137,4699,4035,2744,1061,305,1679,2539,2381,3454,3081,732,4326,1772,2324,4962,2592,3630,3536,4911,5000,4337,2562,4739,2415,3070,4772,3802,23,4211,1516,358,4066,372,4853,429,4238,4551,4303,4388,2288,1341,1122,2176,4110,5000,1388,1567,4535,1491,2881,4764,3448,2950,1440,1146,2949,3738,4643,351,3187,1446,739,2703,1898,4402,1392,3856,911,3948,3153,3948,914,5000,2993,1338,1904,5000,2735,2753,7219
|
||||||
|
3257,3914,2291,2797,3097,4654,3990,2705,1032,318,1689,2575,2418,3484,3100,742,4336,1792,2282,4905,2531,3581,3515,4881,5000,4343,2583,4769,2439,2668,4787,3822,53,4179,1477,312,4042,343,4814,379,4245,4566,4334,4435,2310,1380,1166,2214,4133,5000,1326,1527,4501,1462,2834,4795,3481,2981,1465,1164,2963,3755,4671,366,3131,1394,716,2676,1861,3933,1336,3868,940,3958,3151,3951,939,5000,3019,1360,1860,5000,2701,2710,7269
|
||||||
|
3285,3944,2321,2825,3119,4671,4007,2729,1066,267,1638,2548,2392,3451,3057,753,4346,1814,2300,4909,2533,3595,3555,4913,5000,4286,2962,4736,2400,2623,4741,3842,83,4208,1499,328,4080,376,4837,390,4191,4519,4304,4420,2269,1357,1210,2251,4155,5000,1327,1549,4528,1496,2850,4764,3453,2950,1427,1118,2914,3710,4700,379,3138,1405,1174,2711,1885,3945,1343,3819,905,3905,3088,3893,903,5000,3045,1381,1879,5000,2728,2729,7320
|
||||||
|
3251,3913,2290,2790,3078,4626,4024,2753,1100,279,1649,2584,2427,3480,3075,701,4295,1774,2256,4851,2474,3549,3595,4944,5000,4291,2983,4765,2424,2641,4757,3801,52,4174,1458,282,4056,409,4858,401,4199,4536,4337,4469,2289,1397,1191,2225,4113,5000,1266,1510,4494,1530,2865,4795,3486,2981,1452,1135,2928,3729,4667,330,3083,1355,1151,2684,1909,3957,1350,3832,932,3913,3086,3897,929,5000,3008,1340,1835,5000,2693,2686,7372
|
||||||
|
3279,3944,2321,2817,3099,4643,3980,2716,1073,228,1598,2558,2401,3509,3093,711,4307,1796,2272,4855,2477,3567,3573,4912,5000,4233,2942,4732,2027,2658,4773,3823,84,4203,1480,298,4093,379,4817,350,4146,4492,4309,4455,2309,1437,1234,2261,4133,5000,1267,1533,4523,1502,2819,4764,3457,2950,1413,1151,2941,3747,4698,343,3089,1367,1189,2718,1450,3907,1295,3785,895,3859,3022,3903,956,5000,3033,1361,1854,5000,2719,2703,7425
|
||||||
|
3246,3913,2289,2781,3120,4659,3998,2741,1108,239,1610,2594,2436,3475,3048,659,4257,1758,2226,4858,2481,3586,3613,4942,5000,4658,2963,4760,1987,2614,4728,3783,54,4169,1439,313,4131,410,4837,360,4154,4510,4345,4505,2266,1415,1215,2234,4090,4990,1269,1557,4552,1536,2835,4796,3490,2980,1437,1105,2893,3705,4667,293,3034,1739,1227,2752,1473,3918,1302,3800,920,3865,3021,3849,920,5000,2997,1319,1810,5000,2745,2721,7479
|
||||||
|
3274,3943,2320,2808,3079,4614,3954,2705,1082,188,1622,2630,2471,3502,3065,669,4269,1783,2241,4799,2423,3545,3590,4909,5000,4600,2985,4788,2009,2631,4745,3806,88,4197,1460,267,4106,379,4795,308,4102,4530,4381,4554,2286,1455,1258,2268,4108,4994,1209,1518,4519,1509,2789,4765,3462,3010,1460,1121,2907,3725,4698,304,3041,1754,1203,2723,1433,3867,1248,3754,943,3871,3019,3857,948,5000,3022,1340,1829,5000,2708,2676,7534
|
||||||
|
3241,3974,2350,2834,3099,4631,3973,2732,1118,199,1573,2604,2444,3467,3020,617,4220,1809,2254,4801,2428,3567,3629,4938,5000,4604,2944,4334,1969,2586,4700,3768,121,4224,1481,282,4143,409,4814,318,4111,4489,4357,4542,2242,1434,1239,2239,4125,4997,1211,1542,4548,1543,2805,4797,3495,2978,1420,1075,2859,3684,4669,315,3048,1769,1241,2336,1454,3877,1255,3771,903,3814,2956,3805,913,5000,2984,1360,1847,5000,2733,2693,7590
|
||||||
|
3269,3943,2318,2798,3057,4585,3930,2697,1155,210,1587,2640,2479,3494,3036,627,4234,1774,2205,4742,2373,3529,3606,4966,5000,4608,2966,4361,1991,2602,4717,3792,94,4190,1439,236,4118,377,4770,327,4121,4511,4396,4593,2261,1475,1281,2272,4080,4939,1152,1505,4516,1516,2821,4829,3528,3007,1442,1090,2874,3705,4702,263,3414,1724,1217,2306,1413,3826,1263,3788,924,3818,2956,3817,942,5000,3009,1319,1804,5000,2696,2648,7640
|
||||||
|
3298,3974,2348,2823,3077,4602,3950,2725,1130,158,1539,2614,2451,3458,3051,637,4248,1801,2217,4744,2380,3555,3645,4931,5000,4550,2926,4326,1950,2557,4735,3817,129,4217,1460,251,4154,407,4788,274,4070,4472,4374,4581,2217,1516,1323,2304,4095,4941,1156,1529,4547,1551,2776,4798,3499,2975,1402,1044,2826,3727,4736,273,3422,1742,1255,2338,1434,3835,1209,3745,882,3760,2893,3768,970,5000,3033,1339,1823,5000,2719,2664,7691
|
||||||
|
3265,3943,2316,2787,3034,4619,3969,2753,1168,169,1555,2650,2485,3484,3004,585,4201,1768,2166,4685,2326,3582,3683,4958,5000,4554,2528,4353,1971,2574,4691,3781,103,4182,1417,204,4191,435,4805,284,4081,4496,4415,4632,2234,1495,1303,2273,4048,4882,1099,1492,4577,1586,2793,4830,3531,3004,1424,1059,2841,3688,4709,220,3368,1700,811,2370,1454,3845,1218,3765,900,3763,2894,3782,937,5000,2996,1297,1779,5000,2681,2680,7743
|
||||||
|
3294,3973,2346,2812,3054,4574,3928,2720,1144,118,1508,2624,2519,3509,3019,595,4216,1798,2177,4687,2335,3549,3658,5000,5000,4496,2489,4380,1992,2591,4709,3807,139,4209,1437,219,4165,401,4759,231,4031,4459,4395,4683,2251,1536,1344,2304,4062,4885,1104,1518,4546,1559,2748,4800,3502,2970,1445,1074,2857,3711,4744,650,3377,1720,848,2339,1411,3792,1165,3723,856,3703,2894,3798,966,5000,3020,1317,1798,5000,2704,2633,7796
|
||||||
|
3261,3942,2313,2836,3073,4591,3949,2750,1182,129,1525,2660,2490,3471,2972,543,4170,1766,2186,4690,2346,3580,3696,5000,5000,4500,2511,4344,1950,2545,4666,3772,114,4173,1457,235,4200,429,4776,240,4043,4485,4437,4671,2206,1516,1323,2271,4013,4887,1110,1543,4578,1594,2766,4832,3535,2998,1403,1027,2810,3673,4718,597,3324,1742,886,2370,1430,3802,1175,3745,873,3705,2834,3754,934,5000,2981,1275,1818,5000,2727,2649,7850
|
||||||
|
3290,3973,2342,2799,3030,4546,3908,2718,1159,140,1543,2696,2523,3495,2986,553,4187,1798,2133,4630,2295,3550,3671,5000,5000,4084,2534,4370,1970,2562,4685,3800,151,4199,1414,188,4174,394,4729,187,4057,4513,4481,4722,2223,1557,1364,2300,4026,4827,1054,1507,4547,1567,2722,4802,3567,3026,1424,1042,2826,3698,4755,605,3334,1283,861,2338,1386,3749,1122,3767,888,3707,2836,3773,964,5000,3005,1294,1775,5000,2687,2602,7905
|
||||||
|
3319,4004,2371,2823,3049,4563,3930,2749,1198,89,1499,2670,2494,3457,2938,501,4205,1830,2141,4633,2308,3584,4127,5000,5000,4026,2495,4334,1928,2517,4643,3829,189,4225,1433,203,4209,420,4744,196,4008,4479,4463,4710,2177,1537,1342,2328,4037,4829,1062,1533,4579,1602,2740,4834,3537,2992,1382,994,2780,3661,5000,614,3345,1307,898,2368,1404,3757,1133,3728,841,3646,2777,3732,932,5000,3029,1314,1794,5000,2709,2617,7961
|
||||||
|
3286,3972,2338,2785,3006,4518,3890,2780,1237,101,1519,2705,2526,3480,2951,512,4161,1801,2087,4574,2260,3557,4164,5000,5000,4031,2518,4359,1947,2534,4664,3796,165,4188,1389,156,4182,384,4759,205,4023,4509,4508,4760,2193,1579,1382,2294,3986,4770,1009,1498,4549,1637,2759,4867,3570,3019,1402,1009,2797,3687,5000,560,3294,1270,873,2335,1359,3766,1143,3753,855,3647,2780,3755,963,5000,2990,1272,1752,5000,2668,2570,8011
|
||||||
|
3316,4003,2367,2809,3024,4536,3912,2750,1214,51,1477,2679,2496,3503,2964,523,4180,1835,2094,4578,2275,3594,4137,5000,5000,3974,2479,4322,1905,2551,4684,3825,203,4214,1408,172,4216,409,4711,152,3976,4477,4492,4747,2208,1621,1421,2320,3997,4772,1018,1525,4581,1610,2716,4837,3540,2984,1359,962,2814,3714,5000,569,2886,1297,909,2364,1375,3713,1093,3716,805,3586,2723,3779,994,5000,3013,1291,1772,5000,2689,2584,8062
|
||||||
|
3283,3972,2333,2770,3043,4553,3936,2783,1254,63,1498,2715,2528,3463,2915,472,4138,1808,2039,4520,2292,4052,4173,5000,5000,3979,2503,4346,1924,2506,4644,3794,180,4177,1364,187,4250,433,4724,161,3993,4509,4538,4796,2161,1601,1398,2284,3944,4712,967,1552,4614,1644,2735,4869,3571,3011,1378,976,2770,4099,5000,515,2837,1262,946,2392,1392,3721,1105,3742,817,3587,2729,3743,963,5000,2974,1249,1730,5000,2709,2599,8114
|
||||||
|
3313,4002,2362,2793,2999,4509,3897,2754,1231,14,1458,2751,2560,3485,2928,483,4158,1844,2045,4524,2248,4030,4146,5000,5000,3922,2526,4371,1943,2523,4665,3825,219,4201,1382,141,4222,395,4676,109,3948,4480,4585,4845,2176,1643,1437,2309,3954,4714,978,1517,4585,1617,2694,4840,3541,3037,1397,991,2788,4127,5000,523,2851,1292,920,2358,1346,3668,1055,3708,766,3587,2735,3771,995,5000,2997,1268,1750,5000,2668,2551,8167
|
||||||
|
3280,3971,2390,2816,3017,4527,3921,2787,1271,27,1482,2724,2529,3444,2878,433,4118,1881,2051,4529,2267,4071,4180,4922,5000,3927,2488,4333,1900,2478,4626,3794,196,4226,1400,156,4255,419,4689,118,3966,4514,4570,4831,2129,1623,1413,2271,3962,4717,991,1545,4618,1651,2714,4872,3573,3001,1354,944,2744,4094,5000,50,2865,1322,956,2385,1361,3676,1068,3736,776,3526,2681,3738,964,5000,2958,1288,1770,5000,2687,2566,8221
|
||||||
|
3310,4001,2356,2777,2973,4484,3884,2759,1310,41,1506,2760,2559,3465,2891,445,4140,1856,1994,4473,2646,4052,4152,4880,5000,3933,2513,4357,1918,2496,4648,3827,236,4188,1356,110,4226,380,4639,128,3984,4548,4618,4878,2143,1665,1450,2294,3908,4658,942,1512,4589,1623,2674,4904,3604,3027,1372,959,3183,4124,5000,58,2819,1291,930,2350,1314,3623,1082,3765,785,3526,2690,3769,996,5000,2980,1245,1729,5000,2645,2518,8252
|
||||||
|
3339,4032,2383,2799,2991,4502,3909,2794,1288,0,1469,2733,2528,3423,2841,457,4163,1894,1999,4479,2668,4096,4186,4900,5000,3877,2475,4318,1875,2452,4672,3859,275,4212,1374,126,4259,402,4651,76,3942,4522,4604,4862,2095,1645,1487,2316,3916,4661,957,1540,4622,1657,2695,4875,3573,2990,1328,912,3140,4154,5000,67,2835,1324,965,2376,1328,3631,1034,3733,732,3465,2638,3740,967,5000,3002,1265,1750,5000,2664,2533,8284
|
||||||
|
3307,4000,2349,2759,2947,4459,3935,2828,1328,8,1495,2769,2558,3443,2853,408,4124,1871,1942,4424,2630,4141,3799,4919,5000,3884,2500,4341,1893,2470,4634,3831,253,4174,1329,80,4229,424,4663,87,3962,4559,4652,4908,2109,1687,1461,2276,3861,4602,911,1507,4655,1691,2718,4907,3604,3015,1346,927,3160,4123,4859,14,2790,1296,938,2340,1342,3640,1049,3764,740,3466,2649,3774,999,5000,2963,1222,1709,5000,2621,2547,8310
|
||||||
|
3337,4030,2376,2781,2965,4478,3899,2802,1305,0,1460,2742,2587,3463,2865,421,4149,1911,1947,4852,2655,4125,3769,4876,5000,3829,2463,4302,1912,2488,4658,3865,293,4197,1347,96,4261,383,4613,36,3922,4535,4638,4952,2123,1729,1497,2297,3867,4606,928,1536,4627,1662,2679,4877,3573,2978,1302,1362,3181,4154,4900,23,2809,1331,973,2365,1294,3587,1003,3734,685,3405,2661,3810,1032,5000,2985,1242,1730,5000,2639,2499,8337
|
||||||
|
3304,3998,2341,2803,2983,4498,3926,2837,1345,0,1489,2777,2555,3420,2814,372,4112,1889,1889,4860,2682,4172,3802,4894,5000,3837,2488,4325,1868,2444,4621,3837,270,4159,1364,113,4292,403,4624,47,3944,4573,4687,4934,2074,1709,1471,2255,3811,4549,947,1565,4660,1695,2702,4910,3603,3002,1319,1315,3140,4124,4880,0,2766,1367,1008,2390,1308,3596,1020,3767,691,3407,2613,3785,1002,5000,2945,1199,1689,5000,2657,2514,8365
|
||||||
|
3334,4028,2368,2762,2939,4455,3891,2811,1322,0,1518,2812,2583,3439,2826,386,4138,1930,1893,4808,2648,3739,3771,4849,4964,3845,2513,4347,1886,2463,4647,3872,310,4181,1319,67,4260,361,4574,0,3906,4613,4736,4976,2087,1750,1505,2275,3817,4553,904,1533,4632,1666,2664,4880,3634,3026,1337,1330,3161,3737,4921,0,2786,1343,980,2352,1259,3543,975,3739,697,3408,2628,3824,1035,5000,2967,1219,1711,5000,2613,2467,8394
|
||||||
|
3302,4058,2394,2784,2957,4476,3919,2848,1361,0,1486,2785,2550,3395,2776,339,4165,1971,2318,4818,2677,3788,3802,4866,4970,3791,2477,4308,1842,2420,4611,3845,350,4204,1336,84,4291,381,4585,9,3930,4591,4722,4956,2038,1730,1477,2293,3823,4559,925,1563,4666,1699,2689,4912,3602,2988,1712,1284,3121,3708,4901,0,2808,1381,1014,2376,1271,3552,993,3774,641,3349,2583,3803,1006,5000,2988,1238,1733,5000,2630,2481,8424
|
||||||
|
3332,4026,2359,2743,2913,4434,3885,2884,1400,0,1517,2819,2578,3413,2787,354,4131,1951,2260,4768,2646,3776,3771,4883,4977,3801,2503,4329,1860,2440,4638,3881,327,4165,1291,39,4259,338,4596,22,3955,4632,4771,4996,2051,1771,1511,2249,3766,4502,885,1531,4637,1669,2714,4944,3632,3011,1729,1300,3144,3742,4943,0,2768,1358,986,2337,1222,3562,1012,3809,646,3351,2601,3844,1039,5000,2948,1196,1694,5000,2586,2434,8455
|
||||||
|
3362,4056,2385,2764,2931,4455,3914,2859,1376,0,1487,2792,2543,3369,2799,369,4159,1994,2265,4780,2258,3827,3801,4837,4921,3748,2467,4289,1816,2460,4665,3917,366,4187,1308,56,4288,356,4545,0,3919,4612,4757,4973,2001,1812,1544,2266,3771,4509,908,1561,4671,1700,2678,4915,3599,2972,1683,1254,2747,3777,4984,0,2792,1399,1019,2360,1234,3509,969,3784,588,3293,2558,3826,1073,5000,2969,1216,1716,5000,2603,2449,8487
|
||||||
|
3329,4023,2349,2722,2887,4476,3943,2896,1415,0,1520,2826,2571,3386,2748,323,4127,2395,2207,4731,2292,3879,3831,4852,4927,3759,2494,4311,1834,2418,4631,3892,343,4147,1263,12,4317,374,4555,0,3946,4654,4805,5000,2014,1791,1514,2221,3713,4454,870,1592,4705,1731,2705,4947,3629,3415,1700,1270,2708,3750,4964,0,2754,1378,990,2383,1246,3519,989,3821,592,3297,2579,3870,1044,5000,2929,1173,1677,5000,2620,2465,8513
|
||||||
|
3359,4053,2375,2743,2905,4435,3911,2872,1391,0,1492,2860,2597,3403,2760,340,4157,2439,2212,4746,2265,3869,3798,4805,4871,3708,2458,4332,1852,2438,4660,3928,382,4169,1279,30,4283,330,4504,0,3912,4635,4853,5000,2026,1832,1546,2237,3718,4461,896,1561,4676,1700,2670,4917,3596,3375,1716,1287,2733,3785,5000,0,2780,1420,1023,2342,1195,3467,948,3797,534,3301,2601,3916,1077,5000,2950,1193,1700,5000,2574,2418,8540
|
||||||
|
3327,4020,2400,2763,2923,4457,3941,2909,1428,0,1526,2832,2562,3357,2709,295,4126,2421,2217,4342,2301,3922,3827,4819,4877,3719,2485,4291,1808,2397,4627,3903,358,4190,1296,48,4311,347,4515,0,3941,4679,4838,5000,1976,1811,1515,2190,3660,4470,923,1592,4710,1731,2697,4949,3625,3398,1671,821,2696,3759,4986,0,2807,1463,1055,2364,1207,3478,970,3836,537,3245,2563,3901,1048,5000,2909,1151,1724,5000,2591,2434,8568
|
||||||
|
3357,4049,2363,2722,2879,4417,3910,2885,1403,0,1562,2866,2588,3373,2721,312,4578,2465,2160,4296,2277,3913,3793,4771,4883,3732,2512,4311,1826,2419,4656,3941,396,4150,1250,4,4277,302,4463,0,3971,4723,4885,5000,1988,1851,1545,2204,3665,4417,889,1561,4681,1699,2664,4981,4074,3419,1687,838,2721,3796,5000,0,2773,1444,1025,2323,1156,3427,931,3876,540,3251,2588,3950,1081,5000,2930,1171,1685,5000,2545,2388,8597
|
||||||
|
3387,4079,2388,2742,2898,4440,3941,2923,1440,0,1536,2837,2552,3327,2671,331,4610,2510,2166,4314,2315,3966,3820,4785,4827,3683,2478,4270,1782,2378,4624,3978,434,4171,1267,23,4304,318,4474,0,3940,4706,4870,5000,1938,1829,1575,2218,3669,4427,919,1592,4715,1728,2693,4951,4040,3379,1641,794,2685,3771,5000,0,2802,1489,1057,2343,1167,3438,955,3854,481,3196,2553,3937,1052,5000,2951,1191,1709,5000,2561,2404,8627
|
||||||
|
3355,4046,2351,2700,2854,4401,3973,2961,1476,0,1573,2870,2577,3343,2683,288,4581,2493,1690,4272,2294,3958,3847,4798,4833,3697,2505,4290,1801,2400,4655,3954,409,4130,1222,0,4268,334,4485,0,3972,4751,4916,5000,1950,1869,1542,2169,3611,4376,887,1562,4686,1757,2722,4983,4068,3400,1237,811,2712,3808,5000,0,2770,1472,1026,2301,1178,3450,979,3895,484,3203,2581,3987,1085,5000,2910,1149,1672,5000,2515,2359,8658
|
||||||
|
3385,4075,2376,2719,2873,4425,3943,2937,1450,0,1549,2841,2540,3358,2695,727,4615,2538,1697,4292,2335,4012,3812,4748,4778,3649,2471,4249,1819,2423,4686,3992,446,4150,1238,0,4294,287,4434,0,3943,4734,4899,5000,1962,1909,1570,2182,3615,4387,919,1594,4719,1724,2690,5000,4034,3359,1191,829,2739,3846,5000,0,2801,1517,1057,2321,1126,3400,942,3875,424,3150,2548,4039,1118,5000,2931,1170,1696,5000,2531,2376,8690
|
||||||
|
3353,4041,2338,2677,2892,4448,3975,2975,1485,0,1588,2874,2564,3311,2645,685,4588,2521,1642,4314,2377,4066,3838,4761,4784,3664,2499,4269,1776,2384,4656,3968,420,4109,1193,18,4319,302,4445,0,3976,4780,4944,5000,1912,1886,1536,2132,3558,4338,952,1626,4752,1752,2721,5000,4062,3380,1207,786,2705,3822,5000,0,2771,1501,1087,2340,1137,3413,968,3917,427,3160,2579,4029,1088,5000,2890,1128,1659,5000,2547,2394,8716
|
||||||
|
3383,4070,2363,2697,2849,4411,3946,2951,1458,0,1627,2906,2588,3326,2658,706,4624,2147,1650,4274,2359,4058,3801,4710,4729,3618,2527,4289,1794,2408,4688,4006,455,4129,1209,0,4282,255,4394,0,3949,4826,4988,5000,1924,1925,1563,2144,3562,4351,925,1596,4722,1717,2690,5000,4028,2981,1224,805,2733,3861,5000,0,2804,1547,1055,2297,1085,3364,933,3898,430,3171,2612,4082,1121,5000,2911,1149,1685,5000,2501,2349,8743
|
||||||
|
3350,4098,2387,2716,2868,4435,3979,2990,1492,0,1605,2877,2550,3279,3029,666,4598,2192,1658,4299,2403,4112,3826,4722,4735,3635,2494,4246,1751,2370,4658,3982,490,4149,1226,0,4307,269,4406,0,3984,4811,4969,5000,1873,1901,1527,2093,3567,4366,960,1628,4755,1744,3142,5000,4055,2939,1178,762,2700,3837,5000,0,2839,1594,1085,2315,1096,3377,961,3942,371,3120,2584,4073,1091,5000,2869,1169,1710,5000,2517,2368,8771
|
||||||
|
3380,4065,2348,2673,2825,4399,3951,2966,1525,0,1645,2909,2573,3293,3042,688,4634,2176,1605,4262,2387,4103,3789,4733,4742,3653,2523,4266,1770,2394,4691,4020,463,4107,1181,0,4269,221,4355,0,4020,4857,5000,5000,1885,1940,1553,2103,3510,4319,935,1598,4725,1709,3174,5000,4082,2959,1194,781,2729,3877,5000,0,2812,1579,1052,2271,1044,3329,989,3986,374,3133,2619,4128,1123,5000,2890,1128,1674,5000,2471,2324,8800
|
||||||
|
3410,4093,2372,2693,2844,4424,3985,3004,1496,0,1624,2878,2534,3245,3055,711,4252,2222,1614,4289,2433,4156,3813,4681,4687,3610,2489,4224,1727,2357,4725,4059,496,4127,1197,0,4292,235,4367,0,3995,4842,4990,5000,1835,1977,1578,2113,3515,4335,973,1630,4757,1735,3145,5000,3626,2917,1148,739,2697,3916,5000,0,2848,1626,1081,2289,1054,3344,956,3969,315,3085,2594,4120,1155,5000,2911,1149,1700,5000,2486,2343,8830
|
||||||
|
3378,4059,2333,2650,2802,4450,4019,3042,1528,0,1665,2910,2556,3679,3006,672,4228,2205,1563,4255,2419,4209,3836,4692,4695,3629,2519,4243,1747,2382,4697,4035,467,4084,1152,0,4315,249,4380,0,4033,4889,5000,5000,1847,1953,1540,2061,3458,4291,950,1601,4789,2180,3178,5000,3653,2936,1164,760,2728,3894,5000,0,2823,1612,1048,2306,1064,3359,987,4014,318,3100,2632,4176,1125,5000,2869,1108,1665,5000,2440,2363,8861
|
||||||
|
3408,4087,2357,2669,2822,4414,3992,3017,1498,0,1645,2879,2578,3693,3020,697,4266,2251,1574,4284,2467,4199,3797,4640,4640,3587,2486,4263,1766,2408,4731,4073,500,4104,1169,0,4276,200,4330,0,4009,4873,5000,5000,1858,1990,1564,2070,3463,4309,990,1633,4758,2144,3149,5000,3617,2894,1180,780,2759,3933,5000,0,2861,1659,1076,2261,1013,3312,956,3997,260,3055,2671,4231,1156,5000,2890,1130,1691,5000,2456,2321,8893
|
||||||
|
3375,4052,2318,2688,2842,4441,4027,3055,1529,0,1687,2910,2538,3644,2972,239,4244,2235,1586,4314,2515,4251,3819,4650,4649,3608,2515,4220,1724,2372,4703,4048,469,4061,1186,0,4298,213,4343,0,4048,4920,5000,5000,1808,1965,1525,2016,3407,4328,1031,1666,4789,2168,3183,5000,3643,2913,1135,739,2729,3911,5000,0,2838,1706,1104,2277,1023,3328,987,4043,264,3072,2650,4225,1125,5000,2848,1089,1719,5000,2472,2341,8919
|
||||||
|
3405,4080,2340,2646,2801,4407,4000,3030,1497,0,1730,2941,2980,3658,2987,265,4283,2280,1536,4284,2503,4240,3779,4597,4595,3630,2545,4239,1744,2399,4738,4086,500,4080,1141,0,4258,164,4294,0,4088,4966,5000,5000,1820,2001,1547,2025,3414,4287,1011,1636,5000,2130,3156,4986,3669,2932,1151,761,2761,3952,5000,0,2877,1692,1069,2231,971,3283,958,4089,268,3091,2692,4281,1156,5000,2869,1111,1684,5000,2426,2300,8946
|
||||||
|
3435,4107,2363,2665,2821,4434,4035,3067,1526,0,1710,2909,2939,3609,2939,229,4324,2325,1550,4317,2553,4290,3801,4606,4604,3591,2513,4196,1703,2364,4711,4123,530,4099,1158,0,4279,177,4308,0,4067,4950,5000,4994,1770,1975,1507,2033,3420,4308,1054,1669,5000,2154,3190,5000,3632,2889,1106,721,2732,3930,5000,0,2917,1739,1096,2247,982,3301,991,4074,211,3049,2673,4275,1125,5000,2889,1133,1712,5000,2442,2322,8974
|
||||||
|
3402,4072,2323,2622,2780,4400,4009,3104,1555,0,1753,2939,2959,3623,2535,256,4302,2308,1503,4289,2541,4277,3821,4615,4613,3615,2543,4216,1724,2392,4747,4098,498,4056,1114,0,4237,127,4322,0,4108,4996,5000,5000,1782,2010,1528,1978,3365,4269,1035,1639,5000,2177,2805,5000,3657,2907,1122,744,2765,3970,5000,0,2897,1724,1061,2200,930,3319,1025,4120,216,3070,2717,4331,1155,5000,2848,1093,1678,5000,2397,2282,9003
|
||||||
|
3432,4099,2346,2641,2801,4429,4045,3079,1521,0,1734,3327,2917,3636,2550,284,4343,2352,1518,4324,2592,4326,3780,4562,4560,3577,2511,4173,1683,2420,4783,4135,526,4075,1131,0,4258,140,4275,0,4087,4980,5000,4959,1795,2045,1548,1986,3373,4293,1080,2091,5000,2137,2779,5000,3620,2864,1077,705,2798,4010,5000,0,2938,1770,1087,2215,941,3275,998,4105,160,3031,2700,4386,1184,5000,2868,1115,1706,5000,2413,2304,9033
|
||||||
|
3399,4064,2306,2598,2823,4458,4081,3115,1548,0,1777,3356,2937,3587,2504,250,4323,2334,1473,4298,2643,4373,3800,4571,4570,3603,2542,4192,1704,2387,4757,4110,492,4031,1087,0,4278,152,4290,0,4129,5000,5000,4971,1745,2017,1506,1931,3319,4255,1064,2124,5000,2159,2814,5000,3644,2882,1094,728,2771,3989,5000,0,2919,1754,1113,2230,952,3295,1034,4152,166,3055,2747,4380,1152,5000,2827,1076,1673,4996,2430,2327,9064
|
||||||
|
3429,4091,2328,2617,2783,4425,4055,3089,1513,0,1758,3385,2957,3180,2521,279,4365,2378,1490,4336,2633,4357,3757,4518,4519,3567,2572,4211,1726,2416,4793,4146,519,4050,1104,0,4235,103,4244,0,4110,5000,5000,4982,1757,2051,1525,1938,3328,4281,1110,2094,5000,1699,2788,5000,3607,2900,1111,752,2805,4029,5000,0,2961,1800,1076,2182,901,3253,1008,4138,111,3080,2794,4435,1181,5000,2847,1099,1702,5000,2384,2289,9096
|
||||||
|
3396,4055,2350,2636,2805,4455,4092,3124,1539,0,2221,3352,2914,3131,2476,247,4345,2422,1509,4375,2684,4402,3776,4526,4530,3595,2541,4169,1686,2383,4767,4119,483,4068,1122,0,4254,115,4260,0,4153,5000,5000,4929,1708,2022,1481,1883,3337,4308,1578,2126,5000,1720,2824,5000,3631,2856,1067,715,2778,4007,5000,0,3005,1845,1101,2196,912,3273,1045,4185,119,3044,2780,4428,1148,5000,2806,1122,1731,5000,2401,2313,9122
|
||||||
|
3425,4082,2309,2593,2765,4423,4066,3098,1564,21,2264,3381,2933,3144,2493,277,4388,2403,1466,4352,2674,4384,3732,4472,4541,3623,2572,4188,1708,2413,4803,4155,508,4024,1078,0,4211,65,4214,0,4196,5000,5000,4938,1721,2055,1499,1889,3285,4274,1564,2096,5000,1679,2798,5000,3655,2874,1084,740,2813,4047,5000,0,2987,1828,1064,2148,862,3233,1083,4232,128,3072,2829,4482,1176,5000,2827,1083,1698,4987,2357,2276,9149
|
||||||
|
3455,4108,2331,2613,2788,4453,4103,3132,1526,0,2244,3347,2470,3096,2449,309,4431,2445,1487,4393,2726,4427,3750,4480,4491,3590,2541,4145,1669,2382,4840,4189,533,4043,1097,0,4229,77,4232,0,4178,5000,5000,4884,1672,2026,1516,1896,3296,4303,1612,2128,4806,1700,2835,5000,3616,2830,1040,703,2787,4087,5000,0,3031,1872,1088,2162,873,3255,1059,4217,75,3039,2817,4474,1142,5000,2847,1106,1728,5000,2374,2301,9177
|
||||||
|
3422,4072,2290,2570,2749,4422,4139,3167,1550,454,2287,3375,2488,3109,2467,278,4412,2425,1447,4372,2716,4468,3768,4488,4504,3620,2572,4165,1692,2412,4815,4162,495,3999,1053,0,4185,90,4249,0,4222,5000,5000,4890,1685,2057,1471,1840,3245,4691,1599,2098,4834,1720,2871,5000,3640,2848,1058,729,2823,4065,5000,0,3014,1853,1049,2114,885,3277,1098,4265,85,3069,2868,4527,1169,5000,2806,1068,1696,4960,2330,2327,9206
|
||||||
|
3451,4098,2311,2589,2772,4453,4114,3138,1511,431,2267,3340,2506,3122,2486,311,4455,2466,1469,4415,2767,4446,3723,4434,4455,3589,2541,4122,1715,2444,4851,4196,518,4017,1072,0,4202,40,4206,0,4204,5000,5000,4896,1699,2089,1487,1846,3257,4722,1649,2130,4799,1677,2845,5000,3601,2803,1014,755,2860,4105,5000,0,3059,1896,1073,2127,835,3239,1075,4250,34,3039,2919,4579,1196,5000,2827,1092,1726,4978,2348,2291,9236
|
||||||
|
3418,4062,2271,2609,2796,4485,4151,3172,1533,470,2309,2947,2462,3073,2443,282,4437,2445,1431,4458,2819,4484,3740,4442,4468,3620,2572,4142,1677,2413,4826,4167,478,3973,1091,0,4219,53,4225,0,4248,5000,5000,4839,1650,2058,1441,1790,3207,4693,1699,1741,4826,1697,2882,5000,3624,2821,1033,720,2834,4082,5000,0,3042,1938,1096,2140,848,3263,1115,4297,46,3072,2909,4569,1161,5000,2786,1054,1695,4995,2366,2318,9267
|
||||||
|
3447,4087,2292,2567,2758,4455,4125,3143,1913,447,2350,2974,2480,3087,2463,315,4480,2485,1456,4441,2808,4459,3694,4388,4420,3653,2604,4162,1701,2445,4863,4200,499,3991,1048,0,4174,3,4182,0,4231,5000,5000,4843,1664,2088,1456,1797,3641,4726,1688,1710,4790,1654,2856,5000,3646,2838,1052,748,2872,4121,5000,0,3087,1917,1056,2091,799,3226,1094,4281,58,3106,2961,4619,1187,5000,2806,1078,1726,4950,2323,2284,9299
|
||||||
|
3413,4112,2312,2586,2782,4487,4162,3175,1934,487,2329,2939,2435,3038,2421,287,4524,2524,1482,4486,2858,4494,3710,4397,4435,3624,2573,4119,1663,2415,4837,4170,520,4009,1067,0,4191,16,4203,0,4276,5000,5000,4785,1616,2055,1408,1803,3655,4760,1739,1742,4816,1672,2893,5000,3607,2794,1009,714,2847,4098,5000,0,3133,1957,1078,2103,812,3251,1135,4328,10,3079,2952,4607,1151,5000,2827,1103,1757,4967,2342,2312,9325
|
||||||
|
3442,4076,2271,2544,2745,4457,4137,3207,1954,527,1950,2965,2452,3052,2442,322,4506,2501,1447,4469,2846,4466,3663,4405,4451,3658,2605,4139,1688,2448,4874,4201,478,3965,1025,0,4145,0,4224,0,4321,5000,5000,4788,1631,2085,1422,1747,3608,4734,1308,1711,4779,1629,2929,5000,3629,2811,1028,742,2885,4136,5000,0,3116,1934,1038,2054,763,3277,1177,4374,24,3116,3005,4655,1176,5000,2787,1065,1726,4923,2299,2279,9352
|
||||||
|
3471,4101,2292,2564,2770,4490,4173,3596,1911,506,1927,2929,2407,3003,2464,358,4550,2539,1476,4516,2896,4498,3678,4351,4405,3631,2574,4098,1651,2481,4910,4232,497,3982,1044,0,4161,0,4183,0,4303,5000,5000,4728,1583,2113,1436,2174,3624,4770,1360,1741,4804,1647,2903,5000,3589,2766,986,709,2922,4175,5000,0,3162,1972,1060,2066,777,3242,1157,4358,0,3091,2996,4641,1201,5000,2808,1090,1757,4940,2319,2308,9380
|
||||||
|
3437,4063,2251,2522,2733,4523,4210,3627,1930,546,1967,2954,2423,3017,2424,331,4532,2514,1443,4501,2945,4529,3693,4359,4422,3667,2606,4118,1677,2452,4885,4201,453,3938,1003,0,4176,0,4205,0,4348,5000,5000,4730,1598,2079,1387,2118,3578,4745,1349,1772,4829,1665,2939,5000,3610,2784,1006,738,2898,4150,5000,0,3145,1946,1019,2079,791,3270,1200,4404,0,3130,3049,4687,1164,5000,2767,1054,1727,4895,2339,2337,9409
|
||||||
|
3466,4089,2272,2543,2760,4495,4185,3596,1887,106,1944,2980,2440,3032,2447,369,4576,2551,1474,4549,2932,4496,3646,4306,4378,3643,2576,4139,1704,2486,4921,4231,472,3957,1024,0,4131,0,4167,0,4331,5000,5000,4733,1614,2107,1400,2126,3596,4364,1402,1741,4792,1621,2913,5000,3570,2740,1027,768,2937,4188,5000,0,3191,1983,1040,2029,744,3237,1182,4388,0,3171,3104,4732,1188,5000,2789,1080,1760,4913,2298,2306,9439
|
||||||
|
3433,4052,2293,2564,2786,4530,4641,3626,1906,148,1983,2944,2395,2985,2409,344,4559,2525,1506,4598,2980,4524,3660,4315,4397,3681,2608,4099,1669,2459,4896,4199,428,3975,1045,0,4146,0,4191,0,4377,5000,5000,4673,1569,2073,1771,2071,3553,4404,1455,1771,4817,1639,2950,5000,3592,2758,986,737,2914,4164,5000,0,3237,2018,1061,2042,760,3266,1226,4433,0,3150,3096,4714,1151,5000,2749,1044,1792,4930,2320,2338,9470
|
||||||
|
3462,4077,2252,2524,2752,4502,4616,3593,1862,190,2021,2969,2411,3000,2433,382,4603,2560,1477,4585,2966,4489,3613,4262,4416,3719,2641,4120,1696,2494,4932,4228,445,3932,1004,0,4100,0,4154,0,4422,5000,5000,4675,1585,2100,1783,2079,3573,4383,1446,1740,4779,1595,2924,5000,3614,2776,1008,769,2954,4201,5000,0,3220,1990,1019,1993,714,3235,1208,4478,0,3193,3150,4757,1175,5000,2771,1071,1763,4886,2280,2308,9502
|
||||||
|
3491,4102,2273,2545,2779,4537,4652,3622,1459,170,1996,2932,2366,2954,2396,421,4647,2595,1512,4635,3012,4513,3627,4272,4375,3697,2611,4080,1662,2467,4907,4256,462,3950,1026,0,4115,0,4180,5,4405,5000,5000,4615,1540,2065,1795,2087,3174,4424,1498,1770,4803,1613,2959,5000,3574,2732,968,738,2931,4176,5000,0,3265,2024,1040,2006,731,3266,1253,4461,0,3174,3143,4737,1137,5000,2794,1098,1796,4904,2302,2340,9528
|
||||||
|
3458,4065,2232,2505,2745,4930,4689,3651,1476,212,2033,2957,2382,2969,2422,398,4629,2566,1485,4623,2996,4475,3640,4282,4396,3737,2644,4102,1690,2502,4943,4222,416,3907,986,0,4069,0,4207,46,4449,5000,5000,4617,1558,2511,1745,2034,3134,4405,1489,1738,4764,1631,2995,5000,3595,2750,990,771,2970,4212,5000,0,3248,1993,998,1956,748,3298,1298,4504,0,3219,3197,4776,1160,5000,2754,1063,1768,4860,2263,2311,9555
|
||||||
|
3486,4089,2253,2527,2773,4965,4663,3617,1430,192,2006,2919,2336,2985,2448,438,4673,2599,1521,4673,3041,4496,3592,4229,4355,3716,2614,4062,1719,2537,4978,4249,432,3926,1009,0,4084,0,4172,25,4431,5000,4983,4557,1575,2536,1756,2042,3156,4448,1541,1767,4787,1587,2968,5000,3555,2706,951,803,3010,4248,5000,0,3293,2024,1017,1969,704,3268,1281,4486,0,3203,3188,4815,1183,5000,2777,1091,1801,4878,2287,2345,9583
|
||||||
|
3453,4052,2212,2487,2802,5000,4698,3224,1446,234,2041,2943,2352,2939,2413,416,4654,2569,1497,4723,3084,4516,3605,4240,4378,3758,2646,4085,1686,2511,4951,4214,385,3882,970,10,4099,0,4199,66,4475,5000,4994,4560,1531,2499,1705,1569,3118,4430,1593,1797,4810,1605,3002,5000,3576,2725,974,774,2987,4221,5000,0,3274,1991,1037,1982,722,3301,1327,4528,0,3249,3242,4790,1143,5000,2738,1057,1773,4897,2310,2378,9612
|
||||||
|
3481,4076,2233,2509,3188,4974,4672,3188,1399,213,2075,2967,2368,2956,2440,456,4698,2599,1535,4711,3065,4472,3556,4188,4339,3738,2679,4108,1715,2546,4986,4239,400,3901,993,0,4052,0,4166,46,4457,5000,5000,4562,1970,2523,1716,1579,3142,4475,1583,1764,4770,1560,2975,5000,3534,2743,998,808,3027,4256,5000,7,3318,2019,994,1932,678,3273,1310,4508,0,3296,3295,4825,1166,5000,2760,1085,1806,4853,2272,2351,9642
|
||||||
|
3447,4100,2254,2532,3218,5000,4707,3214,1414,255,2046,2929,2322,2911,2406,435,4678,2629,1574,4762,3106,4489,3568,4199,4363,3781,2649,4069,1683,2520,4959,4202,415,3920,1016,13,4066,0,4195,88,4500,5000,4951,4503,1927,2485,1664,1527,3168,4520,1635,1792,4792,1578,3008,5000,3555,2700,960,779,3004,4228,5000,51,3361,2046,1012,1945,697,3307,1356,4550,0,3282,3285,4796,1126,4992,2721,1113,1840,4871,2297,2385,9673
|
||||||
|
3475,4062,2213,2492,3185,4984,4260,3178,1429,296,2078,2952,2337,2928,2435,476,4721,2596,1552,4750,3084,4442,3518,4210,4388,3824,2681,4092,1713,2556,4993,4227,367,3876,978,0,4019,0,4163,130,4542,5000,4960,4507,1946,2508,1255,1537,3132,4504,1624,1759,4752,1534,3042,5000,3576,2718,984,814,3044,4261,5000,32,3341,2010,968,1896,655,3280,1402,4590,0,3330,3337,4828,1147,5000,2745,1079,1812,4828,2260,2358,9705
|
||||||
|
3503,4086,2233,2935,3215,5000,4294,3203,1381,275,2047,2912,2291,2883,2464,517,4763,2624,1593,4800,3123,4456,3531,4159,4351,3806,2651,4053,1682,2530,5000,4250,381,3895,1002,18,4034,0,4194,111,4522,5000,4906,4869,1903,2530,1265,1548,3159,4551,1675,1787,4773,1552,3013,5000,3534,2675,947,786,3021,4294,5000,76,3382,2034,986,1909,675,3316,1386,4568,0,3318,3326,4796,1168,5000,2768,1108,1846,4847,2286,2814,9731
|
||||||
|
3469,4048,2192,2896,3183,5000,4328,3227,1395,316,2077,2935,2306,2901,2432,497,4743,2589,1573,4788,3098,4468,3542,4170,4377,3851,2683,4077,1712,2566,4998,4211,332,3852,964,0,4048,0,4225,154,4564,5000,4913,4873,1923,2490,1212,1497,3125,4536,1663,1752,4794,1569,3045,5000,3554,2694,972,821,3060,4264,5000,58,3361,1995,942,1922,695,3352,1432,4607,0,3367,3377,4825,1128,4997,2729,1075,1818,4804,2250,2850,9758
|
||||||
|
3497,4071,2213,2920,3213,4609,4300,3188,1347,295,2044,2895,2322,2919,2462,539,4784,2616,1615,4838,3134,4417,3492,4120,4342,3834,2653,4101,1744,2602,5000,4234,346,3871,989,24,4001,0,4195,135,4542,5000,4857,4878,1944,2092,1222,1509,3155,4584,1713,1780,4753,1526,3015,5000,3513,2651,997,856,3099,4295,5000,102,3401,2017,960,1873,655,3327,1416,4584,0,3356,3426,4852,1148,5000,2753,1104,1853,4823,2276,2824,9786
|
||||||
|
3463,4033,2592,2944,3244,4645,4333,3211,1361,335,2072,2917,2275,2876,2431,519,4763,2579,1658,4887,3169,4427,3503,4133,4370,3880,2685,4064,1713,2576,5000,4193,297,3829,1014,58,4015,0,4227,178,4582,5000,5000,4822,1903,2051,1169,1459,3123,4632,1762,1807,4774,1544,3046,5000,3533,2670,961,830,3076,4264,5000,84,3378,2038,977,1886,676,3365,1462,4621,0,3406,3413,4815,1107,4979,2715,1071,1887,4843,2723,2860,9815
|
||||||
|
3490,4056,2613,2905,3213,4619,4304,3171,1312,374,2099,2939,2290,2894,2462,561,4803,2603,1640,4874,3141,4374,3453,4083,4337,3926,2717,4088,1745,2612,5000,4215,310,3848,977,31,3968,0,4198,160,4622,5000,5000,4829,1924,2071,1178,1472,3154,4619,1748,1771,4732,1500,3015,5000,3553,2689,988,866,3115,4294,5000,129,3416,1995,932,1837,637,3341,1446,4658,13,3457,3461,4839,1127,5000,2739,1101,1859,4800,2688,2835,9845
|
||||||
|
3518,4079,2634,2930,2825,4655,4336,3193,1325,351,2064,2898,2244,2852,2432,541,4842,2626,1685,4923,3173,4381,3464,4097,4366,3910,2686,4051,1715,2586,5000,4235,323,3867,1002,66,3982,0,4232,204,4599,5000,5000,4774,1464,2029,1125,1485,3186,4668,1796,1798,4752,1519,3045,5000,3510,2647,952,840,3091,4262,5000,173,3454,2014,949,1851,660,3379,1492,4631,0,3447,3446,4799,1086,4962,2763,1131,1894,4820,2716,2872,9876
|
||||||
|
3483,4460,2593,2892,2794,4629,4306,3215,1338,390,2089,2919,2259,2871,2464,584,4819,2587,1668,4909,3141,4325,3475,4110,4396,3957,2718,4077,1747,2622,5000,4193,274,3825,966,39,3935,0,4267,247,4637,5000,5000,4783,1486,2049,1134,1437,3156,4656,1781,1762,4710,1537,3074,5000,3530,2666,979,876,3129,4291,5000,155,3428,1969,903,1802,621,3418,1538,4666,22,3499,3492,4819,1106,4984,2725,1099,1866,5000,2683,2847,9908
|
||||||
|
3510,4483,2614,2917,2826,4665,4338,3173,1290,366,2051,2878,2212,2892,2497,627,4858,2608,1713,4957,3171,4330,3424,4062,4364,3942,2687,4040,1718,2657,5000,4213,287,3845,993,74,3949,0,4240,229,4612,5000,5000,4730,1508,2068,1143,1451,3190,4706,1827,1788,4730,1494,3041,5000,3488,2624,945,851,3167,4319,5000,200,3464,1985,919,1816,645,3395,1521,4638,0,3489,3475,4838,1125,5000,2750,1129,1900,5000,2711,2885,9934
|
||||||
|
3475,4444,2574,2460,2858,4701,4369,3193,1303,403,2074,2899,2228,2850,2469,607,4834,2567,1698,4942,3198,4334,3435,4076,4396,3989,2718,4066,1750,2631,5000,4169,238,3802,957,110,3964,0,4275,273,4648,5000,5000,4321,1469,2024,1090,1405,3163,4694,1810,1813,4749,1514,3070,5000,3507,2644,973,887,3143,4284,5000,182,3436,1938,935,1830,670,3435,1567,4671,34,3541,3518,4793,1083,4966,2712,1097,1873,5000,2741,2503,9961
|
||||||
|
3922,4467,2595,2485,2829,4675,4338,3151,1254,378,2034,2919,2243,2871,2503,650,4871,2586,1744,4988,3163,4275,3384,4029,4366,3975,2749,4092,1783,2666,5000,4188,251,3822,984,84,3917,0,4250,254,5000,5000,5000,4333,1493,2042,1099,1421,3198,4744,1854,1776,4707,1471,3036,5000,3465,2665,1001,924,3181,4311,5000,226,3470,1952,889,1783,633,3414,1550,4641,11,3593,3561,4808,1103,4989,2737,1128,2327,5000,2708,2478,9989
|
||||||
|
3887,4428,2616,2511,2861,4711,4368,3170,1267,414,2056,2877,2196,2830,2475,631,4845,2605,1791,5000,3187,4277,3394,4045,4399,4023,2718,4057,1755,2640,5000,4144,202,3842,1012,120,3932,0,4286,298,5000,5000,5000,4283,1455,1998,1046,1375,3234,4794,1898,1801,4726,1491,3063,5000,3484,2623,968,900,3156,4275,5000,207,3502,1965,905,1797,659,3455,1594,4673,51,3583,3540,4760,1061,4949,2700,1159,2361,5000,2738,2516,10018
|
||||||
|
3913,4450,2156,2474,2832,4685,4335,3127,1280,450,2076,2897,2212,2852,2510,674,4881,2561,1777,5000,3149,4216,3343,3999,4433,4071,2749,4084,1788,2675,5000,4161,216,3801,977,94,3885,0,4261,341,5000,5000,4714,4297,1479,2015,1055,1392,3209,4783,1878,1764,4683,1449,3027,5000,3504,2644,997,937,3192,4300,5000,251,3471,1915,858,1750,623,3434,1639,4703,92,3635,3580,4772,1080,4971,2726,1128,2334,5000,2287,2493,10048
|
||||||
|
3940,4473,2177,2500,2865,4720,4364,3145,1231,423,2033,2854,2165,2813,2484,717,4916,2578,1824,5000,3171,4217,3353,4015,4405,4058,2718,4049,1760,2648,5000,4178,230,3821,1005,131,3901,0,4299,743,5000,5000,4656,4251,1442,1969,1064,1410,3247,4833,1919,1788,4702,1470,3053,5000,3461,2603,964,913,3167,4325,5000,294,3502,1926,873,1765,650,3476,1621,4670,71,3625,3557,4721,1038,4994,2751,1579,2368,5000,2318,2531,10079
|
||||||
|
3904,4433,2137,2465,2837,4694,4393,3163,1245,457,2052,2874,2181,2835,2520,697,4889,2532,1810,5000,3130,4218,3364,4032,4440,4106,2748,4076,1794,2682,5000,4133,182,3780,971,105,3854,0,4337,786,5000,5000,4660,4267,1467,1985,1011,1367,3224,4822,1898,1750,4722,1491,3078,5000,3480,2624,994,950,3203,4287,5000,275,3469,1874,827,1718,678,3517,1665,4698,114,3677,3594,4731,1057,4954,2715,1548,2340,5000,2287,2569,10111
|
||||||
|
3930,4036,2159,2492,2870,4729,4359,3118,1196,428,2007,2831,2197,2858,2556,740,4922,2548,1858,5000,3149,4155,3313,3987,4413,4093,2716,4042,1828,2717,5000,4149,197,3801,1000,142,3870,0,4314,766,5000,5000,4603,4285,1493,2001,1020,1386,3264,4872,1937,1773,4679,1451,3041,5000,3437,2584,962,988,3238,4310,5000,317,3497,1884,841,1734,644,3498,1646,4663,95,3666,3630,4739,1077,4977,2740,1580,2374,4764,2319,2545,10137
|
||||||
|
3895,3996,2118,2518,2904,4764,4387,3135,1210,461,2024,2850,2151,2820,2531,720,4893,2500,1845,5000,3167,4155,3323,4005,4450,4142,2746,4070,1800,2689,5000,4103,149,3760,1029,179,3886,0,4773,809,5000,5000,4607,4243,1457,1954,967,1345,3242,4861,1975,1796,4698,1472,3065,5000,3457,2606,993,964,3211,4271,5000,297,3462,1893,856,1750,673,3540,1689,4690,139,3717,3603,4684,1034,4937,3124,1550,2346,4787,2351,2584,10164
|
||||||
|
3921,4018,2140,2484,2876,4736,4352,3090,1162,430,2040,2868,2167,2844,2568,763,4925,2514,1893,5000,3121,4092,3272,3962,4425,4190,2776,4098,1834,2723,5000,4119,165,3781,996,154,3840,0,4751,789,5000,5000,4613,4264,1484,1969,977,1366,3283,4911,1950,1757,4655,1433,3027,5000,3476,2628,1024,1002,3246,4293,5000,338,3488,1839,809,1704,640,3521,1669,4653,184,3768,3636,4690,1054,4960,3150,1582,2380,4748,2322,2560,10192
|
||||||
|
3464,4040,2163,2511,2910,4771,4378,3106,1177,461,1994,2825,2121,2807,2544,743,4957,2527,1941,5000,3136,4091,3283,3981,4462,4177,2744,4065,1807,2694,5000,4072,180,3802,1026,191,3856,0,4790,831,4688,5000,4556,4224,1449,1922,924,1387,3324,4961,1986,1780,4674,1455,3050,5000,3433,2588,993,978,3218,4252,5000,379,3513,1846,824,1721,670,3564,1711,4678,167,3757,3605,4632,1012,4921,3177,1614,1994,4772,2355,2598,10221
|
||||||
|
3490,4000,2123,2477,2882,4743,4343,3122,1192,491,2008,2843,2137,2832,2582,785,4925,2478,1928,5000,3088,4027,3231,4001,4501,4226,2773,4094,1841,2727,5000,4087,135,3762,993,166,3811,189,4831,872,4715,5000,4562,4247,1477,1936,934,1348,3305,4949,1958,1741,4631,1416,3072,5000,3452,2610,1025,1016,3252,4273,5000,357,3475,1792,776,1676,639,3608,1753,4701,213,3806,3636,4636,1032,5000,3141,1584,1965,4734,2326,2574,10251
|
||||||
|
3516,4023,2145,2505,2916,4777,4368,3076,1145,458,1960,2799,2092,2796,2620,826,4955,2490,1976,5000,3101,4026,3242,3959,4478,4212,2740,4061,1814,2760,5000,4102,151,3784,1024,204,3828,217,4809,851,4680,5000,4507,4210,1443,1950,944,1371,3348,4998,1992,1763,4650,1440,3032,5000,3410,2571,995,992,3285,4293,5000,397,3498,1798,791,1693,670,3589,1731,4661,198,3794,3602,4576,1052,5000,3168,1616,1999,4758,2360,2612,10282
|
||||||
|
3479,3983,2106,2471,2888,4811,4393,3091,1161,487,1973,2817,2109,2822,2597,806,4922,2439,1962,5000,3112,4025,3253,3980,4517,4261,2769,4091,1848,2731,5000,4055,107,3744,992,179,3846,246,4850,472,4705,5000,4514,4237,1471,1902,893,1333,3329,4986,1962,1785,4669,1464,3053,5000,3429,2595,1027,1030,3256,4251,4987,374,3457,1742,743,1711,702,3633,1772,4683,245,3842,3630,4578,1010,5000,3133,1166,1970,4721,2394,2650,10314
|
||||||
|
3505,4005,2129,2500,2923,4783,4356,3045,1115,452,1924,2835,2125,2848,2636,847,4950,2450,2010,5000,3061,3962,3202,3940,4495,4247,2736,4120,1883,2763,5000,4070,125,3766,1023,216,4221,213,4829,450,4668,5000,4522,4265,1500,1915,903,1358,3373,5000,1993,1745,4627,1427,3012,5000,3386,2556,1060,1068,3288,4270,4998,412,3478,1748,757,1667,672,3615,1749,4642,231,3890,3655,4578,1451,5000,3160,1199,2004,4746,2366,2626,10340
|
||||||
|
3468,3964,2152,2529,2957,4816,4380,3060,1131,479,1936,2791,2081,2813,2613,826,4915,2398,2057,5000,3070,3961,3214,3962,4536,4295,2764,4088,1856,2733,5000,4022,81,3788,1054,254,4239,243,4870,490,4691,5000,4469,4232,1468,1866,852,1321,3356,5000,2023,1766,4646,1452,3033,5000,3406,2580,1031,1044,3258,4226,4946,387,3498,1754,772,1685,705,3659,1788,4661,279,3876,3618,4516,1409,5000,3125,1170,2037,4772,2401,2664,10367
|
||||||
|
3493,3986,2113,2496,2930,4787,4341,3013,1086,505,1948,2809,2098,2841,2653,866,4942,2408,2043,5000,3016,3899,3163,3922,4577,4343,2793,4119,1891,2764,5000,4037,101,3749,1023,229,4196,212,4430,467,4714,5000,4479,4263,1498,1879,864,1348,3401,5000,1989,1726,4604,1416,2990,5000,3425,2604,1064,1082,3289,4244,4957,424,3454,1697,724,1642,676,3641,1765,4680,328,3922,3641,4515,1430,5000,2733,1202,2008,4735,2374,2639,10395
|
||||||
|
3518,4008,2136,2525,2965,4820,4364,3028,1104,468,1897,2764,2054,2807,2630,906,4967,2417,2090,5000,3024,3899,3175,3946,4556,4329,2759,4087,1863,2733,4998,4051,121,3772,1055,687,4214,243,4471,505,4673,4977,4427,4233,1466,1829,875,1375,3447,5000,2016,1747,4624,1442,3010,5000,3383,2566,1036,1057,3258,4199,4968,460,3472,1703,738,1661,710,3685,1802,4636,315,3906,3600,4872,1390,5000,2761,1235,2040,4762,2409,2676,10424
|
||||||
|
3481,3968,2097,2493,2937,4790,4387,3042,1122,492,1908,2781,2071,2836,2670,884,4930,2364,2074,5000,2968,3838,3186,3970,4598,4376,2786,4118,1898,2763,5000,4004,79,3733,1025,662,4171,274,4513,543,4694,4983,4438,4267,1497,1841,825,1341,3431,5000,1980,1706,4582,1468,3029,5000,3402,2591,1070,1095,3288,4216,4917,433,3426,1646,690,1619,745,3729,1839,4653,364,3951,3620,4870,1411,5000,2727,1206,2011,4726,2382,2651,10454
|
||||||
|
3506,3990,2121,2523,2972,4822,4347,2995,1079,453,1856,2737,2027,2865,2711,923,4954,2372,2120,5000,2973,3839,3136,3932,4579,4361,2752,4087,1932,2793,5000,4018,101,3756,1057,700,4191,0,4493,518,4652,4928,4388,4240,1528,1853,838,1370,3477,5000,2005,1727,4602,1433,2985,5000,3360,2554,1042,1132,3317,4232,4929,467,3442,1651,705,1639,717,3712,1814,4607,352,3932,3577,4867,1433,4943,2755,1239,2043,4753,2418,2688,10485
|
||||||
|
3468,3950,2083,2491,3007,4853,4368,3009,1098,476,1867,2754,2046,2832,2689,900,4915,2318,2104,5000,2978,3841,3149,3958,4622,4408,2779,4119,1905,2761,4996,3971,61,3718,1448,737,4211,0,4535,555,4671,4934,4402,4276,1498,1803,788,1337,3462,5000,2029,1748,4622,1461,3004,5000,3380,2580,1076,1108,3284,4186,4878,438,3395,1595,719,1660,753,3756,1849,4622,401,3975,4014,4803,1393,4904,2721,1210,2013,4781,2454,2725,10517
|
||||||
|
3493,3972,2107,2521,2980,4822,4328,2962,1056,435,1877,2771,2064,2862,2730,938,4938,2326,2149,5000,2920,3782,3099,3922,4604,4392,2806,4150,1939,2790,5000,3986,84,3742,1480,712,4169,0,4515,528,4627,4940,4416,4314,1530,1814,802,1368,3509,5000,1989,1707,4581,1427,2959,4998,3338,2605,1111,1145,3313,4201,4890,471,3409,1600,671,1619,727,3738,1822,4575,451,4017,4029,4799,1415,4928,2749,1243,2045,4746,2428,2699,10543
|
||||||
|
3456,3994,2131,2552,3015,4853,4348,2977,1077,456,1825,2726,2021,2831,2709,914,4897,2334,2193,5000,2924,3786,3112,3948,4647,4438,2771,4120,1912,2757,4972,3939,107,3766,1513,750,3770,0,4557,564,4644,4884,4369,4290,1501,1764,753,1337,3557,5000,2010,1727,4602,1456,2977,5000,3358,2569,1084,1120,3278,4154,4841,502,3422,1606,686,1640,763,3782,1856,4588,439,3996,3982,4734,956,4890,2716,1276,2077,4775,2464,2734,10586
|
||||||
|
3480,3954,2093,2520,2987,4822,4307,2929,1098,476,1834,2743,2041,2861,2749,951,4918,2279,2175,5000,2864,3729,3063,3975,4692,4484,2798,4152,1946,2785,4990,3955,70,4149,1484,725,3729,0,4537,598,4661,4890,4386,4331,1534,1775,768,1369,3543,5000,1968,1686,4561,1423,2994,5000,3378,2596,1119,1156,3306,4169,4853,471,3373,1551,638,1600,738,3764,1889,4601,489,4455,3995,4731,979,4914,2745,1248,2046,4741,2439,2708,10630
|
||||||
|
3504,3976,2118,2552,3022,4852,4327,2944,1058,433,1782,2697,1998,2831,2790,988,4938,2286,2218,5000,2867,3734,3076,3941,4674,4467,2762,4122,1918,2751,5000,3970,95,4173,1518,762,3751,0,4579,570,4614,4834,4341,4310,1505,1786,782,1402,3591,5000,1986,1706,4583,1452,2949,5000,3336,2560,1092,1131,3270,4183,4866,501,3384,1557,653,1622,775,3808,1860,4552,477,4432,3945,4666,1002,4938,2774,1281,2077,4770,2475,2743,10675
|
||||||
|
3466,3936,2081,2521,2995,4881,4346,2959,1081,452,1792,2714,2018,2863,2770,962,4896,2231,2198,5000,2806,3741,3089,3969,4719,4511,2788,4154,1952,2778,4964,3924,59,4136,1490,317,3773,0,4621,603,4629,4840,4360,4352,1539,1735,735,1373,3577,5000,1942,1664,4605,1482,2966,5000,3356,2587,1128,1167,3296,4135,4818,468,3333,1502,605,1645,813,3851,1891,4563,527,4469,3955,4243,964,4900,2741,1252,2046,4737,2450,2777,10721
|
||||||
|
3490,3957,2106,2552,3029,4848,4303,2912,1042,408,1740,2669,2038,2895,2811,997,4914,2238,2239,5000,2808,3687,3041,3936,4702,4493,2752,4187,1985,2805,4982,3940,506,4161,1524,354,3733,0,4601,573,4582,4785,4318,4396,1573,1746,751,1407,3625,5000,1958,1685,4565,1451,2921,4980,3315,2553,1163,1203,3322,4149,4831,496,3344,1510,620,1607,789,3833,1860,4512,935,4443,3965,4240,988,4925,2770,1285,2076,4767,2487,2749,10768
|
||||||
|
3452,3918,2069,2584,3064,4877,4322,2927,1066,425,1749,2686,1997,2865,2790,971,4870,2183,2280,5000,2809,3697,3055,3966,4748,4537,2777,4157,1957,2769,4937,3895,472,4125,1558,390,3756,0,4642,604,4595,4792,4339,4379,1546,1694,705,1380,3611,5000,1973,1705,4588,1482,2937,5000,3335,2580,1137,1177,3285,4100,4784,460,3292,1518,635,1630,828,3876,1890,4522,985,4478,3911,4175,951,4887,2738,1257,2107,4797,2523,2783,10816
|
||||||
|
3476,3940,2094,2554,3036,4844,4278,2880,1029,441,1760,2702,2017,2899,2831,1005,4888,2190,2258,5000,2748,3646,3008,3934,4731,4580,2803,4190,1990,2795,4954,3912,500,4151,1110,365,3718,0,4622,573,4608,4799,4362,4424,1581,1705,722,1416,3660,5000,1925,1663,4549,1451,2892,4960,3356,2608,1173,1212,3309,4113,4799,486,3301,1465,588,1593,805,3857,1858,4532,1034,4512,3498,4173,976,4912,2767,1290,2075,4765,2498,2754,10858
|
||||||
|
3500,3962,2120,2586,3070,4872,4297,2896,1054,395,1708,2657,1977,2870,2810,977,4904,2197,2297,5000,2749,3658,3023,3965,4777,4560,2766,4161,1961,2758,4909,4349,529,4177,1144,401,3741,0,4663,603,4558,4744,4323,4409,1554,1654,677,1452,3708,5000,1938,1683,4572,1483,2908,4981,3315,2575,1147,1185,3271,4064,4814,511,3310,1475,603,1618,844,3899,1886,4899,1021,4483,3443,4109,939,4874,2797,1323,2105,4796,2535,2787,10901
|
||||||
|
3462,3922,2084,2556,3042,4837,4252,2912,1081,410,1719,2674,1998,2904,2851,1010,4858,2143,2273,5000,2688,3609,3038,3997,4823,4601,2791,4194,1994,2783,4925,4305,497,4141,1117,375,3704,0,4704,631,4570,4752,4348,4456,1590,1665,695,1427,3695,5000,1888,1642,4534,1515,2924,5000,3336,2603,1184,1220,3294,4077,4768,474,3256,1423,556,1581,822,3942,1913,4908,1070,4514,3448,4108,965,4899,2765,1294,2072,4765,2510,2757,10945
|
||||||
|
3485,3944,2110,2588,3076,4864,4270,2866,1046,363,1668,2628,1958,2938,2892,1043,4873,2150,2310,5000,2689,3624,2992,3967,4808,4580,2753,4165,1965,2808,4942,4323,528,3747,1152,411,3729,0,4682,598,4519,4699,4312,4442,1626,1675,714,1465,3743,5000,1899,1662,4558,1486,2878,4960,3296,2570,1158,1192,3316,4089,4785,497,3264,1435,572,1607,862,3922,1878,4854,1057,4062,3390,4108,991,4924,2795,1327,2101,4796,2547,2789,10990
|
||||||
|
3447,3905,2074,2559,3110,4891,4287,2882,1073,377,1679,2645,1980,2911,2871,1012,4826,2095,2284,5000,2690,3640,3008,3999,4854,4620,2778,4199,1997,2770,5000,4280,498,3712,1125,447,3754,0,4723,625,4529,4707,4340,4491,1600,1624,671,1441,3729,5000,1847,1682,4582,1519,2895,4981,3317,2600,1194,1226,3276,4040,4740,457,3210,1385,587,1633,902,3964,2324,4861,1104,4090,3394,4046,956,4887,2764,1298,2068,4766,2584,2821,11036
|
||||||
|
3470,3927,2101,2592,3082,4855,4242,2837,1040,329,1629,2662,2003,2946,2912,1044,4840,2103,2319,5000,2630,3596,2962,3971,4839,4598,2802,4232,2029,2793,5000,4299,530,3739,1160,420,3718,0,4701,589,4477,4655,4368,4540,1637,1635,691,1480,3778,5000,1855,1641,4545,1490,2849,4939,3277,2629,1231,1260,3298,4052,4758,478,3218,1398,541,1598,880,3943,2287,4806,1090,4118,3396,4047,983,4912,2794,1331,2097,4798,2559,2789,11083
|
||||||
|
3432,3887,2127,2624,3115,4881,4259,2854,1069,342,1641,2617,1964,2920,2891,1012,4791,2111,2353,5000,2631,3616,2979,4005,4885,4637,2764,4203,1999,2754,5000,4257,81,3766,1196,456,3744,0,4741,615,4487,4665,4336,4528,1612,1584,649,1458,3825,5000,1864,1661,4570,1524,2866,4960,3299,2597,1205,1231,3257,4003,4715,437,3225,1412,557,1625,921,3984,2311,4813,717,4082,3337,3987,949,4876,2763,1364,2125,4830,2595,2820,11131
|
||||||
|
3455,3910,2092,2595,3086,4844,4214,2810,1099,355,1654,2634,1987,2955,2931,1042,4804,2058,2324,5000,2571,3575,2934,3977,4932,4675,2788,4237,2030,3197,5000,4278,115,3732,1169,429,3709,0,4718,640,4496,4676,4368,4578,1650,1594,670,1498,3811,5000,1809,1620,4534,1496,2820,4980,3321,2627,1242,1264,3277,4015,4734,456,3170,1365,511,1591,900,4382,2335,4820,763,4107,3338,3991,977,4901,2793,1335,2091,4801,2570,2787,11173
|
||||||
|
3479,3933,2120,2628,3119,4869,4230,2828,1067,306,1606,2589,1949,2930,2910,1071,4817,2067,2356,5000,2574,3598,2951,4012,4917,4650,2749,4208,1999,3157,5000,4299,149,3760,1205,463,3737,6,4757,602,4443,4625,4338,4567,1625,1543,692,1539,3859,5000,1815,1641,4560,1530,2837,4939,3281,2596,1217,1234,3235,4027,4755,475,3178,1381,528,1619,942,4422,2295,4764,747,4068,3277,3933,943,4927,2824,1368,2119,4834,2607,2816,11216
|
||||||
|
3440,3893,2085,2599,3090,4832,4247,2847,1098,318,1620,2606,1973,2966,2950,1038,4767,2014,2325,5000,2515,3622,2970,4048,4963,4686,2773,4242,2030,3179,5000,3839,122,3726,1178,436,3702,46,4795,625,4452,4638,4372,4618,1663,1555,652,1518,3844,5000,1759,1600,4587,1565,2854,4959,3304,2627,1253,1266,3255,3978,4713,431,3123,1337,482,1586,983,4462,2317,4350,792,4090,3277,3938,972,4891,2793,1339,2084,4805,2581,2845,11260
|
||||||
|
3463,3916,2113,2633,3123,4856,4201,2804,1068,268,1573,2561,1998,3003,2990,1066,4778,2024,2354,5000,2519,3586,2926,4022,4948,4660,2734,4213,2481,3201,5000,3861,158,3754,1214,470,3731,23,4771,586,4398,4589,4345,4669,1702,1566,675,1560,3891,5000,1764,1621,4551,1538,2809,4917,3264,2596,1228,1297,3274,3990,4735,448,3130,1355,499,1615,1383,4439,2276,4294,774,4049,3277,3944,1001,4917,2824,1371,2111,4838,2617,2811,11305
|
||||||
|
3424,3877,2078,2666,3155,4880,4217,2823,1101,280,1589,2578,1961,2978,2967,1031,4727,1972,2321,5000,2523,3614,2945,4059,4994,4695,2757,4247,2449,3160,5000,3822,132,3721,1250,504,3760,63,4809,608,4407,4603,4381,4658,1678,1515,637,1541,3875,5000,1768,1642,4579,1573,2827,4938,3287,2627,1265,1267,3231,3941,4695,402,3076,1375,516,1645,1425,4478,2296,4299,818,4069,3215,3890,968,4881,2793,1342,2076,4872,2653,2839,11351
|
||||||
|
3447,3900,2106,2637,3125,4841,4171,2781,1072,230,1605,2595,1986,3015,3007,1057,4738,1983,2349,4963,2466,3581,2902,4034,4979,4729,2780,4280,2479,3181,4839,3845,169,3750,1224,475,3727,41,4784,566,4353,4618,4418,4710,1717,1527,661,1583,3921,5000,1710,1601,4544,1546,2783,4896,3311,2659,1301,1297,3249,3954,4719,418,3083,1334,472,1613,1405,4454,1834,4243,861,4087,3214,3899,998,4907,2825,1374,2102,4844,2627,2804,11398
|
||||||
|
3408,3923,2135,2671,3157,4864,4187,2801,1106,241,1560,2551,1950,2991,2984,1021,4748,1994,2375,4969,2472,3612,2922,4071,5000,4700,2740,4671,2446,3139,4793,3808,207,3779,1260,508,3757,81,4821,587,4361,4571,4394,4699,1694,1477,624,1627,3967,5000,1713,1623,4572,1581,2801,4916,3272,2628,1276,1265,3204,3905,4681,433,3091,1356,489,2064,1447,4491,1853,4248,841,4043,3151,3848,966,4871,2856,1407,2128,4878,2663,2830,11446
|
||||||
|
3431,3885,2101,2642,3126,4825,4141,2822,1141,253,1579,2568,1976,3029,3023,1046,4696,1944,2338,4912,2416,3583,2881,4110,5000,4732,2763,4705,2475,3159,4810,3833,183,3746,1234,479,3725,59,4857,606,4368,4588,4433,4750,1733,1489,649,1608,3950,5000,1653,1583,4539,1555,2819,4937,3296,2661,1312,1295,3222,3918,4705,385,3037,1317,445,2033,1427,4528,1870,4254,882,4059,3150,3860,997,4898,2826,1377,2092,4850,2636,2794,11488
|
||||||
|
3454,3908,2130,2676,3158,4847,4157,2782,1114,202,1536,2524,1941,3005,3061,1071,4706,1956,2363,4916,2424,3617,2901,4086,5000,4701,2723,4676,2442,2759,4826,3858,221,3776,1270,512,3756,100,4830,563,4314,4544,4412,4739,1711,1501,676,1653,3994,5000,1655,1605,4568,1590,2776,4895,3258,2631,1286,1262,3239,3932,4731,399,3046,1342,464,2065,1469,4082,1826,4197,860,4012,3087,3811,1028,4925,2857,1409,2118,4884,2671,2819,11531
|
||||||
|
3415,3870,2097,2648,3126,4869,4173,2804,1149,213,1555,2542,1968,3043,3038,1032,4654,1908,2324,4858,2433,3652,2923,4125,5000,4732,3165,4709,2470,2716,4781,3822,198,3744,1244,482,3787,140,4865,580,4322,4563,4453,4790,1751,1452,641,1635,3976,5000,1595,1627,4597,1626,2796,4915,3282,2664,1323,1291,3193,3884,4695,349,2993,1305,840,2097,1512,4118,1842,4203,899,4025,3086,3826,997,4890,2827,1379,2081,4857,2706,2844,11575
|
||||||
|
3438,3893,2126,2681,3157,4828,4127,2765,1124,163,1514,2560,1995,3082,3075,1055,4663,1922,2346,4862,2381,3628,2883,4103,5000,4700,3125,4743,2498,2735,4799,3849,237,3774,1280,514,3757,118,4837,536,4268,4520,4496,4840,1790,1464,668,1680,4020,5000,1597,1588,4565,1599,2753,4874,3244,2635,1359,1319,3209,3898,4723,362,3002,1332,859,2067,1492,4091,1796,4146,876,4038,3086,3842,1029,4917,2859,1410,2106,4892,2679,2806,11620
|
||||||
|
3399,3855,2155,2715,3187,4849,4144,2788,1160,175,1536,2516,1961,3058,3051,1016,4610,1874,2367,4866,2391,3666,2905,4143,5000,4728,3147,4714,2043,2692,4754,3815,214,3804,1316,545,3789,159,4871,552,4277,4541,4477,4828,1768,1415,635,1663,4001,5000,1598,1610,4595,1635,2774,4894,3269,2668,1333,1285,3163,3850,4689,312,3012,1361,878,2100,1115,4126,1811,4152,913,3988,3023,3798,999,4882,2828,1380,2131,4927,2713,2830,11666
|
||||||
|
3422,3879,2123,2686,3156,4808,4098,2750,1135,186,1559,2535,1989,3097,3088,1037,4619,1890,2325,4807,2341,3644,2866,4121,5000,5000,3169,4747,2070,2711,4772,3843,254,3773,1290,515,3759,137,4842,506,4285,4563,4521,4877,1809,1429,664,1708,4043,5000,1537,1571,4563,1609,2732,4915,3294,2701,1369,1312,3179,3865,4717,323,2961,1748,836,2072,1095,4098,1763,4158,949,3998,3023,3818,1031,4909,2860,1411,2093,4900,2685,2791,11713
|
||||||
|
3444,3903,2152,2720,3185,4829,4114,2774,1173,136,1520,2491,1956,3074,3062,1058,4628,1906,2344,4810,2355,3686,2889,4162,5000,5000,3129,4717,2035,2667,4728,3872,294,3804,1327,545,3792,178,4874,522,4232,4524,4504,4864,1787,1381,693,1754,4085,5000,1537,1595,4594,1645,2754,4873,3257,2672,1342,1277,3132,3818,4747,334,2972,1779,856,2105,1137,4131,1777,4102,923,3946,2961,3777,1001,4936,2892,1442,2117,4935,2719,2813,11761
|
||||||
|
3405,3865,2120,2691,3153,4787,4131,2799,1211,149,1545,2510,1984,3114,3098,1017,4575,1861,2300,4751,2307,3667,2913,4204,5000,5000,3150,4330,2062,2685,4747,3840,272,3773,1301,514,3764,218,4906,536,4240,4548,4549,4912,1827,1395,662,1738,4063,5000,1476,1556,4563,1680,2775,4894,3282,2706,1378,1304,3147,3834,4716,282,2922,1749,814,1658,1180,4164,1790,4108,957,3954,2961,3800,1034,4902,2862,1411,2079,4909,2690,2773,11803
|
||||||
|
3428,3890,2150,2725,3182,4807,4085,2763,1188,99,1509,2467,1952,3153,3134,1036,4584,1879,2317,4754,2323,3711,2876,4183,5000,5000,3110,4301,2088,2703,4766,3870,312,3804,1337,544,3797,196,4875,488,4188,4511,4533,4897,1868,1409,693,1784,4104,5000,1477,1580,4595,1654,2736,4853,3246,2678,1351,1329,3162,3850,4747,292,3354,1782,835,1692,1160,4134,1741,4053,928,3900,2901,3825,1067,4930,2894,1442,2102,4944,2723,2795,11846
|
||||||
|
3389,3852,2118,2696,3210,4826,4102,2789,1226,112,1535,2486,1982,3130,3107,994,4531,1836,2271,4757,2340,3757,2900,4225,5000,5000,3131,4333,2051,2659,4723,3839,290,3774,1311,573,3831,236,4905,502,4197,4537,4580,4944,1847,1362,662,1768,4081,5000,1478,1604,4626,1689,2759,4873,3272,2712,1386,1293,3115,3805,4717,240,3306,1755,856,1727,1202,4165,1753,4060,960,3907,2902,3789,1038,4896,2864,1411,2063,4980,2756,2816,11890
|
||||||
|
3411,3877,2149,2730,3177,4783,4058,2754,1203,64,1563,2505,2012,3170,3142,1012,4540,1856,2286,4699,2296,3741,2864,4205,5000,5000,2732,4365,2076,2677,4743,3871,331,3806,1347,540,3804,214,4873,453,4145,4565,4628,4989,1888,1377,694,1814,4119,5000,1416,1567,4596,1662,2720,4832,3236,2746,1421,1318,3129,3822,4749,250,3320,1790,395,1701,1182,4133,1703,4006,991,3913,2904,3817,1071,4924,2896,1441,2086,4953,2726,2774,11935
|
||||||
|
3372,3902,2179,2763,3205,4802,4075,2782,1243,77,1530,2463,1980,3147,3114,968,4486,1876,2300,4702,2316,3789,2890,4668,5000,5000,2691,4335,2039,2632,4701,3841,371,3838,1382,569,3839,254,4902,466,4156,4531,4613,4972,1866,1331,665,1799,4157,5000,1417,1592,4629,1697,2744,4853,3263,2719,1393,1280,3081,3778,4721,679,3335,1827,417,1737,1224,4163,1714,4014,959,3857,2846,3785,1043,4890,2867,1471,2109,4989,2758,2795,11981
|
||||||
|
3395,3865,2148,2734,3170,4759,4031,2748,1283,91,1559,2483,2011,3187,3148,985,4496,1836,2251,4644,2275,3776,2854,4711,5000,5000,2713,4367,2064,2650,4722,3874,349,3808,1356,536,3812,232,4868,478,4167,4561,4662,5000,1907,1346,699,1845,4132,5000,1356,1555,4599,1670,2769,4874,3289,2753,1428,1304,3096,3796,4755,626,3289,1803,377,1711,1204,4130,1724,4022,987,3862,2849,3816,1076,4918,2899,1439,2069,4962,2727,2752,12028
|
||||||
|
3418,3890,2179,2768,3198,4778,4048,2777,1260,44,1528,2441,1980,3164,3181,1002,4505,1858,2264,4648,2298,3826,2881,4692,5000,4699,2672,4336,2026,2605,4743,3907,390,3840,1392,564,3848,271,4896,428,4116,4529,4648,4996,1886,1363,733,1891,4168,5000,1358,1580,4632,1705,2732,4833,3254,2726,1400,1265,3048,3814,4790,636,3306,1421,399,1748,1245,4159,1672,3969,953,3804,2792,3787,1110,4946,2931,1469,2091,4998,2758,2771,12076
|
||||||
|
3379,3853,2148,2739,3163,4796,4067,2806,1300,60,1559,2461,2012,3204,3152,957,4452,1819,2213,4590,2260,3876,3329,4735,5000,4719,2693,4368,2049,2623,4702,3879,368,3811,1365,529,3884,310,4923,439,4128,4561,4697,5000,1927,1318,706,1876,4142,5000,1298,1544,4665,1739,2758,4854,3282,2761,1434,1288,3062,3772,5000,583,3262,1399,360,1784,1287,4186,1682,3979,980,3807,2798,3822,1082,4913,2901,1437,2051,4971,2726,2790,12118
|
||||||
|
3402,3878,2179,2772,3189,4752,4023,2774,1279,13,1530,2420,2044,3244,3184,972,4462,1844,2224,4595,2286,3866,3295,4717,5000,4676,2652,4399,2073,2640,4725,3914,408,3843,1401,556,3858,287,4887,388,4079,4532,4684,5000,1968,1335,741,1922,4176,5000,1300,1570,4637,1711,2723,4814,3247,2734,1467,1311,3076,3792,5000,592,3280,1440,383,1760,1266,4151,1629,3927,943,3748,2804,3858,1115,4942,2933,1466,2073,5000,2756,2747,12161
|
||||||
|
3362,3842,2149,2804,3216,4770,4042,2805,1319,30,1563,2441,2014,3221,3154,926,4410,1807,2234,4600,2313,3918,3324,4761,5000,4695,2673,4368,2034,2596,4685,3887,386,3814,1436,583,3895,326,4913,399,4093,4565,4734,5000,1947,1291,715,1907,4148,5000,1302,1596,4670,1745,2750,4835,3275,2769,1439,1271,3028,3750,5000,539,2818,1482,407,1797,1307,4178,1638,3938,967,3751,2750,3834,1087,4908,2904,1433,2094,5000,2786,2765,12205
|
||||||
|
3385,3868,2180,2775,3180,4726,3999,2774,1297,47,1598,2462,2047,3261,3185,940,4420,1833,2182,4544,2279,4329,3291,4743,5000,4712,2694,4398,2057,2613,4708,3922,425,3847,1409,548,3870,302,4875,347,4107,4600,4783,5000,1988,1309,752,1953,4180,5000,1243,1561,4642,1717,2715,4794,3303,2804,1471,1293,3041,4191,5000,548,2839,1462,369,1773,1285,4141,1585,3949,990,3753,2759,3873,1121,4937,2936,1462,2053,5000,2753,2720,12250
|
||||||
|
3408,3894,2212,2808,3205,4744,4019,2805,1338,3,1571,2421,2018,3238,3154,893,4431,1860,2190,4551,2309,4382,3320,4787,5000,4667,2653,4367,2017,2569,4670,3958,464,3880,1444,574,3907,340,4899,358,4059,4574,4770,5000,1967,1266,727,1999,4212,5000,1247,1588,4676,1750,2744,4816,3269,2777,1442,1252,2993,4151,5000,557,2861,1506,393,1811,1325,4166,1593,3900,950,3693,2707,3852,1093,4905,2968,1491,2074,5000,2782,2738,12296
|
||||||
|
3369,3858,2181,2778,3169,4700,3977,2838,1378,21,1607,2443,2051,3277,3184,907,4380,1826,2137,4496,2278,4374,3351,4411,5000,4683,2674,4397,2039,2586,4694,3932,441,3852,1417,537,3882,316,4923,368,4075,4611,4819,5000,2008,1285,764,1983,4181,5000,1189,1553,4648,1783,2773,4838,3298,2812,1474,1273,3007,4174,5000,85,2822,1488,356,1788,1303,4191,1601,3912,971,3695,2718,3894,1127,4934,2938,1457,2033,5000,2748,2693,12343
|
||||||
|
3392,3884,2213,2810,3193,4717,3998,2808,1356,0,1582,2403,2023,3317,3213,920,4391,1855,2144,4504,2731,4428,3319,4393,5000,4636,2633,4365,1999,2604,4719,3969,479,3885,1452,562,3920,353,4884,316,4029,4587,4807,5000,2049,1304,802,2029,4211,5000,1194,1581,4682,1753,2740,4798,3265,2785,1444,1231,3442,4197,5000,94,2846,1533,381,1827,1343,4152,1547,3864,929,3634,2668,3938,1160,4963,2970,1485,2054,5000,2776,2709,12391
|
||||||
|
3353,3849,2183,2780,3218,4735,4019,2842,1396,0,1620,2425,2057,3294,3180,871,4341,1823,2089,4450,2765,4483,3350,4438,5000,4650,2654,4394,2020,2560,4683,3944,455,3857,1424,587,3957,390,4906,325,4046,4625,4855,5000,2027,1263,779,2012,4178,5000,1138,1609,4716,1785,2770,4820,3294,2821,1476,1251,3394,4159,5000,42,2809,1516,407,1866,1382,4175,1555,3878,948,3635,2682,3921,1132,4930,2940,1451,2012,5000,2804,2726,12433
|
||||||
|
3376,3875,2215,2812,3180,4690,3978,2814,1375,0,1596,2448,2091,3333,3208,884,4353,1854,2096,4460,2738,4475,2900,4421,5000,4602,2675,4424,2041,2578,4709,3982,493,3891,1458,549,3933,365,4865,273,4002,4603,4904,5000,2068,1283,818,2057,4206,5000,1144,1575,4688,1755,2739,4780,3261,2856,1507,1271,3408,4183,4884,53,2835,1562,371,1843,1359,4135,1501,3832,903,3637,2697,3967,1166,4960,2972,1479,2032,5000,2768,2680,12476
|
||||||
|
3338,3840,2247,2844,3204,4708,4000,2848,1414,0,1635,2409,2064,3309,3174,834,4304,1886,2102,4891,2775,4530,2932,4465,5000,4615,2634,4391,2000,2534,4673,3957,468,3925,1493,573,3972,401,4886,283,4021,4643,4890,5000,2047,1243,796,2040,4233,5000,1151,1604,4723,1786,2770,4802,3291,2829,1476,1648,3361,4146,4862,2,2863,1609,397,1882,1398,4157,1508,3848,920,3576,2651,3953,1137,4928,2942,1506,2052,5000,2795,2696,12520
|
||||||
|
3361,3867,2217,2813,3166,4663,3960,2821,1454,2,1675,2432,2099,3348,3201,846,4317,1856,2046,4840,2751,4522,2903,4448,5000,4627,2656,4419,2021,2552,4700,3995,504,3897,1464,535,3948,375,4844,292,4041,4685,4938,5000,2087,1264,836,2085,4197,5000,1097,1571,4695,1755,2740,4825,3320,2865,1507,1667,3375,4172,4903,13,2829,1594,362,1860,1374,4115,1515,3865,936,3577,2669,4002,1170,4958,2974,1472,2010,5000,2759,2650,12565
|
||||||
|
3384,3894,2249,2845,3189,4680,3983,2856,1432,0,1653,2393,2072,3325,3165,858,4331,1890,2052,4852,2790,4157,2936,4493,5000,4577,2615,4386,1979,2509,4728,4034,540,3931,1498,558,3986,410,4864,240,4000,4664,4923,5000,2065,1225,876,2129,4223,5000,1106,1600,4729,1785,2772,4785,3288,2838,1475,1623,3328,3779,4945,24,2859,1641,389,1900,1412,4135,1461,3820,889,3517,2626,3990,1141,4987,3006,1498,2030,5000,2784,2665,12611
|
||||||
|
3345,3860,2220,2814,3150,4636,4006,2892,1471,0,1694,2417,2107,3363,3191,808,4283,1863,2415,4804,2769,4211,2970,4537,5000,4588,2637,4414,1999,2527,4694,4010,513,3903,1469,519,3963,445,4883,250,4021,4707,4970,5000,2105,1248,855,2111,4185,4986,1054,1568,4764,1815,2805,4808,3319,2873,1925,1642,3343,3744,4924,0,2827,1627,355,1878,1449,4155,1468,3839,902,3518,2646,4041,1174,4955,2975,1463,1988,5000,2747,2681,12658
|
||||||
|
3369,3887,2252,2845,3173,4653,3967,2866,1448,0,1674,2379,2143,3401,3217,819,4298,1898,2421,4818,2811,4203,2942,4520,5000,4536,2596,4379,2019,2546,4723,4049,548,3938,1503,541,4002,417,4839,198,3982,4689,4954,5000,2145,1271,897,2154,4208,4990,1065,1598,4736,1782,2776,4769,3287,2847,1892,1660,3359,3772,4966,0,2859,1674,383,1918,1424,4112,1414,3797,853,3458,2668,4092,1207,4985,3007,1490,2007,5000,2772,2634,12706
|
||||||
|
3330,3853,2222,2875,3195,4671,3992,2902,1486,0,1716,2404,2117,3377,3179,768,4252,1872,2364,4834,2434,4257,2976,4564,5000,4546,2618,4407,1977,2504,4690,4025,519,3910,1536,563,4040,451,4857,208,4005,4733,5000,5000,2123,1233,876,2135,4169,4931,1077,1628,4770,1811,2810,4792,3318,2882,1921,1615,2893,3738,4946,0,2829,1722,411,1958,1460,4130,1421,3817,865,3461,2630,4083,1177,4954,2977,1454,1965,5000,2796,2649,12748
|
||||||
|
3354,3881,2255,2844,3155,4626,3954,2877,1462,0,1758,2428,2153,3414,3203,779,4268,2329,2369,4789,2416,4248,2949,4547,5000,4555,2640,4434,1997,2523,4720,4064,553,3945,1506,523,4017,422,4812,156,3967,4777,5000,5000,2163,1258,918,2178,4190,4934,1028,1596,4743,1777,2782,4753,3349,3337,1950,1632,2909,3768,4988,0,2863,1709,378,1936,1434,4086,1367,3776,876,3463,2655,4137,1209,4984,3008,1480,1984,5000,2758,2602,12791
|
||||||
|
3315,3909,2287,2874,3177,4644,3980,2914,1500,0,1739,2391,2127,3389,3165,728,4285,2367,2374,4807,2461,4300,2985,4591,5000,4502,2599,4399,1954,2481,4689,4041,585,3979,1539,544,4056,455,4828,167,3992,4760,5000,5000,2140,1221,899,2220,4211,4937,1042,1627,4777,1805,2817,4777,3318,3310,1916,1587,2863,3735,4969,0,2898,1757,407,1977,1469,4103,1374,3799,825,3404,2619,4129,1179,4952,3040,1505,2003,5000,2781,2617,12835
|
||||||
|
3339,3875,2258,2842,3137,4600,3943,2951,1537,0,1782,2417,2164,3426,3188,739,4241,2343,2317,4344,2445,4290,2959,4636,5000,4510,2621,4426,1973,2502,4720,4080,555,3952,1509,503,4033,425,4845,178,4019,4806,5000,5000,2179,1246,941,2200,4169,4878,995,1596,4749,1771,2852,4800,3349,3345,1944,1184,2880,3766,5000,0,2872,1743,375,1955,1442,4119,1382,3822,834,3408,2647,4184,1211,4983,3009,1469,1960,5000,2742,2570,12880
|
||||||
|
3363,3903,2290,2871,3158,4617,3969,2927,1512,0,1763,2380,2139,3401,3210,749,4679,2383,2322,4364,2492,4341,2995,4618,5000,4456,2582,4390,1930,2522,4751,4119,586,3987,1541,524,4072,457,4798,127,3984,4790,5000,5000,2156,1272,984,2241,4188,4881,1011,1627,4783,1797,2825,4762,3739,3318,1909,1139,2897,3797,5000,0,2908,1790,405,1996,1477,4073,1328,3784,781,3350,2614,4177,1242,5000,3040,1494,1980,5000,2764,2585,12926
|
||||||
|
3325,3869,2261,2839,3117,4635,3996,2965,1548,0,1806,2407,2175,3437,3170,698,4637,2361,2266,4323,2539,4391,3032,4662,5000,4463,2604,4416,1949,2481,4721,4095,554,3959,1510,482,4111,488,4813,138,4012,4836,5000,5000,2194,1237,966,2219,4144,4822,966,1658,4816,1824,2861,4786,3771,3353,1936,1155,2852,3767,5000,0,2884,1776,373,2037,1511,4088,1337,3810,789,3355,2645,4233,1211,4982,3009,1457,1937,5000,2786,2601,12973
|
||||||
|
3349,3898,2293,2868,3138,4592,3961,2941,1522,0,1788,2433,2213,3473,3192,709,4656,2402,1851,4346,2526,4379,3007,4643,5000,4408,2564,4441,1968,2502,4754,4134,583,3994,1542,502,4088,457,4766,88,3979,4821,5000,5000,2233,1265,1009,2259,4161,4825,985,1628,4788,1788,2835,4748,3741,3326,1543,1171,2870,3800,5000,0,2922,1823,404,2015,1482,4041,1283,3774,734,3360,2677,4289,1241,5000,3040,1482,1956,5000,2746,2554,13021
|
||||||
|
3311,3865,2326,2897,3158,4610,3989,2980,1557,0,1832,2398,2188,3447,3151,1077,4615,2381,1857,4370,2575,4427,3044,4687,5000,4415,2587,4405,1925,2462,4725,4110,550,4029,1573,522,4127,487,4780,101,4009,4868,5000,5000,2209,1230,991,2236,4115,4828,1005,1660,4821,1813,2871,5000,3773,3360,1507,1125,2826,3771,5000,0,2961,1870,435,2056,1515,4055,1292,3801,741,3305,2649,4283,1210,4981,3009,1444,1975,5000,2767,2569,13063
|
||||||
|
3335,3894,2296,2863,3116,4567,3955,2956,1529,27,1875,2425,2225,3482,3171,1088,4636,2423,1801,4333,2563,4412,3020,4668,5000,4421,2610,4430,1944,2484,4758,4149,578,4002,1541,479,4103,454,4732,51,4040,4915,5000,5000,2247,1259,1035,2275,4131,4769,964,1630,4792,1776,2845,5000,3805,3395,1533,1141,2845,3805,5000,0,2940,1855,405,2035,1485,4007,1239,3829,747,3312,2684,4340,1239,5000,3040,1469,1932,5000,2726,2522,13106
|
||||||
|
3359,3923,2329,2892,3137,4586,3984,2995,1563,0,1857,2390,2201,3455,3129,1098,4659,2046,1808,4360,2614,4458,3058,4711,5000,4365,2571,4392,1901,2445,4730,4187,605,4037,1572,498,4142,483,4746,65,4010,4900,5000,4993,2222,1226,1079,2313,4145,4772,986,1662,4825,1800,2882,5000,3776,2947,1496,1095,2802,3777,5000,0,2980,1901,437,2076,1517,4021,1249,3796,691,3258,2659,4335,1207,5000,3070,1493,1951,5000,2746,2538,13150
|
||||||
|
3322,3890,2299,2858,3094,4543,4014,3034,1597,32,1900,2417,2238,3490,3568,1047,4620,2027,1753,4326,2603,4441,3097,4754,5000,4370,2594,4417,1919,2468,4764,4163,569,4010,1540,455,4118,511,4759,78,4043,4947,5000,5000,2259,1255,1061,2288,4097,4714,947,1633,4796,1824,3339,5000,3808,2981,1521,1111,2822,3812,5000,0,2960,1885,407,2054,1548,4033,1259,3826,696,3267,2696,4391,1236,5000,3039,1455,1908,5000,2704,2492,13195
|
||||||
|
3346,3919,2332,2886,3114,4562,3981,3010,1567,5,1882,2383,2214,3524,3587,1058,4643,2070,1760,4355,2654,4484,3074,4735,5000,4314,2555,4379,1938,2491,4799,4200,595,4045,1570,474,4157,477,4710,31,4014,4933,5000,4949,2296,1286,1105,2325,4111,4718,972,1665,4828,1785,3314,5000,3779,2953,1484,1126,2842,3848,5000,0,3002,1930,440,2095,1516,3984,1208,3795,639,3214,2674,4448,1265,5000,3069,1478,1927,5000,2724,2508,13241
|
||||||
|
3309,3887,2302,2851,3134,4582,4012,3049,1599,40,1925,2412,2252,3496,3543,1007,4186,2052,1706,4385,2706,4527,3113,4777,5000,4319,2579,4403,1895,2453,4772,4176,557,4017,1537,492,4195,504,4722,45,4049,4980,5000,4956,2271,1254,1088,2299,4061,4660,998,1698,4860,1808,3351,5000,3392,2987,1508,1080,2801,3822,5000,0,2983,1912,473,2136,1546,3996,1219,3827,644,3225,2714,4442,1231,5000,3037,1440,1884,5000,2743,2524,13288
|
||||||
|
3333,3917,2334,2878,3091,4540,3980,3026,1569,14,1968,2440,2290,3949,3561,1019,4211,2097,1715,4355,2696,4505,3091,4757,5000,4262,2603,4426,1913,2478,4807,4212,581,4052,1567,448,4171,468,4673,0,4022,5000,5000,4963,2307,1286,1132,2334,4073,4664,963,1668,4830,2189,3326,5000,3363,3020,1532,1095,2822,3858,5000,0,3027,1955,444,2114,1514,3946,1168,3798,648,3237,2756,4498,1259,5000,3067,1463,1903,5000,2700,2478,13336
|
||||||
|
3296,3946,2367,2905,3110,4560,4012,3065,1599,51,1949,2407,2266,3920,3516,968,4175,2142,1724,4387,2749,4545,3131,4798,5000,4267,2565,4388,1870,2440,4781,4187,604,4087,1595,466,4209,494,4685,15,4059,5000,5000,4907,2281,1255,1115,2307,4084,4669,992,1701,4861,2211,3363,5000,3396,2992,1493,1049,2782,3833,5000,0,3071,1998,478,2154,1543,3957,1180,3832,590,3188,2737,4492,1224,5000,3035,1486,1922,5000,2719,2495,13378
|
||||||
|
3321,3914,2337,2870,3068,4518,3981,3042,1629,89,1991,2437,2304,3952,3533,560,4202,2125,1672,4359,2739,4521,3109,4840,5000,4272,2589,4411,1889,2466,4817,4223,564,4060,1562,421,4184,458,4635,32,4096,5000,5000,4912,2316,1288,1159,2341,4033,4613,959,1672,4830,2170,3400,4789,3430,3025,1516,1064,2804,3871,5000,0,3053,1978,450,2133,1509,3906,1193,3867,593,3202,2781,4547,1251,5000,3064,1447,1880,5000,2675,2450,13421
|
||||||
|
3346,3944,2369,2896,3087,4539,4013,3080,1597,65,1971,2404,2700,3923,3550,572,4230,2171,1682,4395,2792,4557,3150,4818,5000,4214,2552,4372,1846,2429,4853,4259,586,4095,1590,439,4222,482,4646,0,4072,5000,5000,4854,2290,1320,1204,2374,4042,4619,990,1705,5000,2192,3375,4753,3401,2996,1477,1017,2765,3909,5000,0,3098,2018,484,2173,1536,3917,1144,3840,535,3155,2764,4539,1278,5000,3094,1469,1899,5000,2693,2467,13465
|
||||||
|
3309,3912,2339,2861,3044,4560,4046,3119,1625,103,2012,2434,2738,3954,3504,522,4197,2154,1631,4369,2782,4592,3190,4859,5000,4219,2576,4394,1865,2456,4827,4232,545,4067,1556,394,4259,507,4658,5,4110,5000,5000,4858,2324,1292,1186,2344,3990,4564,960,1676,5000,2212,3412,4780,3435,3028,1500,1033,2788,3885,5000,0,3081,1996,457,2213,1563,3927,1157,3877,538,3171,2811,4593,1242,5000,3061,1430,1857,5000,2649,2485,13510
|
||||||
|
3334,3943,2371,2886,3062,4520,4017,3095,1591,81,1991,2402,2776,3985,3099,535,4226,2200,1643,4406,2834,4563,3169,4837,5000,4162,2539,4416,1884,2482,4864,4266,565,4102,1584,411,4234,468,4608,0,4088,5000,5000,4862,2359,1326,1231,2376,3998,4571,994,1709,5000,2171,2967,4744,3406,2999,1522,1048,2812,3924,5000,0,3127,2035,492,2190,1528,3875,1110,3852,480,3126,2858,4646,1268,5000,3090,1452,1876,5000,2667,2441,13556
|
||||||
|
3298,3911,2340,2912,3081,4542,4050,3133,1618,120,2031,2853,2752,3954,3053,486,4195,2185,1656,4445,2886,4595,3211,4876,5000,4166,2564,4376,1842,2447,4839,4238,522,4074,1611,428,4270,491,4619,0,4128,5000,5000,4803,2331,1298,1213,2344,3945,4579,1029,2163,5000,2191,3004,4771,3440,3031,1481,1002,2775,3901,5000,0,3111,2073,527,2230,1554,3885,1125,3890,483,3145,2844,4637,1231,5000,3057,1412,1896,5000,2685,2459,13603
|
||||||
|
3323,3942,2372,2875,3038,4502,4022,3109,1582,160,2070,2883,2790,3984,3068,499,4226,2231,1608,4423,2876,4563,3190,4853,5000,4171,2590,4398,1861,2475,4876,4272,541,4109,1576,382,4245,452,4568,0,4168,5000,5000,4805,2364,1333,1257,2374,3953,4525,1002,2134,5000,2149,2978,4736,3474,3063,1502,1018,2800,3940,5000,0,3157,2048,500,2207,1517,3833,1078,3929,486,3165,2894,4688,1256,5000,3086,1434,1854,5000,2640,2416,13651
|
||||||
|
3349,3972,2404,2900,3057,4525,4056,3147,1608,139,2047,2853,2767,3532,3020,451,4257,2277,1623,4463,2928,4591,3232,4892,5000,4114,2553,4358,1819,2441,4851,4305,559,4143,1602,399,4281,474,4580,0,4148,5000,5000,4745,2335,1306,1239,2404,3960,4535,1039,2167,5000,1748,3014,4763,3446,3032,1461,971,2764,3918,5000,0,3203,2083,536,2247,1542,3843,1095,3907,428,3124,2882,4677,1218,5000,3115,1455,1874,5000,2657,2435,13693
|
||||||
|
3312,3941,2373,2863,3013,4486,4029,3185,1633,179,2505,2884,2805,3561,3035,465,4228,2262,1576,4443,2917,4556,3274,4930,5000,4119,2579,4379,1839,2470,4889,4275,514,4115,1566,353,4254,433,4591,0,4189,5000,5000,4748,2368,1342,1283,2370,3905,4483,1435,2138,5000,1767,3051,4791,3480,3064,1482,987,2790,3958,5000,0,3187,2056,510,2224,1504,3852,1112,3948,432,3146,2932,4726,1242,5000,3081,1415,1832,5000,2612,2393,13736
|
||||||
|
3338,3972,2404,2888,3032,4509,4064,3160,1595,158,2481,2854,2780,3590,3049,479,4261,2308,1593,4486,2967,4581,3254,4906,5000,4063,2543,4339,1797,2499,4926,4307,531,4150,1592,369,4290,454,4541,0,4170,5000,5000,4688,2400,1378,1327,2398,3911,4493,1475,2171,5000,1724,3024,4756,3452,3033,1440,941,2817,3998,5000,0,3233,2089,547,2262,1527,3800,1068,3927,374,3108,2922,4774,1266,5000,3109,1436,1853,5000,2629,2413,13780
|
||||||
|
3302,3941,2373,2850,3050,4533,4099,3197,1618,200,2518,2886,2398,3556,3001,433,4233,2292,1548,4467,3017,4604,3296,4943,5000,4068,2570,4360,1817,2466,4901,4276,486,4122,1556,386,4325,474,4552,0,4213,5000,5000,4690,2370,1353,1309,2362,3856,4443,1453,2204,4908,1743,3060,4784,3486,3064,1460,958,2782,3977,5000,0,3217,2059,583,2301,1550,3809,1086,3969,378,3133,2974,4759,1228,5000,3075,1396,1811,5000,2646,2433,13825
|
||||||
|
3328,3971,2404,2874,3007,4495,4073,3171,1579,599,2491,2918,2436,3584,3014,448,4268,2339,1567,4512,3004,4564,3276,4918,5000,4012,2596,4381,1838,2496,4939,4307,501,4156,1581,340,4297,432,4502,0,4194,5000,5000,4692,2402,1390,1352,2388,3861,4876,1494,2175,4873,1699,3033,4750,3458,3094,1480,974,2810,4017,5000,0,3263,2090,558,2277,1510,3756,1044,3950,320,3159,3027,4805,1251,5000,3103,1417,1832,5000,2600,2392,13871
|
||||||
|
3292,3940,2435,2898,3026,4520,4108,3207,1601,641,2526,2888,2412,3550,2965,402,4242,2385,1586,4557,3052,4585,3319,4954,5000,4018,2561,4339,1796,2464,4914,4275,455,4189,1606,356,4331,451,4514,0,4238,5000,5000,4632,2371,1365,1333,2352,3867,4889,1537,2208,4900,1718,3068,4778,3492,3063,1438,929,2777,3996,5000,0,3309,2120,595,2315,1532,3765,1064,3993,325,3125,3018,4787,1211,5000,3069,1438,1853,5000,2617,2414,13918
|
||||||
|
3319,3972,2404,2859,2982,4483,4082,3181,1622,683,2560,2501,2449,3576,2978,419,4278,2369,1545,4541,3038,4541,3299,4927,5000,4024,2588,4360,1817,2495,4951,4304,470,4161,1569,310,4303,408,4464,0,4282,5000,5000,4635,2401,1403,1376,2376,3811,4841,1518,1759,4864,1674,3040,4807,3526,3092,1457,946,2806,4037,5000,0,3293,2086,571,2290,1491,3713,1084,4037,331,3153,3071,4831,1234,5000,3096,1397,1812,5000,2571,2374,13966
|
||||||
|
3345,4003,2434,2882,3001,4508,4119,3216,2001,663,2532,2472,2425,3541,2929,436,4315,2414,1567,4588,3085,4558,3342,4962,5000,3969,2553,4318,1777,2464,4988,4333,484,4194,1593,326,4337,426,4476,0,4265,5000,5000,4575,2369,1380,1418,2400,4236,4857,1562,1791,4890,1692,3074,4774,3498,3060,1414,901,2774,4078,5000,0,3338,2113,608,2327,1512,3722,1044,4019,275,3122,3063,4810,1194,5000,3124,1417,1833,5000,2588,2396,14008
|
||||||
|
3310,3972,2403,2843,2958,4472,4155,3251,2021,705,2564,2505,2462,3567,2942,392,4291,2398,1527,4573,3069,4574,3384,4996,5000,3976,2581,4339,1798,2496,4964,4298,437,4166,1555,280,4308,444,4488,0,4309,5000,5000,4579,2399,1418,1398,2360,4179,4811,1545,1762,4916,1709,3107,4802,3533,3089,1432,918,2804,4057,5000,0,3321,2077,584,2302,1532,3731,1067,4064,281,3153,3117,4850,1215,5000,3089,1376,1793,5000,2542,2419,14051
|
||||||
|
3337,4003,2433,2866,2977,4499,4130,3223,1978,685,2113,2477,2499,3592,2955,410,4329,2443,1551,4621,3113,4526,3365,4968,5000,3922,2547,4297,1820,2528,5000,4326,450,4199,1578,295,4340,399,4439,0,4292,5000,5000,4582,2428,1457,1440,2382,4184,4828,1171,1794,4879,1665,3078,4769,3505,3056,1389,936,2835,4098,5000,0,3365,2102,622,2339,1490,3679,1028,4047,226,3124,3171,4889,1237,5000,3116,1397,1815,5000,2559,2381,14095
|
||||||
|
3301,3973,2401,2888,2996,4525,4167,3677,1997,727,2143,2510,2474,3555,2905,367,4306,2425,1514,4669,3157,4539,3408,5000,5000,3930,2575,4317,1780,2498,4975,4290,402,4170,1602,311,4372,416,4452,0,4338,5000,5000,4525,2395,1435,1420,2762,4127,4784,1218,1826,4904,1683,3111,4799,3539,3085,1407,892,2804,4077,5000,0,3347,2125,660,2375,1509,3688,1052,4093,233,3158,3164,4864,1196,5000,3080,1355,1775,5000,2575,2405,14140
|
||||||
|
3328,4004,2431,2849,2953,4491,4142,3649,1954,707,2173,2544,2511,3580,2918,387,4345,2470,1540,4656,3138,4488,3389,4972,5000,3938,2603,4337,1803,2531,5000,4316,415,4203,1563,265,4342,371,4403,0,4321,5000,5000,4530,2423,1475,1461,2782,4132,4804,1204,1796,4866,1639,3081,4766,3573,3113,1425,910,2836,4118,5000,0,3391,2085,636,2349,1465,3636,1015,4077,242,3193,3218,4900,1217,5000,3107,1375,1797,5000,2530,2368,14186
|
||||||
|
3294,4035,2460,2871,2972,4518,4179,3682,1971,328,2139,2517,2485,3542,2868,345,4385,2513,1567,4705,3179,4498,3432,5000,5000,3885,2569,4295,1764,2502,4986,4279,429,4235,1585,281,4373,387,4417,0,4366,5000,5000,4473,2389,1453,1439,2802,4137,4404,1252,1828,4890,1656,3112,4796,3545,3080,1381,867,2807,4097,5000,39,3434,2106,674,2384,1483,3646,1041,4124,189,3167,3210,4871,1175,5000,3133,1396,1820,5000,2547,2393,14233
|
||||||
|
3321,4005,2428,2831,2930,4484,4574,3714,1988,370,2166,2551,2522,3565,2880,365,4364,2495,1533,4693,3158,4445,3412,5000,5000,3895,2598,4315,1787,2536,5000,4304,380,4206,1546,235,4342,341,4431,0,4412,5000,5000,4480,2416,1493,1900,2759,4080,4363,1239,1797,4852,1612,3143,4826,3579,3107,1399,886,2840,4138,5000,18,3414,2064,651,2357,1439,3656,1067,4171,198,3205,3264,4904,1196,5000,3097,1354,1781,5000,2501,2357,14281
|
||||||
|
3348,4036,2457,2852,2950,4513,4612,3684,1943,349,2131,2524,2496,3526,2892,387,4405,2537,1563,4743,3197,4452,3455,5000,5000,3843,2565,4273,1749,2569,5000,4327,393,4238,1568,252,4372,356,4384,0,4395,5000,4986,4425,2381,1534,1939,2777,4085,4386,1289,1829,4875,1630,3111,4793,3551,3073,1354,844,2873,4178,5000,60,3456,2082,690,2392,1456,3604,1033,4156,147,3181,3256,4872,1216,5000,3123,1374,1804,5000,2518,2383,14323
|
||||||
|
3314,4006,2424,2812,2907,4542,4649,3715,1539,390,2156,2558,2532,3549,2843,347,4384,2518,1531,4731,3235,4458,3498,5000,5000,3854,2594,4293,1773,2541,5000,4289,344,4208,1528,206,4402,371,4399,2,4441,5000,4993,4434,2407,1513,1917,2733,3608,4347,1277,1860,4898,1648,3141,4824,3585,3100,1372,863,2845,4157,5000,40,3435,2038,666,2426,1473,3614,1061,4204,158,3221,3310,4901,1174,5000,3087,1333,1765,5000,2535,2410,14366
|
||||||
|
3341,4037,2453,2833,2927,4929,4624,3684,1493,368,2118,2593,2567,3571,2855,370,4426,2559,1562,4781,3209,4402,3479,5000,5000,3803,2562,4313,1797,2576,5000,4311,357,4240,1549,222,4370,324,4352,0,4424,5000,5000,4443,2433,1974,1955,2750,3614,4372,1328,1829,4859,1604,3108,4792,3557,3064,1389,884,2880,4197,5000,83,3475,2054,705,2398,1427,3563,1028,4189,108,3262,3363,4928,1194,5000,3113,1353,1789,5000,2491,2375,14410
|
||||||
|
3307,4007,2481,2854,2947,4959,4662,3714,1508,408,2142,2567,2541,3530,2805,331,4407,2538,1595,4832,3244,4406,3521,5000,5000,3815,2591,4271,1760,2548,5000,4271,308,4272,1570,239,4398,339,4368,9,4469,5000,4944,4391,2397,1953,1932,2704,3557,4397,1379,1859,4881,1622,3136,4822,3590,3091,1344,842,2852,4175,5000,64,3514,2069,744,2431,1443,3574,1058,4237,121,3242,3354,4892,1152,5000,3076,1311,1813,5000,2508,2404,14455
|
||||||
|
3335,4038,2448,2813,2906,4927,4637,3261,1461,448,2164,2602,2576,3552,2818,355,4449,2578,1567,4820,3216,4347,3502,4998,5000,3828,2621,4291,1785,2583,5000,4292,321,4241,1529,193,4365,291,4322,0,4514,5000,4949,4403,2422,1995,1969,2299,3563,4362,1369,1828,4841,1578,3102,4853,3624,3117,1361,863,2888,4214,5000,107,3490,2022,721,2402,1396,3524,1027,4285,135,3285,3406,4916,1171,5000,3101,1331,1775,5000,2463,2370,14501
|
||||||
|
3363,4070,2476,2834,3346,4957,4674,3290,1475,425,2124,2575,2548,3510,2768,380,4492,2618,1601,4870,3248,4349,3544,5000,5000,3779,2589,4249,1748,2556,5000,4313,334,4273,1550,210,4393,305,4339,18,4497,5000,4892,4353,2804,1975,2007,2314,3569,4390,1421,1858,4863,1597,3130,4822,3596,3081,1316,822,2862,4192,5000,150,3528,2035,760,2435,1411,3535,1059,4271,88,3267,3396,4876,1129,5000,3126,1351,1800,5000,2481,2399,14548
|
||||||
|
3329,4039,2442,2793,3305,4926,4712,3318,1490,464,2144,2611,2583,3531,2781,344,4474,2595,1575,4859,3217,4289,3586,5000,5000,3793,2619,4269,1774,2591,5000,4271,286,4242,1509,165,4358,319,4357,55,4542,5000,4897,4367,2829,2017,1981,2267,3514,4357,1411,1825,4822,1615,3156,4853,3629,3106,1333,844,2898,4231,5000,132,3503,1985,737,2405,1425,3548,1091,4319,104,3312,3447,4897,1149,5000,3089,1309,1763,5000,2437,2367,14596
|
||||||
|
3357,4071,2470,2814,3326,4957,4267,3284,1442,440,2102,2585,2555,3551,2793,370,4517,2633,1611,4909,3247,4290,3566,5000,5000,3746,2588,4227,1800,2627,5000,4290,300,4272,1529,182,4385,271,4313,30,4524,5000,4840,4320,2852,2059,1598,2280,3521,4387,1463,1855,4843,1572,3120,4822,3600,3069,1288,867,2934,4269,5000,176,3538,1996,776,2437,1377,3498,1062,4305,59,3295,3435,4916,1168,5000,3114,1330,1788,5000,2455,2397,14638
|
||||||
|
3323,4040,2435,3192,3348,4989,4304,3311,1455,478,2121,2621,2589,3508,2744,335,4499,2608,1587,4958,3275,4290,3608,5000,5000,3762,2618,4247,1765,2600,5000,4247,252,4241,1487,199,4411,284,4331,68,4567,5000,4844,4756,2813,2039,1571,2231,3466,4356,1516,1884,4864,1592,3146,4853,3634,3094,1304,827,2909,4245,5000,159,3511,1945,815,2468,1391,3511,1096,4353,77,3342,3484,4871,1126,5000,3076,1288,1752,5000,2474,2848,14681
|
||||||
|
3352,4072,2463,3212,3307,4959,4279,3275,1407,453,2138,2657,2623,3527,2757,363,4543,2645,1625,4946,3239,4228,3588,5000,5000,3716,2649,4267,1792,2636,5000,4266,266,4271,1507,155,4375,235,4289,45,4549,5000,4848,4774,2836,2082,1606,2244,3474,4388,1506,1851,4822,1549,3109,4823,3605,3119,1321,851,2946,4283,5000,203,3545,1955,793,2436,1343,3463,1069,4339,96,3389,3533,4887,1145,5000,3100,1308,1777,5000,2431,2817,14725
|
||||||
|
3319,4103,2490,3233,3329,4571,4315,3301,1420,489,2093,2631,2594,3484,2709,329,4526,2680,1665,4995,3264,4228,3629,5000,5000,3733,2618,4226,1757,2610,5000,4222,281,4301,1526,172,4401,249,4308,84,4592,5000,4790,4731,2796,1642,1578,2194,3482,4421,1559,1880,4842,1569,3133,4854,3638,3081,1276,812,2922,4259,5000,247,3577,1964,832,2466,1356,3477,1104,4386,54,3375,3519,4839,1103,5000,3062,1328,1803,5000,2450,2849,14770
|
||||||
|
3347,4073,2875,3191,3289,4542,4290,3264,1433,525,2110,2667,2627,3502,2722,358,4570,2653,1643,4981,3226,4165,3608,5000,5000,3751,2649,4246,1785,2646,5000,4239,234,4269,1484,128,4364,200,4267,124,4634,5000,5000,4752,2818,1685,1612,2205,3429,4393,1550,1846,4801,1527,3157,4886,3670,3105,1293,836,2960,4296,5000,230,3547,1910,809,2434,1307,3429,1140,4434,75,3424,3565,4852,1123,5000,3086,1287,1768,5000,2827,2819,14816
|
||||||
|
3376,4104,2901,3211,3312,4575,4326,3289,1384,498,2063,2642,2598,3458,2736,387,4614,2687,1684,5000,3248,4164,3650,5000,5000,3708,2618,4205,1751,2620,5000,4257,250,4299,1503,146,4388,213,4288,102,4614,5000,5000,4711,2778,1728,1645,2216,3438,4429,1603,1874,4820,1548,3118,4856,3641,3066,1248,799,2936,4332,5000,274,3577,1918,848,2463,1319,3444,1115,4419,35,3412,3549,4801,1142,5000,3110,1307,1795,5000,2847,2852,14863
|
||||||
|
3343,4073,2866,3169,2852,4608,4362,3313,1398,532,2078,2678,2630,3476,2688,356,4597,2658,1664,5000,3207,4163,3690,5000,5000,3728,2650,4225,1779,2656,5000,4211,204,4266,1460,103,4412,226,4310,143,4655,5000,5000,4735,2379,1708,1616,2164,3386,4403,1593,1839,4840,1569,3140,4888,3674,3089,1264,824,2974,4306,5000,256,3545,1863,825,2492,1332,3460,1153,4466,59,3462,3593,4811,1100,5000,3072,1266,1760,5000,2805,2885,14911
|
||||||
|
3372,4524,2892,3189,2875,4580,4336,3274,1349,503,2030,2652,2662,3493,2702,387,4642,2690,1707,5000,3227,4099,3669,4998,4953,3686,2619,4246,1808,2692,5000,4228,220,4295,1478,121,4374,177,4270,122,4633,5000,5000,4759,2399,1751,1647,2174,3397,4440,1645,1867,4798,1528,3100,4858,3644,3050,1281,850,3013,4341,5000,300,3573,1870,864,2458,1282,3414,1129,4450,21,3450,3637,4819,1120,5000,3095,1286,1787,5000,2826,2857,14953
|
||||||
|
3339,4494,2856,3209,2899,4614,4372,3297,1362,536,2043,2689,2632,3448,2655,356,4624,2659,1750,5000,3245,4098,3709,5000,4960,3707,2651,4205,1775,2666,5000,4182,176,4262,1497,140,4397,190,4293,163,4673,5000,5000,4724,2357,1732,1617,2122,3346,4478,1697,1894,4817,1550,3122,4890,3676,3073,1236,814,2990,4314,5000,282,3539,1876,903,2486,1294,3430,1168,4497,47,3501,3617,4765,1079,5000,3056,1245,1815,5000,2847,2891,14996
|
||||||
|
3368,4525,2882,2746,2860,4586,4345,3257,1313,568,2056,2726,2663,3465,2670,389,4669,2689,1732,5000,3200,4035,3687,4978,4905,3730,2683,4227,1805,2702,5000,4197,194,4291,1453,97,4357,141,4255,143,4713,5000,5000,4331,2377,1774,1647,2131,3358,4455,1687,1858,4774,1510,3080,4860,3708,3095,1253,840,3029,4348,5000,326,3565,1820,880,2451,1243,3385,1146,4542,73,3552,3657,4770,1099,5000,3080,1266,1781,5000,2806,2443,15040
|
||||||
|
2
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod serial;
|
||||||
|
pub mod window;
|
||||||
289
src-tauri/src/commands/serial.rs
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
use crate::serial_core::codecs::test::{export_recording_csv, TestCodec, TestCsvImporter, TestHandler};
|
||||||
|
use crate::serial_core::error::SerialError;
|
||||||
|
use crate::serial_core::record::CsvImporter;
|
||||||
|
use crate::serial_core::{TestRecording, serial};
|
||||||
|
use log::info;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
use tauri::{async_runtime::JoinHandle, AppHandle, Manager, State};
|
||||||
|
use tokio_serial::{available_ports, SerialPortBuilderExt};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
type SharedTestRecording = Arc<Mutex<TestRecording>>;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SerialConnectResponse {
|
||||||
|
pub port: String,
|
||||||
|
pub connected: bool,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SerialExportResponse {
|
||||||
|
pub path: String,
|
||||||
|
pub frame_count: usize,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SerialImportFrame {
|
||||||
|
pub data: Vec<i32>,
|
||||||
|
pub dts_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SerialImportResponse {
|
||||||
|
pub file_name: String,
|
||||||
|
pub frame_count: usize,
|
||||||
|
pub channel_count: usize,
|
||||||
|
pub frames: Vec<SerialImportFrame>,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SerialSession {
|
||||||
|
port: String,
|
||||||
|
cancel: CancellationToken,
|
||||||
|
task: JoinHandle<()>,
|
||||||
|
current_record: SharedTestRecording,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SerialConnectionState {
|
||||||
|
session: Mutex<Option<SerialSession>>,
|
||||||
|
last_record: Mutex<Option<SharedTestRecording>>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn serial_enum() -> Result<Vec<String>, SerialError> {
|
||||||
|
let ports = available_ports()
|
||||||
|
.map_err(|_| SerialError::ScanError)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|info| info.port_name)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn serial_connect(
|
||||||
|
app: AppHandle,
|
||||||
|
port: String,
|
||||||
|
state: State<'_, SerialConnectionState>,
|
||||||
|
) -> Result<SerialConnectResponse, SerialError> {
|
||||||
|
let port_name = port.trim().to_string();
|
||||||
|
if port_name.is_empty() {
|
||||||
|
return Err(SerialError::InvalidConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||||
|
if session.is_some() {
|
||||||
|
return Err(SerialError::AlreadyConnected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancel = CancellationToken::new();
|
||||||
|
let current_record = Arc::new(Mutex::new(TestRecording::new()));
|
||||||
|
let task_record = current_record.clone();
|
||||||
|
let task_cancel = cancel.clone();
|
||||||
|
let task_app = app.clone();
|
||||||
|
let task_port_name = port_name.clone();
|
||||||
|
|
||||||
|
let port = tokio_serial::new(&port_name, 115200)
|
||||||
|
.open_native_async()
|
||||||
|
.map_err(|_| SerialError::OpenError)?;
|
||||||
|
let session_started_at = Instant::now();
|
||||||
|
|
||||||
|
let task = tauri::async_runtime::spawn(async move {
|
||||||
|
let codec = TestCodec::new();
|
||||||
|
let handler = TestHandler;
|
||||||
|
|
||||||
|
if let Err(error) = serial::run_serial(
|
||||||
|
task_app.clone(),
|
||||||
|
port,
|
||||||
|
codec,
|
||||||
|
handler,
|
||||||
|
session_started_at,
|
||||||
|
task_record.clone(),
|
||||||
|
task_cancel,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("serial task exited with error: {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let manager = task_app.state::<SerialConnectionState>();
|
||||||
|
if let Ok(mut last_record) = manager.last_record.lock() {
|
||||||
|
*last_record = Some(task_record);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut session = match manager.session.lock() {
|
||||||
|
Ok(session) => session,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let should_clear = session
|
||||||
|
.as_ref()
|
||||||
|
.map(|current| current.port.as_str() == task_port_name.as_str())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if should_clear {
|
||||||
|
session.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||||
|
if session.is_some() {
|
||||||
|
cancel.cancel();
|
||||||
|
task.abort();
|
||||||
|
return Err(SerialError::AlreadyConnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
*session = Some(SerialSession {
|
||||||
|
port: port_name.clone(),
|
||||||
|
cancel,
|
||||||
|
task,
|
||||||
|
current_record
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(SerialConnectResponse {
|
||||||
|
port: port_name,
|
||||||
|
connected: true,
|
||||||
|
message: "connected".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn serial_disconnect(
|
||||||
|
state: State<'_, SerialConnectionState>,
|
||||||
|
) -> Result<SerialConnectResponse, SerialError> {
|
||||||
|
let session = {
|
||||||
|
let mut guard = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||||
|
guard.take()
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(SerialSession {
|
||||||
|
port,
|
||||||
|
cancel,
|
||||||
|
task,
|
||||||
|
current_record,
|
||||||
|
}) = session
|
||||||
|
else {
|
||||||
|
return Ok(SerialConnectResponse {
|
||||||
|
port: String::new(),
|
||||||
|
connected: false,
|
||||||
|
message: "already disconnected".to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
cancel.cancel();
|
||||||
|
let _ = task.await;
|
||||||
|
let frame_count = current_record.lock().map(|record| {
|
||||||
|
record.frames.len()
|
||||||
|
}).unwrap_or(0);
|
||||||
|
|
||||||
|
info!("last_record has {} frames", frame_count);
|
||||||
|
|
||||||
|
if let Ok(mut last_record) = state.last_record.lock() {
|
||||||
|
*last_record = Some(current_record);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Ok(SerialConnectResponse {
|
||||||
|
port,
|
||||||
|
connected: false,
|
||||||
|
message: "disconnected".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn serial_export_csv(
|
||||||
|
app: AppHandle,
|
||||||
|
state: State<'_, SerialConnectionState>,
|
||||||
|
) -> Result<SerialExportResponse, SerialError> {
|
||||||
|
let current_record = {
|
||||||
|
let session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||||
|
session
|
||||||
|
.as_ref()
|
||||||
|
.map(|current_session| current_session.current_record.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
let record = if let Some(recording) = current_record {
|
||||||
|
recording
|
||||||
|
} else {
|
||||||
|
let last_record = state.last_record.lock().map_err(|_| SerialError::StateError)?;
|
||||||
|
last_record.clone().ok_or(SerialError::NoRecordedData)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut output_dir = match app.path().desktop_dir() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(_) => std::env::current_dir().map_err(|_| SerialError::ExportError)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_millis())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
output_dir.push(format!("joyson_export_{timestamp}.csv"));
|
||||||
|
let mut file = File::create(&output_dir).map_err(|_| SerialError::ExportError)?;
|
||||||
|
|
||||||
|
let frame_count = {
|
||||||
|
let recording = record.lock().map_err(|_| SerialError::StateError)?;
|
||||||
|
if recording.frames.is_empty() {
|
||||||
|
return Err(SerialError::NoRecordedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export_recording_csv(&recording, &mut file).map_err(|_| SerialError::ExportError)?;
|
||||||
|
recording.frames.len()
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = output_dir.display().to_string();
|
||||||
|
info!("csv exported to {path}, frame_count={frame_count}");
|
||||||
|
|
||||||
|
Ok(SerialExportResponse {
|
||||||
|
path,
|
||||||
|
frame_count,
|
||||||
|
message: "exported".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn serial_import_csv(file_name: String, csv_content: String) -> Result<SerialImportResponse, SerialError> {
|
||||||
|
let mut importer = TestCsvImporter::new(file_name.as_str());
|
||||||
|
let packets = importer
|
||||||
|
.load(Cursor::new(csv_content.into_bytes()))
|
||||||
|
.map_err(|_| SerialError::ImportError)?;
|
||||||
|
|
||||||
|
if packets.is_empty() {
|
||||||
|
return Err(SerialError::NoRecordedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel_count = packets.first().map(|item| item.data.len()).unwrap_or(0);
|
||||||
|
let frame_count = packets.len();
|
||||||
|
let frames = packets
|
||||||
|
.into_iter()
|
||||||
|
.map(|packet| SerialImportFrame {
|
||||||
|
data: packet.data,
|
||||||
|
dts_ms: packet.dts_ms,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(SerialImportResponse {
|
||||||
|
file_name,
|
||||||
|
frame_count,
|
||||||
|
channel_count,
|
||||||
|
frames,
|
||||||
|
message: "imported".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
32
src-tauri/src/commands/window.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use tauri::{AppHandle, Manager, WebviewWindow};
|
||||||
|
|
||||||
|
fn main_window(app: &AppHandle) -> Result<WebviewWindow, String> {
|
||||||
|
app.get_webview_window("main")
|
||||||
|
.ok_or_else(|| "Can't find main window".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn win_minimize(app: AppHandle) -> Result<(), String> {
|
||||||
|
main_window(&app)?
|
||||||
|
.minimize()
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn win_toggle_maximize(app: AppHandle) -> Result<(), String> {
|
||||||
|
let window = main_window(&app)?;
|
||||||
|
let is_maximized = window.is_maximized().map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
if is_maximized {
|
||||||
|
window.unmaximize().map_err(|error| error.to_string())
|
||||||
|
} else {
|
||||||
|
window.maximize().map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn win_close(app: AppHandle) -> Result<(), String> {
|
||||||
|
main_window(&app)?
|
||||||
|
.close()
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
23
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
mod commands;
|
||||||
|
pub mod serial_core;
|
||||||
|
pub mod log;
|
||||||
|
use commands::serial::SerialConnectionState;
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.manage(SerialConnectionState::default())
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
commands::serial::serial_enum,
|
||||||
|
commands::serial::serial_connect,
|
||||||
|
commands::serial::serial_disconnect,
|
||||||
|
commands::serial::serial_export_csv,
|
||||||
|
commands::serial::serial_import_csv,
|
||||||
|
commands::window::win_minimize,
|
||||||
|
commands::window::win_toggle_maximize,
|
||||||
|
commands::window::win_close
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
36
src-tauri/src/log.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use fern::colors::{Color, ColoredLevelConfig};
|
||||||
|
use log::{debug, error, info, trace, warn};
|
||||||
|
use std::time::SystemTime;
|
||||||
|
pub fn setup_logger() {
|
||||||
|
let colors_line = ColoredLevelConfig::new()
|
||||||
|
.error(Color::Red)
|
||||||
|
.warn(Color::Yellow)
|
||||||
|
.info(Color::Green)
|
||||||
|
.debug(Color::White)
|
||||||
|
.trace(Color::BrightBlack);
|
||||||
|
|
||||||
|
let colors_level = colors_line.info(Color::Green);
|
||||||
|
fern::Dispatch::new()
|
||||||
|
.format(move |out, message, record| {
|
||||||
|
out.finish(
|
||||||
|
format_args!(
|
||||||
|
"{colors_line}[{data} {level} {target} {colors_line}] {message}\x1B[0m",
|
||||||
|
colors_line = format_args!(
|
||||||
|
"\x1B[{}m",
|
||||||
|
colors_line.get_color(&record.level()).to_fg_str()
|
||||||
|
),
|
||||||
|
data = humantime::format_rfc3339_seconds(SystemTime::now()),
|
||||||
|
target = record.target(),
|
||||||
|
level = colors_level.color(record.level()),
|
||||||
|
message = message,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.level(log::LevelFilter::Info)
|
||||||
|
.chain(std::io::stdout())
|
||||||
|
.chain(fern::DateBased::new("program.log", "%Y-%m-%d"))
|
||||||
|
.apply()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
debug!("logging initialized");
|
||||||
|
}
|
||||||
10
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
use log::{debug, error, info, trace, warn};
|
||||||
|
use tauri_demo_lib::log::setup_logger;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
setup_logger();
|
||||||
|
debug!("logging initialized");
|
||||||
|
tauri_demo_lib::run()
|
||||||
|
}
|
||||||
6
src-tauri/src/serial_core/codec.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use crate::serial_core::error::CodecError;
|
||||||
|
use std::time::Instant;
|
||||||
|
pub trait Codec<F> {
|
||||||
|
fn decode(&mut self, input: &[u8], session_started_at: Instant) -> Result<Vec<F>, CodecError>;
|
||||||
|
fn encode(&self, frame: &F) -> Result<Vec<u8>, CodecError>;
|
||||||
|
}
|
||||||
4
src-tauri/src/serial_core/codecs/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
use crate::serial_core::{frame::TestFrame, record::Recording};
|
||||||
|
|
||||||
|
pub mod test;
|
||||||
|
pub type TestRecording = Recording<TestFrame>;
|
||||||
258
src-tauri/src/serial_core/codecs/test.rs
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
use std::time::Instant;
|
||||||
|
use crate::serial_core::frame::{crc8, usize_to_u16_be_bytes, FrameHandler};
|
||||||
|
use crate::serial_core::{codec::Codec, error::CodecError, frame::TestFrame};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::Local;
|
||||||
|
use csv::StringRecord;
|
||||||
|
use crate::serial_core::record::{write_csv, CsvExporter, CsvImporter, RecordedFrame, Recording};
|
||||||
|
|
||||||
|
pub struct TestCodec {
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TestHandler;
|
||||||
|
|
||||||
|
impl TestCodec {
|
||||||
|
pub fn new() -> TestCodec {
|
||||||
|
Self { buffer: Vec::new() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Codec<TestFrame> for TestCodec {
|
||||||
|
fn decode(&mut self, input: &[u8], session_started_at: Instant) -> Result<Vec<TestFrame>, CodecError> {
|
||||||
|
self.buffer.extend_from_slice(input);
|
||||||
|
let mut frames = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if self.buffer.len() < 6 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let header_pos = self.buffer.windows(2).position(|w| w == [0xAA, 0x55]);
|
||||||
|
|
||||||
|
let Some(pos) = header_pos else {
|
||||||
|
self.buffer.clear();
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if pos > 0 {
|
||||||
|
self.buffer.drain(0..pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.buffer.len() < 6 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmd = self.buffer[2];
|
||||||
|
let length_bytes = [self.buffer[3], self.buffer[4]];
|
||||||
|
let length = u16::from_be_bytes(length_bytes) as usize;
|
||||||
|
let frame_length = (length + 6) as usize;
|
||||||
|
if self.buffer.len() < frame_length {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let payload = self.buffer[5..5 + length].to_vec();
|
||||||
|
let checksum = crc8(payload.as_slice());
|
||||||
|
if self.buffer[frame_length - 1] != checksum {
|
||||||
|
self.buffer.drain(0..1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let dts = elapsed_millis(session_started_at);
|
||||||
|
println!("dts_ms: {dts}");
|
||||||
|
frames.push(TestFrame {
|
||||||
|
header: [0xAA, 0x55],
|
||||||
|
cmd: cmd,
|
||||||
|
length: length,
|
||||||
|
payload: payload,
|
||||||
|
checksum: checksum,
|
||||||
|
dts_ms: dts,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.buffer.drain(0..frame_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(frames)
|
||||||
|
}
|
||||||
|
fn encode(&self, frame: &TestFrame) -> Result<Vec<u8>, CodecError> {
|
||||||
|
let _ = u16::try_from(frame.payload.len()).map_err(|_| CodecError::PayloadTooLarge)?;
|
||||||
|
let mut out = Vec::with_capacity(6 + frame.length);
|
||||||
|
out.extend_from_slice(&frame.header);
|
||||||
|
out.push(frame.cmd);
|
||||||
|
out.extend_from_slice(&usize_to_u16_be_bytes(frame.length));
|
||||||
|
out.extend_from_slice(&frame.payload);
|
||||||
|
out.push(frame.checksum);
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FrameHandler<TestFrame, i32> for TestHandler {
|
||||||
|
async fn on_frame(&mut self, frame: &TestFrame) -> anyhow::Result<Option<Vec<i32>>> {
|
||||||
|
match frame.cmd {
|
||||||
|
0x01 => {
|
||||||
|
let vals = parse_data_frame(&frame.payload)?;
|
||||||
|
Ok(Some(vals))
|
||||||
|
}
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_data_frame(data: &[u8]) -> Result<Vec<i32>, CodecError> {
|
||||||
|
if data.len() % 2 != 0 {
|
||||||
|
return Err(CodecError::InvalidLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
let vals: Vec<i32> = data
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]) as i32)
|
||||||
|
.collect::<Vec<i32>>();
|
||||||
|
|
||||||
|
Ok(vals)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn elapsed_millis(start_at: Instant) -> u64 {
|
||||||
|
start_at.elapsed().as_millis() as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TestCsvExporter;
|
||||||
|
pub struct TestCsvImporter {
|
||||||
|
channels: usize,
|
||||||
|
data_row: usize,
|
||||||
|
packets: Vec<TestDataPacket>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TestDataPacket {
|
||||||
|
pub data: Vec<i32>,
|
||||||
|
pub dts_ms: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&TestFrame> for TestDataPacket {
|
||||||
|
type Error = CodecError;
|
||||||
|
fn try_from(frame: &TestFrame) -> Result<TestDataPacket, Self::Error> {
|
||||||
|
let data = parse_data_frame(&frame.payload)?;
|
||||||
|
let dts = frame.dts_ms;
|
||||||
|
Ok(TestDataPacket { data: data, dts_ms: dts })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// impl From<TestFrame> for TestDataPacket {
|
||||||
|
// fn from(frame: TestFrame) -> Self {
|
||||||
|
// let data = parse_data_frame(&frame.payload)?;
|
||||||
|
// let dts = frame.dts_ms;
|
||||||
|
// TestDataPacket { data: data, dts_ms: dts }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
impl CsvExporter<TestFrame> for TestCsvExporter {
|
||||||
|
type Error = CodecError;
|
||||||
|
fn csv_header(&self, recording: &Recording<TestFrame>) -> Vec<String> {
|
||||||
|
let channel_nb = recording
|
||||||
|
.frames
|
||||||
|
.iter()
|
||||||
|
.find_map(|frame| parse_data_frame(&frame.frame.payload).ok().map(|vals| vals.len()))
|
||||||
|
.unwrap_or(0);
|
||||||
|
let mut header: Vec<String> = Vec::new();
|
||||||
|
for i in 0..channel_nb {
|
||||||
|
header.push(format!("channel{}", i + 1));
|
||||||
|
}
|
||||||
|
header.push("dts".to_string());
|
||||||
|
|
||||||
|
header
|
||||||
|
}
|
||||||
|
|
||||||
|
fn csv_row(&self, item: &RecordedFrame<TestFrame>) -> anyhow::Result<Vec<String>> {
|
||||||
|
let packet: TestDataPacket = TestDataPacket::try_from(&item.frame)?;
|
||||||
|
let mut row: Vec<String> = packet.data.iter().map(|&x| x.to_string()).collect();
|
||||||
|
row.push(packet.dts_ms.to_string());
|
||||||
|
Ok(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestCsvImporter {
|
||||||
|
pub fn new(_path: &str) -> TestCsvImporter {
|
||||||
|
Self {
|
||||||
|
channels: 0,
|
||||||
|
data_row: 0,
|
||||||
|
packets: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_record(&mut self, record: StringRecord) -> anyhow::Result<TestDataPacket>{
|
||||||
|
if self.channels == 0 {
|
||||||
|
return Err(anyhow!("csv header is missing channel columns"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.len() < self.channels + 1 {
|
||||||
|
return Err(anyhow!("csv row has insufficient columns"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data = Vec::with_capacity(self.channels);
|
||||||
|
for index in 0..self.channels {
|
||||||
|
let cell = record.get(index).ok_or_else(|| anyhow!("missing channel cell"))?;
|
||||||
|
data.push(cell.parse::<i32>()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dts_cell = record
|
||||||
|
.get(self.channels)
|
||||||
|
.ok_or_else(|| anyhow!("missing dts cell"))?;
|
||||||
|
let dts_ms = dts_cell.parse::<u64>()?;
|
||||||
|
|
||||||
|
Ok(TestDataPacket {
|
||||||
|
data: data,
|
||||||
|
dts_ms: dts_ms,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CsvImporter<TestDataPacket> for TestCsvImporter {
|
||||||
|
fn load<R: Read>(&mut self, reader: R) -> anyhow::Result<Vec<TestDataPacket>> {
|
||||||
|
let mut rdr = csv::Reader::from_reader(reader);
|
||||||
|
let headers = rdr.headers()?.clone();
|
||||||
|
self.channels = headers.len().saturating_sub(1);
|
||||||
|
self.data_row = 0;
|
||||||
|
self.packets.clear();
|
||||||
|
|
||||||
|
for record in rdr.records() {
|
||||||
|
let record = record?;
|
||||||
|
let packet = self.parse_record(record)?;
|
||||||
|
self.packets.push(packet);
|
||||||
|
self.data_row += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.packets.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn export_recording_csv<W>(recording: &Recording<TestFrame>, writer: W) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
W: std::io::Write,
|
||||||
|
{
|
||||||
|
let now = Local::now();
|
||||||
|
let filename = format!("joyson_{}", now.format("%Y%m%d_%H%M%S"));
|
||||||
|
write_csv(recording, &TestCsvExporter, &filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use csv::Reader;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_csv_basic() -> anyhow::Result<()> {
|
||||||
|
let mut rdr = Reader::from_path("recording_20260329_125238.csv")?;
|
||||||
|
let headers = rdr.headers()?;
|
||||||
|
println!("headers: {:?}", headers);
|
||||||
|
|
||||||
|
for result in rdr.records() {
|
||||||
|
let record = result?;
|
||||||
|
println!("record: {:?}", record);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src-tauri/src/serial_core/error.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub enum SerialError {
|
||||||
|
OpenError,
|
||||||
|
CloseError,
|
||||||
|
ScanError,
|
||||||
|
InvalidConfig,
|
||||||
|
AlreadyConnected,
|
||||||
|
StateError,
|
||||||
|
NoRecordedData,
|
||||||
|
ExportError,
|
||||||
|
ImportError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SerialError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
SerialError::OpenError => write!(f, "Opening Error"),
|
||||||
|
SerialError::CloseError => write!(f, "Closing Error"),
|
||||||
|
SerialError::ScanError => write!(f, "Scan Error"),
|
||||||
|
SerialError::InvalidConfig => write!(f, "Invalid Config"),
|
||||||
|
SerialError::AlreadyConnected => write!(f, "Already Connected"),
|
||||||
|
SerialError::StateError => write!(f, "State Error"),
|
||||||
|
SerialError::NoRecordedData => write!(f, "No Recorded Data"),
|
||||||
|
SerialError::ExportError => write!(f, "Export Error"),
|
||||||
|
SerialError::ImportError => write!(f, "Import Error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CodecError {
|
||||||
|
InvalidHeader,
|
||||||
|
InvalidTail,
|
||||||
|
InvalidLength,
|
||||||
|
PayloadTooLarge,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CodecError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
CodecError::InvalidHeader => write!(f, "Invalid Header"),
|
||||||
|
CodecError::InvalidTail => write!(f, "Invalid Tail"),
|
||||||
|
CodecError::InvalidLength => write!(f, "Invalid Length"),
|
||||||
|
CodecError::PayloadTooLarge => write!(f, "Payload too large"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for CodecError {}
|
||||||
43
src-tauri/src/serial_core/frame.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TestFrame {
|
||||||
|
pub header: [u8; 2],
|
||||||
|
pub cmd: u8,
|
||||||
|
pub length: usize,
|
||||||
|
pub payload: Vec<u8>,
|
||||||
|
pub checksum: u8,
|
||||||
|
pub dts_ms: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait FrameHandler<F, T>: Send {
|
||||||
|
async fn on_frame(&mut self, frame: &F) -> Result<Option<Vec<T>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn usize_to_u16_be_bytes(n: usize) -> [u8; 2] {
|
||||||
|
(n as u16).to_be_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn usize_to_u16_le_bytes(n: usize) -> [u8; 2] {
|
||||||
|
(n as u16).to_be_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn crc8(data: &[u8]) -> u8 {
|
||||||
|
let mut crc: u8 = 0x00;
|
||||||
|
|
||||||
|
for &byte in data {
|
||||||
|
crc ^= byte;
|
||||||
|
for _ in 0..8 {
|
||||||
|
if (crc & 0x80) != 0 {
|
||||||
|
crc = (crc << 1) ^ 0x07;
|
||||||
|
} else {
|
||||||
|
crc <<= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
crc
|
||||||
|
}
|
||||||
27
src-tauri/src/serial_core/mod.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use crate::serial_core::{frame::TestFrame, record::Recording};
|
||||||
|
|
||||||
|
pub mod codec;
|
||||||
|
pub mod codecs;
|
||||||
|
pub mod error;
|
||||||
|
pub mod frame;
|
||||||
|
pub mod model;
|
||||||
|
pub mod serial;
|
||||||
|
pub mod record;
|
||||||
|
|
||||||
|
pub type TestRecording = Recording<TestFrame>;
|
||||||
|
|
||||||
|
pub struct SerialConnection {
|
||||||
|
pub port: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect(port: &str) -> Result<SerialConnection, String> {
|
||||||
|
let port = port.trim();
|
||||||
|
|
||||||
|
if port.is_empty() {
|
||||||
|
return Err("Serial port is required".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SerialConnection {
|
||||||
|
port: port.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
500
src-tauri/src/serial_core/model.rs
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
use crate::serial_core::frame::TestFrame;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
const MAX_POINTS: usize = 28;
|
||||||
|
const MAX_SUMMARY_POINTS: usize = 42;
|
||||||
|
const PANEL_STALE_AFTER: Duration = Duration::from_millis(2400);
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HudPacket {
|
||||||
|
pub ts: u64,
|
||||||
|
pub panels: Vec<HudSignalPanel>,
|
||||||
|
pub summary: HudSummary,
|
||||||
|
pub pressure_matrix: Option<Vec<f32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HudSummary {
|
||||||
|
pub label: String,
|
||||||
|
pub points: Vec<f32>,
|
||||||
|
pub latest: Option<f32>,
|
||||||
|
pub min: Option<f32>,
|
||||||
|
pub max: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone, Copy)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum HudPanelSide {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone, Copy)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum HudTone {
|
||||||
|
Cyan,
|
||||||
|
Lime,
|
||||||
|
Orange,
|
||||||
|
Violet,
|
||||||
|
Gold,
|
||||||
|
Rose,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HudSignalPanel {
|
||||||
|
pub id: String,
|
||||||
|
pub code: String,
|
||||||
|
pub title: String,
|
||||||
|
pub side: HudPanelSide,
|
||||||
|
pub active: bool,
|
||||||
|
pub series: Vec<HudSignalSeries>,
|
||||||
|
pub icons: Vec<HudSignalIcon>,
|
||||||
|
pub latest: Option<f32>,
|
||||||
|
pub min: Option<f32>,
|
||||||
|
pub max: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HudSignalSeries {
|
||||||
|
pub id: String,
|
||||||
|
pub tone: HudTone,
|
||||||
|
pub points: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HudSignalIcon {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
pub tone: HudTone,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HudPanelUpdate {
|
||||||
|
source_id: String,
|
||||||
|
values: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PanelEntry {
|
||||||
|
panel: HudSignalPanel,
|
||||||
|
last_seen: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HudChartState {
|
||||||
|
panels: HashMap<String, PanelEntry>,
|
||||||
|
order: Vec<String>,
|
||||||
|
summary_points: Vec<f32>,
|
||||||
|
pressure_matrix: Option<Vec<f32>>,
|
||||||
|
last_frame_seen: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HudChartState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
panels: HashMap::new(),
|
||||||
|
order: Vec::new(),
|
||||||
|
summary_points: Vec::new(),
|
||||||
|
pressure_matrix: None,
|
||||||
|
last_frame_seen: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_summary(&mut self, value: f32) {
|
||||||
|
push_summary_point(&mut self.summary_points, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_pressure_matrix(&mut self, values: &[i32]) {
|
||||||
|
if values.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pressure_matrix = Some(values.iter().map(|value| *value as f32).collect());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_frame(&mut self, frame: &TestFrame, decoded_values: Option<&[i32]>) -> HudPacket {
|
||||||
|
let now = Instant::now();
|
||||||
|
self.last_frame_seen = Some(now);
|
||||||
|
|
||||||
|
for update in expand_frame_updates(frame, decoded_values) {
|
||||||
|
self.apply_update(update, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prune_stale_at(now);
|
||||||
|
self.snapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prune_stale(&mut self) -> Option<HudPacket> {
|
||||||
|
let before = self.panels.len();
|
||||||
|
let summary_points_before = self.summary_points.len();
|
||||||
|
self.prune_stale_at(Instant::now());
|
||||||
|
|
||||||
|
if before == self.panels.len() && summary_points_before == self.summary_points.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(self.snapshot())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_update(&mut self, update: HudPanelUpdate, now: Instant) {
|
||||||
|
if update.values.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.panels.contains_key(&update.source_id) {
|
||||||
|
let next_side = side_for_index(self.order.len());
|
||||||
|
self.order.push(update.source_id.clone());
|
||||||
|
self.panels.insert(
|
||||||
|
update.source_id.clone(),
|
||||||
|
PanelEntry {
|
||||||
|
panel: build_panel(&update.source_id, next_side, update.values.len()),
|
||||||
|
last_seen: now,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = self
|
||||||
|
.panels
|
||||||
|
.get_mut(&update.source_id)
|
||||||
|
.expect("panel entry should exist after insertion");
|
||||||
|
|
||||||
|
entry.last_seen = now;
|
||||||
|
entry.panel.active = true;
|
||||||
|
ensure_panel_channels(&mut entry.panel, update.values.len());
|
||||||
|
|
||||||
|
for (index, value) in update.values.into_iter().enumerate() {
|
||||||
|
if let Some(series) = entry.panel.series.get_mut(index) {
|
||||||
|
push_point(&mut series.points, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_panel_stats(&mut entry.panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prune_stale_at(&mut self, now: Instant) {
|
||||||
|
self.panels
|
||||||
|
.retain(|_, entry| now.duration_since(entry.last_seen) <= PANEL_STALE_AFTER);
|
||||||
|
self.order.retain(|id| self.panels.contains_key(id));
|
||||||
|
|
||||||
|
let summary_stale = self
|
||||||
|
.last_frame_seen
|
||||||
|
.map(|last_seen| now.duration_since(last_seen) > PANEL_STALE_AFTER)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if summary_stale {
|
||||||
|
self.summary_points.clear();
|
||||||
|
self.pressure_matrix = None;
|
||||||
|
self.last_frame_seen = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(&mut self) -> HudPacket {
|
||||||
|
self.rebalance_sides();
|
||||||
|
|
||||||
|
let panels = self
|
||||||
|
.order
|
||||||
|
.iter()
|
||||||
|
.filter_map(|id| self.panels.get(id).map(|entry| entry.panel.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
HudPacket {
|
||||||
|
ts: now_millis(),
|
||||||
|
panels,
|
||||||
|
summary: build_summary(&self.summary_points),
|
||||||
|
pressure_matrix: self.pressure_matrix.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebalance_sides(&mut self) {
|
||||||
|
for (index, id) in self.order.iter().enumerate() {
|
||||||
|
if let Some(entry) = self.panels.get_mut(id) {
|
||||||
|
entry.panel.side = side_for_index(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HudChartState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_panel(source_id: &str, side: HudPanelSide, channel_count: usize) -> HudSignalPanel {
|
||||||
|
HudSignalPanel {
|
||||||
|
id: format!("panel-{source_id}"),
|
||||||
|
code: source_id.to_string(),
|
||||||
|
title: format!("Source {source_id}"),
|
||||||
|
side,
|
||||||
|
active: true,
|
||||||
|
series: build_panel_series(source_id, channel_count, &[]),
|
||||||
|
icons: build_panel_icons(source_id, channel_count),
|
||||||
|
latest: None,
|
||||||
|
min: None,
|
||||||
|
max: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_frame_updates(frame: &TestFrame, decoded_values: Option<&[i32]>) -> Vec<HudPanelUpdate> {
|
||||||
|
if let Some(values) = decoded_values {
|
||||||
|
if values.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec![HudPanelUpdate {
|
||||||
|
source_id: format_source_id(frame.cmd),
|
||||||
|
values: values.iter().map(|value| *value as f32).collect(),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunks = frame.payload.chunks_exact(4);
|
||||||
|
|
||||||
|
if !frame.payload.is_empty() && chunks.remainder().is_empty() {
|
||||||
|
return chunks.map(build_update_from_chunk).collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![HudPanelUpdate {
|
||||||
|
source_id: format_source_id(frame.cmd),
|
||||||
|
values: fallback_values(frame),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_update_from_chunk(chunk: &[u8]) -> HudPanelUpdate {
|
||||||
|
HudPanelUpdate {
|
||||||
|
source_id: format_source_id(chunk[0]),
|
||||||
|
values: chunk[1..]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, byte)| normalize_value(*byte, tone_for_index(index)))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_values(frame: &TestFrame) -> Vec<f32> {
|
||||||
|
let mut bytes = frame.payload.clone();
|
||||||
|
|
||||||
|
if bytes.is_empty() {
|
||||||
|
bytes.extend([
|
||||||
|
frame.cmd,
|
||||||
|
frame.length as u8,
|
||||||
|
frame.checksum,
|
||||||
|
frame.cmd.wrapping_add(frame.checksum),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
while bytes.len() < 3 {
|
||||||
|
let previous = *bytes.last().unwrap_or(&frame.cmd);
|
||||||
|
bytes.push(
|
||||||
|
previous
|
||||||
|
.wrapping_add(frame.cmd)
|
||||||
|
.wrapping_add(bytes.len() as u8),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, byte)| normalize_value(byte, tone_for_index(index)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_value(byte: u8, tone: HudTone) -> f32 {
|
||||||
|
let base = (byte as f32 / 255.0) * 100.0;
|
||||||
|
let offset = match tone {
|
||||||
|
HudTone::Cyan => 6.0,
|
||||||
|
HudTone::Lime => 0.0,
|
||||||
|
HudTone::Orange => -6.0,
|
||||||
|
HudTone::Violet => 10.0,
|
||||||
|
HudTone::Gold => -10.0,
|
||||||
|
HudTone::Rose => 3.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
(base + offset).clamp(0.0, 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_source_id(byte: u8) -> String {
|
||||||
|
if byte.is_ascii_alphanumeric() {
|
||||||
|
(byte as char).to_ascii_uppercase().to_string()
|
||||||
|
} else {
|
||||||
|
format!("CH{:02X}", byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn side_for_index(index: usize) -> HudPanelSide {
|
||||||
|
if index % 2 == 0 {
|
||||||
|
HudPanelSide::Left
|
||||||
|
} else {
|
||||||
|
HudPanelSide::Right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_point(points: &mut Vec<f32>, value: f32) {
|
||||||
|
if points.len() >= MAX_POINTS {
|
||||||
|
points.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
points.push((value * 10.0).round() / 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_panel_series(
|
||||||
|
source_id: &str,
|
||||||
|
channel_count: usize,
|
||||||
|
previous: &[HudSignalSeries],
|
||||||
|
) -> Vec<HudSignalSeries> {
|
||||||
|
(0..channel_count)
|
||||||
|
.map(|index| HudSignalSeries {
|
||||||
|
id: format!("{source_id}-series-{}", index + 1),
|
||||||
|
tone: tone_for_index(index),
|
||||||
|
points: previous
|
||||||
|
.get(index)
|
||||||
|
.map(|series| series.points.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_panel_icons(source_id: &str, channel_count: usize) -> Vec<HudSignalIcon> {
|
||||||
|
(0..channel_count)
|
||||||
|
.map(|index| HudSignalIcon {
|
||||||
|
id: format!("{source_id}-icon-{}", index + 1),
|
||||||
|
label: if channel_count == 1 {
|
||||||
|
"TOTAL".to_string()
|
||||||
|
} else {
|
||||||
|
format!("{source_id}-{}", index + 1)
|
||||||
|
},
|
||||||
|
tone: tone_for_index(index),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_panel_channels(panel: &mut HudSignalPanel, channel_count: usize) {
|
||||||
|
if panel.series.len() == channel_count && panel.icons.len() == channel_count {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.series = build_panel_series(&panel.code, channel_count, &panel.series);
|
||||||
|
panel.icons = build_panel_icons(&panel.code, channel_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_panel_stats(panel: &mut HudSignalPanel) {
|
||||||
|
let latest_values: Vec<f32> = panel
|
||||||
|
.series
|
||||||
|
.iter()
|
||||||
|
.filter_map(|series| series.points.last().copied())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
panel.latest = if latest_values.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(latest_values.iter().sum::<f32>() / latest_values.len() as f32)
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.min = panel
|
||||||
|
.series
|
||||||
|
.iter()
|
||||||
|
.flat_map(|series| series.points.iter().copied())
|
||||||
|
.reduce(f32::min);
|
||||||
|
|
||||||
|
panel.max = panel
|
||||||
|
.series
|
||||||
|
.iter()
|
||||||
|
.flat_map(|series| series.points.iter().copied())
|
||||||
|
.reduce(f32::max);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tone_for_index(index: usize) -> HudTone {
|
||||||
|
match index % 6 {
|
||||||
|
0 => HudTone::Cyan,
|
||||||
|
1 => HudTone::Lime,
|
||||||
|
2 => HudTone::Orange,
|
||||||
|
3 => HudTone::Violet,
|
||||||
|
4 => HudTone::Gold,
|
||||||
|
_ => HudTone::Rose,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_summary_point(points: &mut Vec<f32>, value: f32) {
|
||||||
|
if points.len() >= MAX_SUMMARY_POINTS {
|
||||||
|
points.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
points.push((value * 10.0).round() / 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_summary(points: &[f32]) -> HudSummary {
|
||||||
|
HudSummary {
|
||||||
|
label: "TOTAL".to_string(),
|
||||||
|
points: points.to_vec(),
|
||||||
|
latest: points.last().copied(),
|
||||||
|
min: points.iter().copied().reduce(f32::min),
|
||||||
|
max: points.iter().copied().reduce(f32::max),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now_millis() -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_millis() as u64)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[cfg(test)]
|
||||||
|
// mod tests {
|
||||||
|
// use super::*;
|
||||||
|
//
|
||||||
|
// fn sample_frame() -> TestFrame {
|
||||||
|
// TestFrame {
|
||||||
|
// header: [0xAA, 0x55],
|
||||||
|
// cmd: 0x01,
|
||||||
|
// length: 4,
|
||||||
|
// payload: vec![0x00, 0x0A, 0x00, 0x14],
|
||||||
|
// checksum: 0,
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[test]
|
||||||
|
// fn prune_stale_clears_panels_and_summary_after_timeout() {
|
||||||
|
// let mut state = HudChartState::new();
|
||||||
|
// let frame = sample_frame();
|
||||||
|
//
|
||||||
|
// state.record_summary(30.0);
|
||||||
|
// let _ = state.apply_frame(&frame, Some(&[10, 20]));
|
||||||
|
//
|
||||||
|
// let stale_now = Instant::now();
|
||||||
|
// let stale_seen = stale_now - PANEL_STALE_AFTER - Duration::from_millis(1);
|
||||||
|
//
|
||||||
|
// state.last_frame_seen = Some(stale_seen);
|
||||||
|
//
|
||||||
|
// for entry in state.panels.values_mut() {
|
||||||
|
// entry.last_seen = stale_seen;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let packet = state
|
||||||
|
// .prune_stale()
|
||||||
|
// .expect("stale data should emit an update");
|
||||||
|
//
|
||||||
|
// assert!(packet.panels.is_empty());
|
||||||
|
// assert!(packet.summary.points.is_empty());
|
||||||
|
// assert!(state.panels.is_empty());
|
||||||
|
// assert!(state.summary_points.is_empty());
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[test]
|
||||||
|
// fn prune_stale_keeps_recent_summary_points() {
|
||||||
|
// let mut state = HudChartState::new();
|
||||||
|
// let frame = sample_frame();
|
||||||
|
//
|
||||||
|
// state.record_summary(30.0);
|
||||||
|
// let _ = state.apply_frame(&frame, Some(&[10, 20]));
|
||||||
|
//
|
||||||
|
// state.last_frame_seen = Some(Instant::now());
|
||||||
|
//
|
||||||
|
// assert!(state.prune_stale().is_none());
|
||||||
|
// assert_eq!(state.summary_points, vec![30.0]);
|
||||||
|
// assert_eq!(state.panels.len(), 1);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
64
src-tauri/src/serial_core/record.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use std::fs::{write, File};
|
||||||
|
use std::io;
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use csv::Reader;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FrameTiming {
|
||||||
|
pub pts_ms: Option<u64>,
|
||||||
|
pub dts_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RecordedFrame<F> {
|
||||||
|
pub timing: FrameTiming,
|
||||||
|
pub frame: F
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct Recording<F> {
|
||||||
|
pub frames: Vec<RecordedFrame<F>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F> Recording<F> {
|
||||||
|
pub fn new() -> Recording<F> { Self { frames: Vec::new() } }
|
||||||
|
pub fn push(&mut self, ite: RecordedFrame<F>) {
|
||||||
|
self.frames.push(ite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CsvExporter<F> {
|
||||||
|
type Error: std::error::Error + Send + Sync + 'static;
|
||||||
|
fn csv_header(&self, recording: &Recording<F>) -> Vec<String>;
|
||||||
|
fn csv_row(&self, item: &RecordedFrame<F>) -> anyhow::Result<Vec<String>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: CsvImporter
|
||||||
|
pub trait CsvImporter<P> {
|
||||||
|
fn load<R: std::io::Read>(&mut self, reader: R) -> anyhow::Result<Vec<P>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_csv<F, E>(
|
||||||
|
recording: &Recording<F>,
|
||||||
|
exporter: &E,
|
||||||
|
path: &str
|
||||||
|
// mut writer: W,
|
||||||
|
) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
E: CsvExporter<F>,
|
||||||
|
// W: std::io::Write
|
||||||
|
{
|
||||||
|
let header = exporter.csv_header(&recording);
|
||||||
|
// let mut wrt = csv::Writer::from_writer(io::stdout());
|
||||||
|
|
||||||
|
let mut wrt = csv::Writer::from_path(format!("{}.csv", path))?;
|
||||||
|
wrt.write_record(header)?;
|
||||||
|
for f in &recording.frames {
|
||||||
|
let row = exporter.csv_row(f)?;
|
||||||
|
wrt.write_record(&row)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrt.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
80
src-tauri/src/serial_core/serial.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
use crate::serial_core::codec::Codec;
|
||||||
|
use crate::serial_core::frame::{FrameHandler, TestFrame};
|
||||||
|
use crate::serial_core::model::HudChartState;
|
||||||
|
use anyhow::Result;
|
||||||
|
use tauri::{AppHandle, Emitter};
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
use tokio::time::{self, Duration, MissedTickBehavior};
|
||||||
|
use tokio_serial::SerialStream;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Instant;
|
||||||
|
use log::info;
|
||||||
|
use crate::serial_core::record::{FrameTiming, RecordedFrame};
|
||||||
|
use crate::serial_core::TestRecording;
|
||||||
|
|
||||||
|
pub async fn run_serial<C, H, T>(
|
||||||
|
app: AppHandle,
|
||||||
|
mut port: SerialStream,
|
||||||
|
mut codec: C,
|
||||||
|
mut handler: H,
|
||||||
|
session_started_at: Instant,
|
||||||
|
recording: Arc<Mutex<TestRecording>>,
|
||||||
|
cancel: CancellationToken,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
C: Codec<TestFrame> + Send + 'static,
|
||||||
|
H: FrameHandler<TestFrame, T> + Send + 'static,
|
||||||
|
T: Into<i32>,
|
||||||
|
{
|
||||||
|
let mut chart_state = HudChartState::new();
|
||||||
|
let mut buffer = [0u8; 1024];
|
||||||
|
let mut prune_interval = time::interval(Duration::from_millis(450));
|
||||||
|
prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel.cancelled() => break,
|
||||||
|
_ = prune_interval.tick() => {
|
||||||
|
if let Some(packet) = chart_state.prune_stale() {
|
||||||
|
app.emit("hud_stream", packet)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read_result = port.read(&mut buffer) => {
|
||||||
|
let n = read_result?;
|
||||||
|
if n == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let frames = codec.decode(&buffer[..n], session_started_at)?;
|
||||||
|
for frame in frames {
|
||||||
|
let decode_res = handler
|
||||||
|
.on_frame(&frame)
|
||||||
|
.await?
|
||||||
|
.map(|vals| vals.into_iter().map(Into::into).collect::<Vec<i32>>());
|
||||||
|
|
||||||
|
let mut record = recording.lock().map_err(|_| anyhow::anyhow!("recording state poisoned"))?;
|
||||||
|
record.push(RecordedFrame{
|
||||||
|
timing: FrameTiming { pts_ms: None, dts_ms: frame.dts_ms },
|
||||||
|
frame: frame.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let display_values = if let Some(vals) = decode_res.as_ref() {
|
||||||
|
let summary = vals.iter().copied().sum::<i32>();
|
||||||
|
info!("dot value summary: {}", summary);
|
||||||
|
chart_state.record_summary(summary as f32);
|
||||||
|
chart_state.record_pressure_matrix(vals.as_slice());
|
||||||
|
Some(vec![summary])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let packet = chart_state.apply_frame(&frame, display_values.as_deref());
|
||||||
|
app.emit("hud_stream", packet)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
36
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "tauri-demo",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.lenn.tauri-serial",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"frontendDist": "../build"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "joyson-serial",
|
||||||
|
"width": 1366,
|
||||||
|
"height": 860,
|
||||||
|
"decorations": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.ico",
|
||||||
|
"icons/icon.png"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/app.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Tauri + SvelteKit + Typescript App</title>
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
774
src/lib/components/CenterStage.svelte
Normal file
@@ -0,0 +1,774 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import { flip } from "svelte/animate";
|
||||||
|
import { cubicIn, cubicOut } from "svelte/easing";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
|
import ConfigPanel from "$lib/components/ConfigPanel.svelte";
|
||||||
|
import PressureMatrixViewer from "$lib/components/PressureMatrixViewer.svelte";
|
||||||
|
import SignalChart from "$lib/components/SignalChart.svelte";
|
||||||
|
import SummaryCurve from "$lib/components/SummaryCurve.svelte";
|
||||||
|
import type {
|
||||||
|
HudColorMapOption,
|
||||||
|
HudSignalPanel,
|
||||||
|
HudSummary,
|
||||||
|
PressureColorMapPreset,
|
||||||
|
StageStatusTone
|
||||||
|
} from "$lib/types/hud";
|
||||||
|
|
||||||
|
export let title = "";
|
||||||
|
export let hint = "";
|
||||||
|
export let statusText = "";
|
||||||
|
export let statusTone: StageStatusTone = "idle";
|
||||||
|
export let leftPanels: HudSignalPanel[] = [];
|
||||||
|
export let rightPanels: HudSignalPanel[] = [];
|
||||||
|
export let summary: HudSummary;
|
||||||
|
export let pressureMatrix: number[] | null = null;
|
||||||
|
export let showConfigPanel = false;
|
||||||
|
export let configPanelTitle = "";
|
||||||
|
export let configPanelHint = "";
|
||||||
|
export let matrixSizeLabel = "";
|
||||||
|
export let matrixRowsLabel = "";
|
||||||
|
export let matrixColsLabel = "";
|
||||||
|
export let rangeLabel = "";
|
||||||
|
export let rangeMinLabel = "";
|
||||||
|
export let rangeMaxLabel = "";
|
||||||
|
export let colorMapLabel = "";
|
||||||
|
export let resetConfigLabel = "";
|
||||||
|
export let applyLiveHint = "";
|
||||||
|
export let matrixRows = 12;
|
||||||
|
export let matrixCols = 7;
|
||||||
|
export let rangeMin = 0;
|
||||||
|
export let rangeMax = 5000;
|
||||||
|
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
||||||
|
export let colorMapOptions: HudColorMapOption[] = [];
|
||||||
|
export let replaySectionLabel = "";
|
||||||
|
export let replayPlayLabel = "";
|
||||||
|
export let replayPauseLabel = "";
|
||||||
|
export let replayStopLabel = "";
|
||||||
|
export let replaySpeedLabel = "";
|
||||||
|
export let replayProgressLabel = "";
|
||||||
|
export let replayHasData = false;
|
||||||
|
export let replayIsPlaying = false;
|
||||||
|
export let replaySpeed = 1;
|
||||||
|
export let replayProgress = 0;
|
||||||
|
export let replayFileName = "";
|
||||||
|
export let replayFrameInfo = "";
|
||||||
|
|
||||||
|
let stagePlaneEl: HTMLDivElement | undefined;
|
||||||
|
let topOverlayEl: HTMLDivElement | undefined;
|
||||||
|
let panelZoneEl: HTMLDivElement | undefined;
|
||||||
|
let leftStackEl: HTMLDivElement | undefined;
|
||||||
|
let rightStackEl: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
let panelZoneTopPx = 88;
|
||||||
|
let leftRailScale = 1;
|
||||||
|
let rightRailScale = 1;
|
||||||
|
let summarySide: "left" | "right" = "left";
|
||||||
|
let replaySide: "left" | "right" = "right";
|
||||||
|
|
||||||
|
const minRailScale = 0.2;
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
configclose: void;
|
||||||
|
replaytoggle: void;
|
||||||
|
replaystop: void;
|
||||||
|
replayseek: number;
|
||||||
|
replayspeed: number;
|
||||||
|
replayclose: void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
$: summarySide = leftPanels.length <= rightPanels.length ? "left" : "right";
|
||||||
|
$: replaySide = summarySide === "left" ? "right" : "left";
|
||||||
|
$: replayToggleButtonText = replayIsPlaying ? replayPauseLabel : replayPlayLabel;
|
||||||
|
$: replayProgressPercent = Math.round(Math.min(1, Math.max(0, replayProgress)) * 100);
|
||||||
|
|
||||||
|
function toPxNumber(rawValue: string): number {
|
||||||
|
const value = Number.parseFloat(rawValue);
|
||||||
|
return Number.isFinite(value) ? value : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateScale(zoneHeight: number, stackHeight: number): number {
|
||||||
|
if (zoneHeight <= 0 || stackHeight <= 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawScale = (zoneHeight - 6) / stackHeight;
|
||||||
|
return clamp(Math.round(rawScale * 1000) / 1000, minRailScale, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function recomputePanelLayout(): void {
|
||||||
|
if (!stagePlaneEl || !topOverlayEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const planeRect = stagePlaneEl.getBoundingClientRect();
|
||||||
|
const overlayRect = topOverlayEl.getBoundingClientRect();
|
||||||
|
const overlayBottom = overlayRect.bottom - planeRect.top;
|
||||||
|
const upperTopLimit = Math.max(72, Math.round(stagePlaneEl.clientHeight * 0.34));
|
||||||
|
panelZoneTopPx = clamp(Math.round(overlayBottom + 8), 56, upperTopLimit);
|
||||||
|
|
||||||
|
const panelZoneBottomPx = panelZoneEl ? toPxNumber(getComputedStyle(panelZoneEl).bottom) : 0;
|
||||||
|
const zoneHeight = Math.max(0, stagePlaneEl.clientHeight - panelZoneTopPx - panelZoneBottomPx);
|
||||||
|
|
||||||
|
leftRailScale = calculateScale(zoneHeight, leftStackEl?.scrollHeight ?? 0);
|
||||||
|
rightRailScale = calculateScale(zoneHeight, rightStackEl?.scrollHeight ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitReplayToggle(): void {
|
||||||
|
dispatch("replaytoggle");
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitReplayStop(): void {
|
||||||
|
dispatch("replaystop");
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitReplaySeek(event: Event): void {
|
||||||
|
const target = event.currentTarget as HTMLInputElement;
|
||||||
|
const normalized = Number(target.value) / 100;
|
||||||
|
dispatch("replayseek", Number.isFinite(normalized) ? normalized : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitReplaySpeed(event: Event): void {
|
||||||
|
const target = event.currentTarget as HTMLSelectElement;
|
||||||
|
const nextSpeed = Number(target.value);
|
||||||
|
dispatch("replayspeed", Number.isFinite(nextSpeed) ? nextSpeed : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitReplayClose(): void {
|
||||||
|
dispatch("replayclose");
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
recomputePanelLayout();
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
recomputePanelLayout();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stagePlaneEl) {
|
||||||
|
resizeObserver.observe(stagePlaneEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topOverlayEl) {
|
||||||
|
resizeObserver.observe(topOverlayEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftStackEl) {
|
||||||
|
resizeObserver.observe(leftStackEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightStackEl) {
|
||||||
|
resizeObserver.observe(rightStackEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", recomputePanelLayout);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
window.removeEventListener("resize", recomputePanelLayout);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="center-stage" aria-label="WebGL2 Area">
|
||||||
|
<article class="stage-shell">
|
||||||
|
<div
|
||||||
|
class="stage-canvas-plane"
|
||||||
|
bind:this={stagePlaneEl}
|
||||||
|
style="--panel-zone-top-dyn: {panelZoneTopPx}px; --rail-scale-left: {leftRailScale}; --rail-scale-right: {rightRailScale};"
|
||||||
|
>
|
||||||
|
<div class="stage-top-overlay" bind:this={topOverlayEl}>
|
||||||
|
<div class="stage-meta">
|
||||||
|
<p class="meta-label">WebGL2 Stage</p>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<p class="meta-hint">{hint}</p>
|
||||||
|
</div>
|
||||||
|
<p class="runtime-status" class:is-ok={statusTone === "ok"} class:is-warn={statusTone === "warn"}>
|
||||||
|
{statusText}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="canvas-wrap">
|
||||||
|
{#key `${matrixRows}x${matrixCols}`}
|
||||||
|
<PressureMatrixViewer
|
||||||
|
{pressureMatrix}
|
||||||
|
{matrixRows}
|
||||||
|
{matrixCols}
|
||||||
|
{rangeMin}
|
||||||
|
{rangeMax}
|
||||||
|
{colorMapPreset}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showConfigPanel}
|
||||||
|
<div class="config-panel-wrap">
|
||||||
|
<ConfigPanel
|
||||||
|
bind:matrixRows
|
||||||
|
bind:matrixCols
|
||||||
|
bind:rangeMin
|
||||||
|
bind:rangeMax
|
||||||
|
title={configPanelTitle}
|
||||||
|
hint={configPanelHint}
|
||||||
|
{matrixSizeLabel}
|
||||||
|
{matrixRowsLabel}
|
||||||
|
{matrixColsLabel}
|
||||||
|
{rangeLabel}
|
||||||
|
{rangeMinLabel}
|
||||||
|
{rangeMaxLabel}
|
||||||
|
{colorMapLabel}
|
||||||
|
bind:colorMapPreset
|
||||||
|
{colorMapOptions}
|
||||||
|
resetLabel={resetConfigLabel}
|
||||||
|
{applyLiveHint}
|
||||||
|
on:close={() => dispatch("configclose")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="panel-zone" bind:this={panelZoneEl}>
|
||||||
|
<aside class="side-rail left-rail">
|
||||||
|
<div class="rail-stack" bind:this={leftStackEl}>
|
||||||
|
{#each leftPanels as panel, index (panel.id)}
|
||||||
|
<div
|
||||||
|
class="panel-motion-shell"
|
||||||
|
animate:flip={{ duration: 280 }}
|
||||||
|
in:fly={{ x: -180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
||||||
|
out:fly={{ x: -180, duration: 280, opacity: 0.06, easing: cubicIn }}
|
||||||
|
>
|
||||||
|
<SignalChart {panel} panelIndex={index} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if summary.points.length > 0 && summarySide === "left"}
|
||||||
|
<div
|
||||||
|
class="panel-motion-shell"
|
||||||
|
in:fly={{ x: -180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
||||||
|
out:fly={{ x: -180, duration: 280, opacity: 0.06, easing: cubicIn }}
|
||||||
|
>
|
||||||
|
<SummaryCurve {summary} side="left" panelIndex={leftPanels.length} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<aside class="side-rail right-rail">
|
||||||
|
<div class="rail-stack" bind:this={rightStackEl}>
|
||||||
|
{#each rightPanels as panel, index (panel.id)}
|
||||||
|
<div
|
||||||
|
class="panel-motion-shell"
|
||||||
|
animate:flip={{ duration: 280 }}
|
||||||
|
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
||||||
|
out:fly={{ x: 180, duration: 280, opacity: 0.06, easing: cubicIn }}
|
||||||
|
>
|
||||||
|
<SignalChart {panel} panelIndex={index} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if summary.points.length > 0 && summarySide === "right"}
|
||||||
|
<div
|
||||||
|
class="panel-motion-shell"
|
||||||
|
in:fly={{ x: 180, duration: 340, opacity: 0.08, easing: cubicOut }}
|
||||||
|
out:fly={{ x: 180, duration: 280, opacity: 0.06, easing: cubicIn }}
|
||||||
|
>
|
||||||
|
<SummaryCurve {summary} side="right" panelIndex={rightPanels.length} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if replayHasData}
|
||||||
|
<aside class="replay-floating-panel" class:is-left={replaySide === "left"} class:is-right={replaySide === "right"}>
|
||||||
|
<div class="replay-panel-head">
|
||||||
|
<div class="replay-panel-title-group">
|
||||||
|
<p class="replay-panel-label">{replaySectionLabel}</p>
|
||||||
|
<p class="replay-panel-file" title={replayFileName}>{replayFileName}</p>
|
||||||
|
</div>
|
||||||
|
<div class="replay-panel-head-actions">
|
||||||
|
{#if replayFrameInfo}
|
||||||
|
<p class="replay-panel-frame">{replayFrameInfo}</p>
|
||||||
|
{/if}
|
||||||
|
<button type="button" class="replay-close-btn" aria-label="Close replay" on:click={emitReplayClose}>×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="replay-panel-controls">
|
||||||
|
<div class="replay-panel-actions">
|
||||||
|
<button type="button" class="replay-action-btn" on:click={emitReplayToggle}>{replayToggleButtonText}</button>
|
||||||
|
<button type="button" class="replay-action-btn is-stop" on:click={emitReplayStop}>{replayStopLabel}</button>
|
||||||
|
<label class="replay-speed-select">
|
||||||
|
<span>{replaySpeedLabel}</span>
|
||||||
|
<select value={replaySpeed} on:change={emitReplaySpeed}>
|
||||||
|
<option value={0.5}>0.5x</option>
|
||||||
|
<option value={1}>1x</option>
|
||||||
|
<option value={1.5}>1.5x</option>
|
||||||
|
<option value={2}>2x</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="replay-progress-slider">
|
||||||
|
<span>{replayProgressLabel}</span>
|
||||||
|
<input type="range" min="0" max="100" step="1" value={replayProgressPercent} on:input={emitReplaySeek} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="stage-bottom-overlay">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.center-stage {
|
||||||
|
block-size: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-shell {
|
||||||
|
position: relative;
|
||||||
|
block-size: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0.72rem;
|
||||||
|
border: 1px solid rgb(101 133 152 / 0.2);
|
||||||
|
background:
|
||||||
|
linear-gradient(170deg, rgb(8 12 16 / 0.86) 0%, rgb(0 0 0 / 0.96) 58%, rgb(6 10 14 / 0.9) 100%),
|
||||||
|
radial-gradient(circle at 50% 0, rgb(62 232 255 / 0.04), transparent 48%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgb(175 216 240 / 0.08),
|
||||||
|
inset 0 -36px 72px rgb(0 0 0 / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-canvas-plane {
|
||||||
|
--rail-width: clamp(17.5rem, 23vw, 21.5rem);
|
||||||
|
--rail-edge-inset: clamp(0.35rem, 1vw, 0.9rem);
|
||||||
|
--safe-gap: clamp(0.35rem, 0.9vw, 0.85rem);
|
||||||
|
--panel-zone-top: clamp(6.4rem, 11.8vh, 8rem);
|
||||||
|
--panel-zone-bottom: clamp(1.8rem, 3.6vh, 2.8rem);
|
||||||
|
--rail-gap: clamp(0.55rem, 1.4vh, 1rem);
|
||||||
|
--bottom-inset: clamp(0.35rem, 1.2vw, 0.9rem);
|
||||||
|
position: relative;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-top-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: clamp(0.55rem, 1.1vw, 0.9rem);
|
||||||
|
left: calc(var(--rail-width) + var(--safe-gap) + var(--rail-edge-inset));
|
||||||
|
right: calc(var(--rail-width) + var(--safe-gap) + var(--rail-edge-inset));
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.7rem;
|
||||||
|
z-index: 7;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-meta {
|
||||||
|
min-width: 0;
|
||||||
|
max-inline-size: min(22rem, 62%);
|
||||||
|
padding: 0.3rem 0.5rem 0.35rem;
|
||||||
|
border: 1px solid rgb(112 146 166 / 0.2);
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
background: rgb(2 8 12 / 0.45);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.56rem;
|
||||||
|
color: rgb(148 171 189 / 0.8);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0.08rem 0 0;
|
||||||
|
font-size: clamp(0.75rem, 1.1vw, 0.92rem);
|
||||||
|
color: rgb(222 241 255 / 0.96);
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-hint {
|
||||||
|
margin: 0.09rem 0 0;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
color: rgb(142 165 183 / 0.76);
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-status {
|
||||||
|
margin: 0;
|
||||||
|
align-self: center;
|
||||||
|
border: 1px solid rgb(95 128 149 / 0.35);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.3rem 0.66rem;
|
||||||
|
font-size: 0.66rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: rgb(150 174 194 / 0.9);
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: rgb(3 10 15 / 0.62);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-status.is-ok {
|
||||||
|
color: rgb(204 248 184 / 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-status.is-warn {
|
||||||
|
color: rgb(255 205 188 / 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-wrap {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel-wrap {
|
||||||
|
position: absolute;
|
||||||
|
top: clamp(5rem, 11.4vh, 6.8rem);
|
||||||
|
right: calc(var(--rail-width) + var(--safe-gap) + var(--rail-edge-inset));
|
||||||
|
z-index: 8;
|
||||||
|
pointer-events: auto;
|
||||||
|
max-inline-size: min(24rem, 40vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-zone {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--panel-zone-top-dyn, var(--panel-zone-top));
|
||||||
|
right: 0;
|
||||||
|
bottom: var(--panel-zone-bottom);
|
||||||
|
left: 0;
|
||||||
|
z-index: 6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-rail {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
inline-size: var(--rail-width);
|
||||||
|
min-height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-rail {
|
||||||
|
left: var(--rail-edge-inset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-rail {
|
||||||
|
right: var(--rail-edge-inset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rail-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--rail-gap);
|
||||||
|
transform: scale(var(--rail-scale));
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-rail .rail-stack {
|
||||||
|
align-items: flex-start;
|
||||||
|
transform: scale(var(--rail-scale-left, 1));
|
||||||
|
transform-origin: left bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-rail .rail-stack {
|
||||||
|
align-items: flex-end;
|
||||||
|
transform: scale(var(--rail-scale-right, 1));
|
||||||
|
transform-origin: right bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-rail :global(.signal-panel),
|
||||||
|
.right-rail :global(.signal-panel) {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-motion-shell {
|
||||||
|
inline-size: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-rail .panel-motion-shell {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-rail .panel-motion-shell {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-floating-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--panel-zone-top-dyn, var(--panel-zone-top)) + 0.2rem);
|
||||||
|
inline-size: min(var(--rail-width), 23.5rem);
|
||||||
|
z-index: 8;
|
||||||
|
pointer-events: auto;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.52rem;
|
||||||
|
border: 1px solid rgb(95 136 159 / 0.34);
|
||||||
|
border-radius: 0.66rem;
|
||||||
|
padding: 0.66rem 0.72rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(8 14 19 / 0.86), rgb(4 8 12 / 0.8)),
|
||||||
|
radial-gradient(circle at 50% 0, rgb(62 232 255 / 0.07), transparent 56%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgb(183 218 239 / 0.08),
|
||||||
|
0 0 18px rgb(62 232 255 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-floating-panel.is-right {
|
||||||
|
right: var(--rail-edge-inset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-floating-panel.is-left {
|
||||||
|
left: var(--rail-edge-inset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-panel-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-panel-head-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-panel-title-group {
|
||||||
|
min-inline-size: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.24rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-panel-label,
|
||||||
|
.replay-panel-file,
|
||||||
|
.replay-panel-frame {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-panel-label {
|
||||||
|
font-size: 0.58rem;
|
||||||
|
letter-spacing: 0.11em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgb(152 185 206 / 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-panel-file {
|
||||||
|
font-size: 0.73rem;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
color: rgb(221 241 255 / 0.94);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-panel-frame {
|
||||||
|
border: 1px solid rgb(133 255 68 / 0.36);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.16rem 0.52rem;
|
||||||
|
background: rgb(17 28 15 / 0.64);
|
||||||
|
color: rgb(204 255 178 / 0.94);
|
||||||
|
font-size: 0.67rem;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-close-btn {
|
||||||
|
inline-size: 1.82rem;
|
||||||
|
block-size: 1.82rem;
|
||||||
|
border: 1px solid rgb(255 98 76 / 0.44);
|
||||||
|
border-radius: 0.32rem;
|
||||||
|
background: rgb(24 10 12 / 0.88);
|
||||||
|
color: rgb(255 210 203 / 0.96);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
box-shadow 180ms ease,
|
||||||
|
color 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-close-btn:hover {
|
||||||
|
border-color: rgb(255 132 115 / 0.66);
|
||||||
|
color: rgb(255 234 228 / 0.98);
|
||||||
|
box-shadow: 0 0 12px rgb(255 91 63 / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-panel-controls {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.46rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-panel-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.48rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-action-btn {
|
||||||
|
min-block-size: 1.82rem;
|
||||||
|
border: 1px solid rgb(62 232 255 / 0.36);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.2rem 0.66rem;
|
||||||
|
background: rgb(8 19 25 / 0.9);
|
||||||
|
color: rgb(225 246 255 / 0.96);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
box-shadow 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-action-btn:hover {
|
||||||
|
border-color: rgb(116 245 255 / 0.58);
|
||||||
|
box-shadow: 0 0 10px rgb(62 232 255 / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-action-btn.is-stop {
|
||||||
|
border-color: rgb(255 91 63 / 0.44);
|
||||||
|
color: rgb(255 223 214 / 0.94);
|
||||||
|
background: rgb(27 12 10 / 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-action-btn.is-stop:hover {
|
||||||
|
border-color: rgb(255 124 101 / 0.64);
|
||||||
|
box-shadow: 0 0 10px rgb(255 91 63 / 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-speed-select,
|
||||||
|
.replay-progress-slider {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.36rem;
|
||||||
|
min-block-size: 1.92rem;
|
||||||
|
border: 1px solid rgb(95 132 158 / 0.32);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.16rem 0.2rem 0.16rem 0.48rem;
|
||||||
|
background: rgb(8 15 21 / 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-speed-select span,
|
||||||
|
.replay-progress-slider span {
|
||||||
|
color: rgb(154 176 194 / 0.84);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-speed-select select {
|
||||||
|
border: 1px solid rgb(95 132 158 / 0.34);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.22rem 0.48rem;
|
||||||
|
background: rgb(4 11 16 / 0.88);
|
||||||
|
color: rgb(216 235 248 / 0.96);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-progress-slider {
|
||||||
|
inline-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-progress-slider input {
|
||||||
|
inline-size: 100%;
|
||||||
|
accent-color: rgb(133 255 68 / 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-bottom-overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(var(--rail-width) + var(--safe-gap) + var(--rail-edge-inset));
|
||||||
|
right: calc(var(--rail-width) + var(--safe-gap) + var(--rail-edge-inset));
|
||||||
|
bottom: var(--bottom-inset);
|
||||||
|
z-index: 7;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.stage-canvas-plane {
|
||||||
|
--rail-width: clamp(14.2rem, 28vw, 16.4rem);
|
||||||
|
--rail-edge-inset: clamp(0.25rem, 0.7vw, 0.55rem);
|
||||||
|
--safe-gap: clamp(0.2rem, 0.75vw, 0.45rem);
|
||||||
|
--panel-zone-top: clamp(6rem, 11.2vh, 7.2rem);
|
||||||
|
--panel-zone-bottom: clamp(1.1rem, 2.7vh, 2rem);
|
||||||
|
--rail-gap: clamp(0.45rem, 1vh, 0.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel-wrap {
|
||||||
|
max-inline-size: min(21rem, 46vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-floating-panel {
|
||||||
|
inline-size: min(var(--rail-width), 20.8rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 900px) {
|
||||||
|
.stage-canvas-plane {
|
||||||
|
--panel-zone-top: clamp(5.6rem, 10vh, 6.8rem);
|
||||||
|
--panel-zone-bottom: clamp(0.9rem, 1.8vh, 1.4rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel-wrap {
|
||||||
|
top: clamp(4.7rem, 10vh, 6rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 760px) {
|
||||||
|
.stage-canvas-plane {
|
||||||
|
--panel-zone-top: clamp(5.1rem, 9.2vh, 6.2rem);
|
||||||
|
--panel-zone-bottom: clamp(0.45rem, 1vh, 0.85rem);
|
||||||
|
--rail-gap: clamp(0.35rem, 0.8vh, 0.62rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel-wrap {
|
||||||
|
top: clamp(4.4rem, 9vh, 5.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-floating-panel {
|
||||||
|
top: calc(var(--panel-zone-top-dyn, var(--panel-zone-top)) + 0.1rem);
|
||||||
|
padding: 0.58rem 0.64rem;
|
||||||
|
gap: 0.44rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 920px) {
|
||||||
|
.config-panel-wrap {
|
||||||
|
left: calc(var(--rail-width) + var(--safe-gap) + var(--rail-edge-inset));
|
||||||
|
right: auto;
|
||||||
|
max-inline-size: min(21rem, 54vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-floating-panel {
|
||||||
|
left: calc(var(--rail-edge-inset) + 0.1rem);
|
||||||
|
right: calc(var(--rail-edge-inset) + 0.1rem);
|
||||||
|
inline-size: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
460
src/lib/components/ConfigPanel.svelte
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import type { HudColorMapOption, PressureColorMapPreset } from "$lib/types/hud";
|
||||||
|
|
||||||
|
export let title = "";
|
||||||
|
export let hint = "";
|
||||||
|
export let matrixSizeLabel = "";
|
||||||
|
export let matrixRowsLabel = "";
|
||||||
|
export let matrixColsLabel = "";
|
||||||
|
export let rangeLabel = "";
|
||||||
|
export let rangeMinLabel = "";
|
||||||
|
export let rangeMaxLabel = "";
|
||||||
|
export let colorMapLabel = "";
|
||||||
|
export let resetLabel = "";
|
||||||
|
export let applyLiveHint = "";
|
||||||
|
export let matrixRows = 12;
|
||||||
|
export let matrixCols = 7;
|
||||||
|
export let rangeMin = 0;
|
||||||
|
export let rangeMax = 5000;
|
||||||
|
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
||||||
|
export let colorMapOptions: HudColorMapOption[] = [];
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
close: void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const presetSizes = [12, 24, 32, 48, 64];
|
||||||
|
const gridMin = 1;
|
||||||
|
const gridMax = 128;
|
||||||
|
const gridStep = 1;
|
||||||
|
const rangeFloor = -9999;
|
||||||
|
const rangeCeiling = 99999;
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGridValue(value: number): number {
|
||||||
|
const numericValue = Number.isFinite(value) ? value : 12;
|
||||||
|
return clamp(Math.round(numericValue), gridMin, gridMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRangeValue(value: number): number {
|
||||||
|
return clamp(Math.round(Number.isFinite(value) ? value : 0), rangeFloor, rangeCeiling);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRowsInput(event: Event): void {
|
||||||
|
const target = event.currentTarget as HTMLInputElement;
|
||||||
|
matrixRows = normalizeGridValue(target.valueAsNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleColsInput(event: Event): void {
|
||||||
|
const target = event.currentTarget as HTMLInputElement;
|
||||||
|
matrixCols = normalizeGridValue(target.valueAsNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRangeMinInput(event: Event): void {
|
||||||
|
const target = event.currentTarget as HTMLInputElement;
|
||||||
|
rangeMin = normalizeRangeValue(target.valueAsNumber);
|
||||||
|
|
||||||
|
if (rangeMin >= rangeMax) {
|
||||||
|
rangeMax = rangeMin + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRangeMaxInput(event: Event): void {
|
||||||
|
const target = event.currentTarget as HTMLInputElement;
|
||||||
|
rangeMax = normalizeRangeValue(target.valueAsNumber);
|
||||||
|
|
||||||
|
if (rangeMax <= rangeMin) {
|
||||||
|
rangeMin = rangeMax - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPreset(size: number): void {
|
||||||
|
matrixRows = size;
|
||||||
|
matrixCols = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyColorMapPreset(id: PressureColorMapPreset): void {
|
||||||
|
colorMapPreset = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDefaults(): void {
|
||||||
|
matrixRows = 12;
|
||||||
|
matrixCols = 7;
|
||||||
|
rangeMin = 0;
|
||||||
|
rangeMax = 5000;
|
||||||
|
colorMapPreset = "emerald";
|
||||||
|
}
|
||||||
|
|
||||||
|
$: selectedColorMap = colorMapOptions.find((option) => option.id === colorMapPreset) ?? colorMapOptions[0];
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const nextRows = normalizeGridValue(matrixRows);
|
||||||
|
if (nextRows !== matrixRows) {
|
||||||
|
matrixRows = nextRows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const nextCols = normalizeGridValue(matrixCols);
|
||||||
|
if (nextCols !== matrixCols) {
|
||||||
|
matrixCols = nextCols;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const nextRangeMin = normalizeRangeValue(rangeMin);
|
||||||
|
if (nextRangeMin !== rangeMin) {
|
||||||
|
rangeMin = nextRangeMin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const nextRangeMax = Math.max(normalizeRangeValue(rangeMax), rangeMin + 1);
|
||||||
|
if (nextRangeMax !== rangeMax) {
|
||||||
|
rangeMax = nextRangeMax;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="config-panel" aria-label={title}>
|
||||||
|
<header class="config-head">
|
||||||
|
<div class="config-copy">
|
||||||
|
<p class="config-label">Stage Config</p>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<p class="config-hint">{hint}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="close-btn" on:click={() => dispatch("close")} aria-label="Close config panel">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-head">
|
||||||
|
<p class="section-title">{matrixSizeLabel}</p>
|
||||||
|
<p class="section-note">{matrixRows} x {matrixCols}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preset-row" role="group" aria-label={matrixSizeLabel}>
|
||||||
|
{#each presetSizes as size}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="preset-btn"
|
||||||
|
class:is-active={matrixRows === size && matrixCols === size}
|
||||||
|
on:click={() => applyPreset(size)}
|
||||||
|
>
|
||||||
|
{size}x{size}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-grid">
|
||||||
|
<label class="field-card">
|
||||||
|
<span class="field-label">{matrixRowsLabel}</span>
|
||||||
|
<input type="number" min={gridMin} max={gridMax} step={gridStep} value={matrixRows} on:input={handleRowsInput} />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field-card">
|
||||||
|
<span class="field-label">{matrixColsLabel}</span>
|
||||||
|
<input type="number" min={gridMin} max={gridMax} step={gridStep} value={matrixCols} on:input={handleColsInput} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-head">
|
||||||
|
<p class="section-title">{rangeLabel}</p>
|
||||||
|
<p class="section-note">{rangeMin} - {rangeMax}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-grid">
|
||||||
|
<label class="field-card">
|
||||||
|
<span class="field-label">{rangeMinLabel}</span>
|
||||||
|
<input type="number" min={rangeFloor} max={rangeCeiling} step="100" value={rangeMin} on:input={handleRangeMinInput} />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="field-card">
|
||||||
|
<span class="field-label">{rangeMaxLabel}</span>
|
||||||
|
<input type="number" min={rangeFloor} max={rangeCeiling} step="100" value={rangeMax} on:input={handleRangeMaxInput} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-head">
|
||||||
|
<p class="section-title">{colorMapLabel}</p>
|
||||||
|
<p class="section-note">{selectedColorMap?.label ?? colorMapPreset}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="palette-row" role="group" aria-label={colorMapLabel}>
|
||||||
|
{#each colorMapOptions as option}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="palette-btn"
|
||||||
|
class:is-active={colorMapPreset === option.id}
|
||||||
|
on:click={() => applyColorMapPreset(option.id)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="palette-preview"
|
||||||
|
style={`--palette-stop-0: ${option.previewStops[0]}; --palette-stop-1: ${option.previewStops[1]}; --palette-stop-2: ${option.previewStops[2]};`}
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
<span class="palette-name">{option.label}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="config-foot">
|
||||||
|
<p class="live-note">{applyLiveHint}</p>
|
||||||
|
<button type="button" class="reset-btn" on:click={resetDefaults}>{resetLabel}</button>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.config-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.9rem;
|
||||||
|
inline-size: min(23rem, 100%);
|
||||||
|
padding: 0.92rem 0.96rem 1rem;
|
||||||
|
border: 1px solid rgb(88 132 116 / 0.3);
|
||||||
|
border-radius: 0.82rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(6 18 14 / 0.92), rgb(4 10 9 / 0.88)),
|
||||||
|
radial-gradient(circle at 100% 0, rgb(97 146 255 / 0.07), transparent 38%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgb(184 236 206 / 0.08),
|
||||||
|
0 18px 46px rgb(0 0 0 / 0.28);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.9rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-label,
|
||||||
|
.section-title,
|
||||||
|
.field-label,
|
||||||
|
.live-note {
|
||||||
|
margin: 0;
|
||||||
|
color: rgb(157 206 181 / 0.8);
|
||||||
|
font-size: 0.58rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0.12rem 0 0;
|
||||||
|
color: rgb(237 248 241 / 0.98);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-hint,
|
||||||
|
.section-note {
|
||||||
|
margin: 0.14rem 0 0;
|
||||||
|
color: rgb(142 182 164 / 0.78);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: relative;
|
||||||
|
inline-size: 2rem;
|
||||||
|
block-size: 2rem;
|
||||||
|
border: 1px solid rgb(82 122 106 / 0.32);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgb(4 12 9 / 0.72);
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn span {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
inline-size: 0.8rem;
|
||||||
|
block-size: 1px;
|
||||||
|
background: rgb(182 210 195 / 0.9);
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn span:first-child {
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn span:last-child {
|
||||||
|
transform: translate(-50%, -50%) rotate(-45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.62rem;
|
||||||
|
padding: 0.76rem 0.8rem;
|
||||||
|
border: 1px solid rgb(72 116 96 / 0.22);
|
||||||
|
border-radius: 0.72rem;
|
||||||
|
background: linear-gradient(180deg, rgb(7 15 12 / 0.76), rgb(5 10 8 / 0.64));
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.48rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-btn,
|
||||||
|
.reset-btn,
|
||||||
|
.palette-btn {
|
||||||
|
border: 1px solid rgb(80 126 105 / 0.28);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.38rem 0.72rem;
|
||||||
|
background: rgb(8 19 15 / 0.76);
|
||||||
|
color: rgb(191 219 206 / 0.92);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 160ms ease,
|
||||||
|
box-shadow 160ms ease,
|
||||||
|
color 160ms ease,
|
||||||
|
background-color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-btn.is-active {
|
||||||
|
border-color: rgb(98 201 149 / 0.48);
|
||||||
|
background: linear-gradient(180deg, rgb(18 54 37 / 0.96), rgb(10 32 23 / 0.92));
|
||||||
|
color: rgb(233 247 240 / 0.98);
|
||||||
|
box-shadow: inset 0 1px 0 rgb(198 246 222 / 0.14), 0 0 16px rgb(63 184 120 / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-btn {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.34rem;
|
||||||
|
min-height: 4rem;
|
||||||
|
padding: 0.52rem 0.56rem 0.58rem;
|
||||||
|
border-radius: 0.74rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-btn.is-active {
|
||||||
|
border-color: rgb(98 201 149 / 0.48);
|
||||||
|
background: linear-gradient(180deg, rgb(18 54 37 / 0.96), rgb(10 32 23 / 0.92));
|
||||||
|
color: rgb(233 247 240 / 0.98);
|
||||||
|
box-shadow: inset 0 1px 0 rgb(198 246 222 / 0.14), 0 0 16px rgb(63 184 120 / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-preview {
|
||||||
|
display: block;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 0.74rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgb(255 255 255 / 0.08);
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--palette-stop-0) 0%,
|
||||||
|
var(--palette-stop-1) 52%,
|
||||||
|
var(--palette-stop-2) 100%
|
||||||
|
),
|
||||||
|
linear-gradient(180deg, rgb(255 255 255 / 0.08), transparent 55%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgb(255 255 255 / 0.1),
|
||||||
|
0 0 12px rgb(0 0 0 / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-name {
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.58rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.38rem;
|
||||||
|
padding: 0.58rem 0.64rem 0.66rem;
|
||||||
|
border: 1px solid rgb(68 106 89 / 0.26);
|
||||||
|
border-radius: 0.58rem;
|
||||||
|
background: linear-gradient(180deg, rgb(6 14 11 / 0.86), rgb(3 8 6 / 0.82));
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card input {
|
||||||
|
inline-size: 100%;
|
||||||
|
border: 1px solid rgb(82 131 109 / 0.28);
|
||||||
|
border-radius: 0.46rem;
|
||||||
|
padding: 0.55rem 0.62rem;
|
||||||
|
background: rgb(7 16 12 / 0.92);
|
||||||
|
color: rgb(238 246 241 / 0.98);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-card input:focus {
|
||||||
|
border-color: rgb(97 201 147 / 0.54);
|
||||||
|
box-shadow: 0 0 0 1px rgb(97 201 147 / 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-foot {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-note {
|
||||||
|
color: rgb(142 182 164 / 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
background: linear-gradient(180deg, rgb(10 21 17 / 0.88), rgb(6 12 10 / 0.84));
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.config-panel {
|
||||||
|
inline-size: min(20rem, 100%);
|
||||||
|
padding: 0.86rem 0.88rem 0.94rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
970
src/lib/components/HudPanel.svelte
Normal file
@@ -0,0 +1,970 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import type {
|
||||||
|
ConnectionState,
|
||||||
|
HudConfigLink,
|
||||||
|
HudNoticeTone,
|
||||||
|
LocaleCode,
|
||||||
|
WindowControlAction
|
||||||
|
} from "$lib/types/hud";
|
||||||
|
|
||||||
|
export let appName = "";
|
||||||
|
export let suiteName = "";
|
||||||
|
export let controlAreaLabel = "";
|
||||||
|
export let locale: LocaleCode = "zh-CN";
|
||||||
|
export let connectionState: ConnectionState = "offline";
|
||||||
|
export let connectionLabel = "";
|
||||||
|
export let connectedLabel = "";
|
||||||
|
export let connectingLabel = "";
|
||||||
|
export let disconnectedLabel = "";
|
||||||
|
export let deviceLabel = "";
|
||||||
|
export let deviceValue = "";
|
||||||
|
export let sampleRateLabel = "";
|
||||||
|
export let sampleRateValue = "";
|
||||||
|
export let channelsLabel = "";
|
||||||
|
export let channelsValue = "";
|
||||||
|
export let serialPortLabel = "";
|
||||||
|
export let serialPortValue = "";
|
||||||
|
export let serialPortOptions: string[] = [];
|
||||||
|
export let refreshPortsLabel = "";
|
||||||
|
export let configLinksLabel = "";
|
||||||
|
export let configLinks: HudConfigLink[] = [];
|
||||||
|
export let connectActionLabel = "";
|
||||||
|
export let disconnectActionLabel = "";
|
||||||
|
export let exportActionLabel = "";
|
||||||
|
export let exportingActionLabel = "";
|
||||||
|
export let importActionLabel = "";
|
||||||
|
export let connectionNotice = "";
|
||||||
|
export let connectionNoticeTone: HudNoticeTone = "info";
|
||||||
|
export let isRefreshingPorts = false;
|
||||||
|
export let isConnectDisabled = false;
|
||||||
|
export let isExporting = false;
|
||||||
|
export let isExportDisabled = false;
|
||||||
|
export let isWindowMaximized = false;
|
||||||
|
let csvInputEl: HTMLInputElement | undefined;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
windowcontrol: WindowControlAction;
|
||||||
|
localechange: LocaleCode;
|
||||||
|
configlink: string;
|
||||||
|
portchange: string;
|
||||||
|
serialrefresh: void;
|
||||||
|
serialconnect: string;
|
||||||
|
serialexport: void;
|
||||||
|
csvimport: File;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const connectionToneByState: Record<ConnectionState, "ok" | "warn" | "idle"> = {
|
||||||
|
online: "ok",
|
||||||
|
connecting: "warn",
|
||||||
|
offline: "idle"
|
||||||
|
};
|
||||||
|
|
||||||
|
$: connectionTone = connectionToneByState[connectionState];
|
||||||
|
$: connectionText =
|
||||||
|
connectionState === "online"
|
||||||
|
? connectedLabel
|
||||||
|
: connectionState === "connecting"
|
||||||
|
? connectingLabel
|
||||||
|
: disconnectedLabel;
|
||||||
|
$: connectButtonText =
|
||||||
|
connectionState === "online"
|
||||||
|
? disconnectActionLabel
|
||||||
|
: connectionState === "connecting"
|
||||||
|
? connectingLabel
|
||||||
|
: connectActionLabel;
|
||||||
|
$: exportButtonText = isExporting ? exportingActionLabel || exportActionLabel : exportActionLabel;
|
||||||
|
$: resolvedSerialPortOptions =
|
||||||
|
serialPortOptions.length > 0 ? serialPortOptions : serialPortValue ? [serialPortValue] : [];
|
||||||
|
|
||||||
|
function emitWindowControl(action: WindowControlAction): void {
|
||||||
|
dispatch("windowcontrol", action);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchLocale(nextLocale: LocaleCode): void {
|
||||||
|
dispatch("localechange", nextLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitConfigLink(linkId: string): void {
|
||||||
|
dispatch("configlink", linkId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitPortChange(event: Event): void {
|
||||||
|
const target = event.currentTarget as HTMLSelectElement;
|
||||||
|
dispatch("portchange", target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitSerialConnect(): void {
|
||||||
|
dispatch("serialconnect", serialPortValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitSerialRefresh(): void {
|
||||||
|
dispatch("serialrefresh");
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitSerialExport(): void {
|
||||||
|
dispatch("serialexport");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCsvPicker(): void {
|
||||||
|
csvInputEl?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitCsvImport(event: Event): void {
|
||||||
|
const target = event.currentTarget as HTMLInputElement;
|
||||||
|
const file = target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
dispatch("csvimport", file);
|
||||||
|
}
|
||||||
|
target.value = "";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="hud-panel" aria-label={controlAreaLabel}>
|
||||||
|
<div class="title-bar" role="group" aria-label="Title Bar" data-tauri-drag-region>
|
||||||
|
<div class="title-cluster" data-tauri-drag-region>
|
||||||
|
<span class="title-pulse" aria-hidden="true"></span>
|
||||||
|
<strong class="app-name">{appName}</strong>
|
||||||
|
<span class="suite-tag">{suiteName}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="window-controls" aria-label="Window Controls">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="window-btn"
|
||||||
|
on:click={() => emitWindowControl("minimize")}
|
||||||
|
aria-label="Minimize window"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 12 12" aria-hidden="true">
|
||||||
|
<path d="M2 6h8"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="window-btn"
|
||||||
|
class:is-maximized={isWindowMaximized}
|
||||||
|
on:click={() => emitWindowControl("toggle-maximize")}
|
||||||
|
aria-label="Toggle maximize window"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 12 12" aria-hidden="true">
|
||||||
|
<path d="M2.6 2.6h6.8v6.8h-6.8z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="window-btn is-close"
|
||||||
|
on:click={() => emitWindowControl("close")}
|
||||||
|
aria-label="Close window"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 12 12" aria-hidden="true">
|
||||||
|
<path d="M2.6 2.6l6.8 6.8"></path>
|
||||||
|
<path d="M9.4 2.6l-6.8 6.8"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-bar">
|
||||||
|
<div class="control-main-row">
|
||||||
|
<section class="config-links" aria-label={configLinksLabel}>
|
||||||
|
{#each configLinks as link (link.id)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="config-link tone-{link.tone ?? 'neutral'}"
|
||||||
|
class:is-active={Boolean(link.active)}
|
||||||
|
on:click={() => emitConfigLink(link.id)}
|
||||||
|
>
|
||||||
|
<span class="config-indicator" aria-hidden="true"></span>
|
||||||
|
<span class="config-label">{link.label}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="state-card" aria-label={connectionLabel}>
|
||||||
|
<span class="state-dot" class:ok={connectionTone === "ok"} class:warn={connectionTone === "warn"}></span>
|
||||||
|
<span class="state-label">{connectionLabel}</span>
|
||||||
|
<strong class="state-value">{connectionText}</strong>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<label class="serial-select" aria-label={serialPortLabel}>
|
||||||
|
<span class="serial-tag">{serialPortLabel}</span>
|
||||||
|
<span class="serial-select-wrap">
|
||||||
|
<select
|
||||||
|
class="serial-select-input"
|
||||||
|
value={serialPortValue}
|
||||||
|
disabled={connectionState !== "offline" || isRefreshingPorts}
|
||||||
|
on:change={emitPortChange}
|
||||||
|
>
|
||||||
|
{#each resolvedSerialPortOptions as option}
|
||||||
|
<option value={option}>{option}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<span class="serial-select-caret" aria-hidden="true"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="refresh-btn"
|
||||||
|
disabled={isRefreshingPorts || connectionState !== "offline"}
|
||||||
|
on:click={emitSerialRefresh}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path d="M13 4.8V1.8"></path>
|
||||||
|
<path d="M13 1.8H10"></path>
|
||||||
|
<path d="M13 1.8a5.7 5.7 0 0 0-9.7 3.4"></path>
|
||||||
|
<path d="M3 11.2v3"></path>
|
||||||
|
<path d="M3 14.2h3"></path>
|
||||||
|
<path d="M3 14.2a5.7 5.7 0 0 0 9.7-3.4"></path>
|
||||||
|
</svg>
|
||||||
|
<span>{isRefreshingPorts ? `${refreshPortsLabel}...` : refreshPortsLabel}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="connect-btn"
|
||||||
|
class:is-busy={connectionState === "connecting"}
|
||||||
|
class:is-connected={connectionState === "online"}
|
||||||
|
disabled={isConnectDisabled}
|
||||||
|
on:click={emitSerialConnect}
|
||||||
|
>
|
||||||
|
<span class="connect-btn-indicator" aria-hidden="true"></span>
|
||||||
|
<span>{connectButtonText}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="export-btn"
|
||||||
|
disabled={isExportDisabled || isExporting}
|
||||||
|
on:click={emitSerialExport}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path d="M8 2.2v7.2"></path>
|
||||||
|
<path d="M5.4 7.8L8 10.5l2.6-2.7"></path>
|
||||||
|
<path d="M3.1 12.3h9.8"></path>
|
||||||
|
</svg>
|
||||||
|
<span>{exportButtonText}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="import-btn" on:click={openCsvPicker}>
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path d="M8 10.8V3.6"></path>
|
||||||
|
<path d="M5.4 6.3L8 3.6l2.6 2.7"></path>
|
||||||
|
<path d="M3.1 12.6h9.8"></path>
|
||||||
|
</svg>
|
||||||
|
<span>{importActionLabel}</span>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
bind:this={csvInputEl}
|
||||||
|
class="hidden-input"
|
||||||
|
type="file"
|
||||||
|
accept=".csv,text/csv"
|
||||||
|
on:change={emitCsvImport}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section class="locale-switch" aria-label="Language">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="locale-btn"
|
||||||
|
class:is-active={locale === "zh-CN"}
|
||||||
|
on:click={() => switchLocale("zh-CN")}
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="locale-btn"
|
||||||
|
class:is-active={locale === "en-US"}
|
||||||
|
on:click={() => switchLocale("en-US")}
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if connectionNotice}
|
||||||
|
<p class="connection-notice tone-{connectionNoticeTone}" role={connectionNoticeTone === "warn" ? "alert" : "status"}>
|
||||||
|
{connectionNotice}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<section class="info-grid">
|
||||||
|
<article class="info-cell">
|
||||||
|
<p class="meta-label">{deviceLabel}</p>
|
||||||
|
<p class="meta-value">{deviceValue}</p>
|
||||||
|
</article>
|
||||||
|
<article class="info-cell">
|
||||||
|
<p class="meta-label">{sampleRateLabel}</p>
|
||||||
|
<p class="meta-value">{sampleRateValue}</p>
|
||||||
|
</article>
|
||||||
|
<article class="info-cell">
|
||||||
|
<p class="meta-label">{channelsLabel}</p>
|
||||||
|
<p class="meta-value">{channelsValue}</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hud-panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
gap: clamp(0.5rem, 1.2vw, 0.85rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid rgb(108 143 166 / 0.22);
|
||||||
|
padding: 0.05rem 0.1rem 0.55rem 0.1rem;
|
||||||
|
background: linear-gradient(180deg, rgb(15 22 28 / 0.32), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-cluster {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-pulse {
|
||||||
|
inline-size: 0.52rem;
|
||||||
|
block-size: 0.52rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgb(133 255 68 / 0.95);
|
||||||
|
box-shadow: 0 0 0 2px rgb(133 255 68 / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: #f0fbff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suite-tag {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--hud-text-dim);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.38rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
inline-size: 1.8rem;
|
||||||
|
block-size: 1.52rem;
|
||||||
|
border: 1px solid rgb(82 120 146 / 0.36);
|
||||||
|
border-radius: 0.34rem;
|
||||||
|
color: rgb(179 245 255 / 0.92);
|
||||||
|
background: rgb(8 12 16 / 0.82);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 200ms ease,
|
||||||
|
border-color 200ms ease,
|
||||||
|
color 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-btn svg {
|
||||||
|
inline-size: 0.8rem;
|
||||||
|
block-size: 0.8rem;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.4;
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-btn:hover {
|
||||||
|
border-color: rgb(62 232 255 / 0.42);
|
||||||
|
background: rgb(14 20 26 / 0.9);
|
||||||
|
color: #f3fdff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-btn.is-maximized {
|
||||||
|
border-color: rgb(133 255 68 / 0.5);
|
||||||
|
color: rgb(211 255 190 / 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-btn.is-close:hover {
|
||||||
|
border-color: rgb(255 91 63 / 0.62);
|
||||||
|
background: rgb(27 11 10 / 0.9);
|
||||||
|
color: rgb(255 200 188 / 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-bar {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
padding: 0 0.1rem;
|
||||||
|
background: linear-gradient(90deg, rgb(62 232 255 / 0.02), transparent 45%, rgb(133 255 68 / 0.015));
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-main-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.58rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.42rem;
|
||||||
|
min-block-size: 2rem;
|
||||||
|
border: 1px solid rgb(95 132 158 / 0.3);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.2rem 0.62rem 0.2rem 0.36rem;
|
||||||
|
background: rgb(10 16 20 / 0.68);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-dot {
|
||||||
|
inline-size: 0.55rem;
|
||||||
|
block-size: 0.55rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgb(143 165 186 / 0.92);
|
||||||
|
box-shadow: 0 0 0 2px rgb(143 165 186 / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-dot.ok {
|
||||||
|
background: var(--hud-lime);
|
||||||
|
box-shadow: 0 0 0 2px rgb(133 255 68 / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-dot.warn {
|
||||||
|
background: var(--hud-orange);
|
||||||
|
box-shadow: 0 0 0 2px rgb(255 91 63 / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-label {
|
||||||
|
color: rgb(154 176 194 / 0.84);
|
||||||
|
font-size: 0.66rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-value {
|
||||||
|
color: #ecf8ff;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.serial-select {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.38rem;
|
||||||
|
min-block-size: 2rem;
|
||||||
|
border: 1px solid rgb(95 132 158 / 0.3);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.18rem 0.2rem 0.18rem 0.56rem;
|
||||||
|
background: rgb(10 16 20 / 0.7);
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.serial-select-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.serial-tag {
|
||||||
|
color: rgb(154 176 194 / 0.84);
|
||||||
|
font-size: 0.66rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.serial-select-input {
|
||||||
|
appearance: none;
|
||||||
|
inline-size: 100%;
|
||||||
|
min-inline-size: 7rem;
|
||||||
|
border: 1px solid rgb(95 132 158 / 0.32);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.3rem 1.5rem 0.3rem 0.6rem;
|
||||||
|
background: rgb(4 11 16 / 0.84);
|
||||||
|
color: #d5ebfb;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
outline: none;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
box-shadow 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.serial-select-input:hover {
|
||||||
|
border-color: rgb(62 232 255 / 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.serial-select-input:focus-visible {
|
||||||
|
border-color: rgb(62 232 255 / 0.5);
|
||||||
|
box-shadow: 0 0 0 2px rgb(62 232 255 / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.serial-select-caret {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-end: 0.68rem;
|
||||||
|
inset-block-start: 50%;
|
||||||
|
inline-size: 0.42rem;
|
||||||
|
block-size: 0.42rem;
|
||||||
|
border-right: 1px solid rgb(153 189 214 / 0.82);
|
||||||
|
border-bottom: 1px solid rgb(153 189 214 / 0.82);
|
||||||
|
transform: translateY(-64%) rotate(45deg);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.36rem;
|
||||||
|
min-block-size: 2rem;
|
||||||
|
border: 1px solid rgb(95 132 158 / 0.34);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.24rem 0.64rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(11 18 24 / 0.92), rgb(7 12 17 / 0.88)),
|
||||||
|
radial-gradient(circle at 50% 0, rgb(62 232 255 / 0.1), transparent 58%);
|
||||||
|
color: rgb(214 236 248 / 0.96);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
background-color 180ms ease,
|
||||||
|
color 180ms ease,
|
||||||
|
box-shadow 200ms ease,
|
||||||
|
opacity 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn svg {
|
||||||
|
inline-size: 0.84rem;
|
||||||
|
block-size: 0.84rem;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.35;
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover:not(:disabled) {
|
||||||
|
border-color: rgb(62 232 255 / 0.44);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgb(167 218 252 / 0.07),
|
||||||
|
0 0 10px rgb(62 232 255 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:disabled {
|
||||||
|
opacity: 0.72;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.42rem;
|
||||||
|
min-block-size: 2rem;
|
||||||
|
border: 1px solid rgb(133 255 68 / 0.4);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.24rem 0.76rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(24 33 22 / 0.96), rgb(12 19 12 / 0.92)),
|
||||||
|
radial-gradient(circle at 50% 0, rgb(133 255 68 / 0.12), transparent 58%);
|
||||||
|
color: #f2ffe8;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
background-color 180ms ease,
|
||||||
|
color 180ms ease,
|
||||||
|
box-shadow 200ms ease,
|
||||||
|
opacity 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-btn:hover:not(:disabled) {
|
||||||
|
border-color: rgb(170 255 121 / 0.62);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgb(231 255 214 / 0.08),
|
||||||
|
0 0 12px rgb(133 255 68 / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-btn:disabled {
|
||||||
|
opacity: 0.72;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-btn.is-busy {
|
||||||
|
border-color: rgb(255 91 63 / 0.48);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(38 18 15 / 0.96), rgb(23 10 10 / 0.92)),
|
||||||
|
radial-gradient(circle at 50% 0, rgb(255 91 63 / 0.12), transparent 58%);
|
||||||
|
color: rgb(255 223 217 / 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-btn.is-connected {
|
||||||
|
border-color: rgb(62 232 255 / 0.46);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(10 28 32 / 0.96), rgb(6 15 18 / 0.92)),
|
||||||
|
radial-gradient(circle at 50% 0, rgb(62 232 255 / 0.14), transparent 58%);
|
||||||
|
color: rgb(227 251 255 / 0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-btn-indicator {
|
||||||
|
inline-size: 0.4rem;
|
||||||
|
block-size: 0.4rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--hud-lime);
|
||||||
|
box-shadow: 0 0 0 2px rgb(133 255 68 / 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-btn.is-busy .connect-btn-indicator {
|
||||||
|
background: var(--hud-orange);
|
||||||
|
box-shadow: 0 0 0 2px rgb(255 91 63 / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-btn.is-connected .connect-btn-indicator {
|
||||||
|
background: var(--hud-cyan);
|
||||||
|
box-shadow: 0 0 0 2px rgb(62 232 255 / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.36rem;
|
||||||
|
min-block-size: 2rem;
|
||||||
|
border: 1px solid rgb(62 232 255 / 0.4);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.24rem 0.72rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(10 28 32 / 0.96), rgb(6 15 18 / 0.92)),
|
||||||
|
radial-gradient(circle at 50% 0, rgb(62 232 255 / 0.14), transparent 58%);
|
||||||
|
color: rgb(227 251 255 / 0.98);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
background-color 180ms ease,
|
||||||
|
color 180ms ease,
|
||||||
|
box-shadow 200ms ease,
|
||||||
|
opacity 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-btn svg {
|
||||||
|
inline-size: 0.84rem;
|
||||||
|
block-size: 0.84rem;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.35;
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-btn:hover:not(:disabled) {
|
||||||
|
border-color: rgb(115 245 255 / 0.62);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgb(231 255 255 / 0.08),
|
||||||
|
0 0 12px rgb(62 232 255 / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-btn:disabled {
|
||||||
|
opacity: 0.72;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.36rem;
|
||||||
|
min-block-size: 2rem;
|
||||||
|
border: 1px solid rgb(122 198 255 / 0.36);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.24rem 0.72rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(11 22 32 / 0.94), rgb(7 13 20 / 0.9)),
|
||||||
|
radial-gradient(circle at 50% 0, rgb(122 198 255 / 0.13), transparent 58%);
|
||||||
|
color: rgb(226 243 255 / 0.97);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 180ms ease,
|
||||||
|
background-color 180ms ease,
|
||||||
|
color 180ms ease,
|
||||||
|
box-shadow 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-btn svg {
|
||||||
|
inline-size: 0.84rem;
|
||||||
|
block-size: 0.84rem;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.35;
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-btn:hover {
|
||||||
|
border-color: rgb(164 220 255 / 0.6);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgb(231 244 255 / 0.08),
|
||||||
|
0 0 12px rgb(122 198 255 / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-input {
|
||||||
|
position: absolute;
|
||||||
|
inline-size: 0;
|
||||||
|
block-size: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-notice {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid rgb(95 132 158 / 0.32);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.45rem 0.7rem;
|
||||||
|
background: rgb(8 14 19 / 0.72);
|
||||||
|
color: rgb(214 236 248 / 0.96);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-notice.tone-warn {
|
||||||
|
border-color: rgb(255 91 63 / 0.42);
|
||||||
|
background: rgb(28 12 11 / 0.78);
|
||||||
|
color: rgb(255 218 208 / 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-notice.tone-ok {
|
||||||
|
border-color: rgb(133 255 68 / 0.4);
|
||||||
|
background: rgb(18 26 16 / 0.76);
|
||||||
|
color: rgb(228 255 214 / 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-notice.tone-info {
|
||||||
|
border-color: rgb(62 232 255 / 0.34);
|
||||||
|
background: rgb(8 17 22 / 0.76);
|
||||||
|
color: rgb(214 236 248 / 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.05rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-inline: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-cell {
|
||||||
|
min-width: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.58rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: rgb(140 163 181 / 0.82);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
color: rgb(205 228 245 / 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-switch {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.34rem;
|
||||||
|
border: 1px solid rgb(95 132 158 / 0.34);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.18rem;
|
||||||
|
background: rgb(10 16 20 / 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-links {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
min-block-size: 2rem;
|
||||||
|
border: 1px solid rgb(95 132 158 / 0.36);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.17rem 0.2rem;
|
||||||
|
background: linear-gradient(180deg, rgb(9 15 19 / 0.9), rgb(4 8 12 / 0.86));
|
||||||
|
box-shadow: inset 0 0 0 1px rgb(140 184 210 / 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.34rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.26rem 0.64rem;
|
||||||
|
background: transparent;
|
||||||
|
color: rgb(164 188 208 / 0.9);
|
||||||
|
font-size: 0.81rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color 180ms ease,
|
||||||
|
border-color 180ms ease,
|
||||||
|
background-color 180ms ease,
|
||||||
|
box-shadow 220ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-indicator {
|
||||||
|
inline-size: 0.34rem;
|
||||||
|
block-size: 0.34rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgb(136 157 174 / 0.88);
|
||||||
|
box-shadow: 0 0 0 2px rgb(136 157 174 / 0.16);
|
||||||
|
transition:
|
||||||
|
background-color 180ms ease,
|
||||||
|
box-shadow 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-label {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-link:hover {
|
||||||
|
color: #d7edfb;
|
||||||
|
border-color: rgb(62 232 255 / 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-link.is-active {
|
||||||
|
color: #f1fdff;
|
||||||
|
border-color: rgb(106 150 180 / 0.56);
|
||||||
|
background: rgb(18 27 35 / 0.9);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgb(167 218 252 / 0.08),
|
||||||
|
0 0 10px rgb(62 232 255 / 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-link.tone-cyan.is-active {
|
||||||
|
border-color: rgb(62 232 255 / 0.48);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-link.tone-lime.is-active {
|
||||||
|
border-color: rgb(133 255 68 / 0.52);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-link.tone-orange.is-active {
|
||||||
|
border-color: rgb(255 91 63 / 0.52);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-link.tone-cyan.is-active .config-indicator {
|
||||||
|
background: var(--hud-cyan);
|
||||||
|
box-shadow: 0 0 0 2px rgb(62 232 255 / 0.17);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-link.tone-lime.is-active .config-indicator {
|
||||||
|
background: var(--hud-lime);
|
||||||
|
box-shadow: 0 0 0 2px rgb(133 255 68 / 0.17);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-link.tone-orange.is-active .config-indicator {
|
||||||
|
background: var(--hud-orange);
|
||||||
|
box-shadow: 0 0 0 2px rgb(255 91 63 / 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-btn {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.26rem 0.6rem;
|
||||||
|
background: transparent;
|
||||||
|
color: rgb(150 173 189 / 0.9);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color 180ms ease,
|
||||||
|
border-color 180ms ease,
|
||||||
|
background-color 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-btn:hover {
|
||||||
|
color: #d7edfb;
|
||||||
|
border-color: rgb(62 232 255 / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-btn.is-active {
|
||||||
|
color: #f1fdff;
|
||||||
|
border-color: rgb(133 255 68 / 0.48);
|
||||||
|
background: rgb(24 31 25 / 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1080px) {
|
||||||
|
.config-links {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.control-main-row {
|
||||||
|
gap: 0.44rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-links {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.serial-select {
|
||||||
|
padding-inline-start: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
gap: 0.84rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
.suite-tag {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-main-row {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.serial-select {
|
||||||
|
inline-size: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
gap: 0.42rem 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-cell {
|
||||||
|
inline-size: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-btn {
|
||||||
|
inline-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
654
src/lib/components/PressureMatrixViewer.svelte
Normal file
@@ -0,0 +1,654 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||||
|
import { pressureColorPalettes } from "$lib/config/color-map";
|
||||||
|
import type { PressureColorMapPreset } from "$lib/types/hud";
|
||||||
|
|
||||||
|
interface ViewerStats {
|
||||||
|
total: number;
|
||||||
|
max: number;
|
||||||
|
avg: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MatrixLayout {
|
||||||
|
cellSpacing: number;
|
||||||
|
boardWidth: number;
|
||||||
|
boardDepth: number;
|
||||||
|
boardPadding: number;
|
||||||
|
gridSpan: number;
|
||||||
|
gridDivisions: number;
|
||||||
|
labelScale: number;
|
||||||
|
labelFloatOffset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let pressureMatrix: number[] | null = null;
|
||||||
|
export let matrixRows = 12;
|
||||||
|
export let matrixCols = 7;
|
||||||
|
export let rangeMin = 0;
|
||||||
|
export let rangeMax = 5000;
|
||||||
|
export let colorMapPreset: PressureColorMapPreset = "emerald";
|
||||||
|
|
||||||
|
let viewerEl: HTMLDivElement | undefined;
|
||||||
|
let canvasEl: HTMLCanvasElement | undefined;
|
||||||
|
let overlayEl: HTMLCanvasElement | undefined;
|
||||||
|
let stats: ViewerStats = { total: 0, max: 0, avg: 0 };
|
||||||
|
|
||||||
|
const RAW_DATA_MAX = 5000;
|
||||||
|
const BASE_MATRIX_SPAN = 24;
|
||||||
|
const MATRIX_SPAN_GROWTH = 0.6;
|
||||||
|
const MIN_MATRIX_SPAN = 24;
|
||||||
|
const MAX_MATRIX_SPAN = 58;
|
||||||
|
const MIN_CELL_SPACING = 0.52;
|
||||||
|
const MAX_CELL_SPACING = 3.8;
|
||||||
|
const MIN_BOARD_PADDING = 2.6;
|
||||||
|
const MAX_BOARD_PADDING = 6.8;
|
||||||
|
const MIN_GRID_DIVISIONS = 12;
|
||||||
|
const MAX_GRID_DIVISIONS = 48;
|
||||||
|
const MIN_LABEL_SCALE = 0.72;
|
||||||
|
const MAX_LABEL_SCALE = 2.45;
|
||||||
|
const MATRIX_OFFSET_Y = -2.4;
|
||||||
|
const MATRIX_OFFSET_Z = 12;
|
||||||
|
const HEIGHT_SCALE = 18.5;
|
||||||
|
const BASE_HEIGHT = 0.18;
|
||||||
|
const GLOW_START = 0.3;
|
||||||
|
const SMOOTHING_SPEED = 8.2;
|
||||||
|
const CAMERA_FOV = 36;
|
||||||
|
const CAMERA_DISTANCE_MIN = 30;
|
||||||
|
const CAMERA_DISTANCE_MAX = 122;
|
||||||
|
const CAMERA_FIT_PADDING = 1.04;
|
||||||
|
const CAMERA_ELEVATION_DEG = 64;
|
||||||
|
const CAMERA_TARGET_X = 0;
|
||||||
|
const CAMERA_TARGET_Y = MATRIX_OFFSET_Y + 0.2;
|
||||||
|
const CAMERA_TARGET_Z = MATRIX_OFFSET_Z - 0.4;
|
||||||
|
const MATRIX_ROTATION_Y = 0;
|
||||||
|
|
||||||
|
const labelVector = new THREE.Vector3();
|
||||||
|
const whiteColor = new THREE.Color("#ffffff");
|
||||||
|
$: resolvedColorPalette = pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
|
||||||
|
$: surfaceBaseColor = new THREE.Color(resolvedColorPalette.surfaceBase);
|
||||||
|
$: surfaceLowColor = new THREE.Color(resolvedColorPalette.surfaceLow);
|
||||||
|
$: surfaceMidColor = new THREE.Color(resolvedColorPalette.surfaceMid);
|
||||||
|
$: surfaceHighColor = new THREE.Color(resolvedColorPalette.surfaceHigh);
|
||||||
|
$: surfaceHotColor = new THREE.Color(resolvedColorPalette.surfaceHot);
|
||||||
|
$: labelZeroColor = new THREE.Color(resolvedColorPalette.labelZero);
|
||||||
|
$: labelLowColor = new THREE.Color(resolvedColorPalette.labelLow);
|
||||||
|
$: labelMidColor = new THREE.Color(resolvedColorPalette.labelMid);
|
||||||
|
$: labelHighColor = new THREE.Color(resolvedColorPalette.labelHigh);
|
||||||
|
|
||||||
|
function sanitizeGridValue(value: number): number {
|
||||||
|
return clamp(Math.round(Number.isFinite(value) ? value : 12), 1, 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeRangePair(minValue: number, maxValue: number): { min: number; max: number } {
|
||||||
|
const resolvedMin = Math.round(Number.isFinite(minValue) ? minValue : 0);
|
||||||
|
const resolvedMax = Math.max(Math.round(Number.isFinite(maxValue) ? maxValue : RAW_DATA_MAX), resolvedMin + 1);
|
||||||
|
return { min: resolvedMin, max: resolvedMax };
|
||||||
|
}
|
||||||
|
|
||||||
|
$: resolvedMatrixRows = sanitizeGridValue(matrixRows);
|
||||||
|
$: resolvedMatrixCols = sanitizeGridValue(matrixCols);
|
||||||
|
$: resolvedRange = sanitizeRangePair(rangeMin, rangeMax);
|
||||||
|
$: resolvedRangeMin = resolvedRange.min;
|
||||||
|
$: resolvedRangeMax = resolvedRange.max;
|
||||||
|
$: matrixLayout = buildMatrixLayout(resolvedMatrixRows, resolvedMatrixCols);
|
||||||
|
$: statsNote = `${resolvedMatrixRows}x${resolvedMatrixCols} / color range ${resolvedRangeMin}-${resolvedRangeMax} / label raw`;
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function smoothstep(edge0: number, edge1: number, x: number): number {
|
||||||
|
const t = clamp((x - edge0) / (edge1 - edge0), 0, 1);
|
||||||
|
return t * t * (3 - 2 * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRawValue(value: number, minValue: number, maxValue: number): number {
|
||||||
|
return clamp((value - minValue) / Math.max(1, maxValue - minValue), 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function surfaceColorMap(valueNormalized: number, target: THREE.Color = new THREE.Color()): THREE.Color {
|
||||||
|
const value = clamp(valueNormalized, 0, 1);
|
||||||
|
let mapped: THREE.Color;
|
||||||
|
|
||||||
|
if (value <= 0.45) {
|
||||||
|
const t = smoothstep(0, 0.45, value);
|
||||||
|
mapped = target.copy(surfaceBaseColor).lerp(surfaceLowColor, t);
|
||||||
|
} else if (value <= 0.78) {
|
||||||
|
const t = smoothstep(0.45, 0.78, value);
|
||||||
|
mapped = target.copy(surfaceLowColor).lerp(surfaceMidColor, t);
|
||||||
|
} else {
|
||||||
|
const t = smoothstep(0.78, 1, value);
|
||||||
|
mapped = target.copy(surfaceMidColor).lerp(surfaceHighColor, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightStrength = smoothstep(0.82, 1, value) * 0.3;
|
||||||
|
return mapped.lerp(surfaceHotColor, highlightStrength);
|
||||||
|
}
|
||||||
|
|
||||||
|
function glowColorMap(valueNormalized: number, target: THREE.Color = new THREE.Color()): THREE.Color {
|
||||||
|
const value = clamp(valueNormalized, 0, 1);
|
||||||
|
const glowStrength = smoothstep(0.55, 1, value);
|
||||||
|
return surfaceColorMap(value, target).lerp(whiteColor, glowStrength * 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelColorMap(valueNormalized: number, target: THREE.Color = new THREE.Color()): THREE.Color {
|
||||||
|
const value = clamp(valueNormalized, 0, 1);
|
||||||
|
let mapped: THREE.Color;
|
||||||
|
|
||||||
|
if (value <= 0.34) {
|
||||||
|
const t = smoothstep(0, 0.34, value);
|
||||||
|
mapped = target.copy(labelZeroColor).lerp(labelLowColor, t);
|
||||||
|
} else if (value <= 0.76) {
|
||||||
|
const t = smoothstep(0.34, 0.76, value);
|
||||||
|
mapped = target.copy(labelLowColor).lerp(labelMidColor, t);
|
||||||
|
} else {
|
||||||
|
const t = smoothstep(0.76, 1, value);
|
||||||
|
mapped = target.copy(labelMidColor).lerp(labelHighColor, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightStrength = smoothstep(0.8, 1, value) * 0.36;
|
||||||
|
return mapped.lerp(whiteColor, highlightStrength);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shapeHeightValue(valueNormalized: number): number {
|
||||||
|
return Math.pow(clamp(valueNormalized, 0, 1), 0.74);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shapeGlowStrength(valueNormalized: number): number {
|
||||||
|
return smoothstep(GLOW_START, 1, Math.pow(clamp(valueNormalized, 0, 1), 0.82));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMatrixLayout(rows: number, cols: number): MatrixLayout {
|
||||||
|
const longestEdge = Math.max(rows, cols, 1);
|
||||||
|
const edgeSpan = Math.max(longestEdge - 1, 1);
|
||||||
|
const targetSpan = clamp(BASE_MATRIX_SPAN + edgeSpan * MATRIX_SPAN_GROWTH, MIN_MATRIX_SPAN, MAX_MATRIX_SPAN);
|
||||||
|
const cellSpacing = clamp(targetSpan / edgeSpan, MIN_CELL_SPACING, MAX_CELL_SPACING);
|
||||||
|
const boardWidth = Math.max(cols, 1) * cellSpacing;
|
||||||
|
const boardDepth = Math.max(rows, 1) * cellSpacing;
|
||||||
|
const boardPadding = clamp(cellSpacing * 1.62, MIN_BOARD_PADDING, MAX_BOARD_PADDING);
|
||||||
|
const gridSpan = Math.max(boardWidth, boardDepth) + boardPadding * 2;
|
||||||
|
const gridDivisions = Math.round(clamp(longestEdge * 1.6, MIN_GRID_DIVISIONS, MAX_GRID_DIVISIONS));
|
||||||
|
const labelScale = clamp(24 / (longestEdge + 2), MIN_LABEL_SCALE, MAX_LABEL_SCALE);
|
||||||
|
const labelFloatOffset = clamp(cellSpacing * 0.74, 0.64, 2.2);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cellSpacing,
|
||||||
|
boardWidth,
|
||||||
|
boardDepth,
|
||||||
|
boardPadding,
|
||||||
|
gridSpan,
|
||||||
|
gridDivisions,
|
||||||
|
labelScale,
|
||||||
|
labelFloatOffset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fitCameraDistance(boardWidth: number, boardDepth: number, boardPadding: number, viewportAspect: number): number {
|
||||||
|
const paddedWidth = boardWidth + boardPadding * 2;
|
||||||
|
const paddedDepth = boardDepth + boardPadding * 2;
|
||||||
|
const safeAspect = Math.max(viewportAspect, 0.5);
|
||||||
|
const effectiveHalfSpan = Math.max(paddedDepth * 0.5, (paddedWidth * 0.5) / safeAspect);
|
||||||
|
const fovRadians = THREE.MathUtils.degToRad(CAMERA_FOV * 0.5);
|
||||||
|
const fitDistance = (effectiveHalfSpan / Math.tan(fovRadians)) * CAMERA_FIT_PADDING;
|
||||||
|
return clamp(fitDistance, CAMERA_DISTANCE_MIN, CAMERA_DISTANCE_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeField(source: Float32Array, target: Float32Array, minValue: number, maxValue: number): number {
|
||||||
|
let max = 0;
|
||||||
|
|
||||||
|
for (let index = 0; index < source.length; index += 1) {
|
||||||
|
const value = source[index];
|
||||||
|
target[index] = normalizeRawValue(value, minValue, maxValue);
|
||||||
|
max = Math.max(max, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyExternalField(target: Float32Array, values: number[]): void {
|
||||||
|
for (let index = 0; index < target.length; index += 1) {
|
||||||
|
target[index] = clamp(values[index] ?? 0, 0, RAW_DATA_MAX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactDisplayValue(rawValue: number, minValue: number, maxValue: number): number {
|
||||||
|
if (rawValue <= minValue + 4) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(clamp(rawValue, 0, Math.max(maxValue, RAW_DATA_MAX)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorToCss(color: THREE.Color): string {
|
||||||
|
return `rgb(${Math.round(color.r * 255)} ${Math.round(color.g * 255)} ${Math.round(color.b * 255)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: labelPalette = Array.from({ length: 33 }, (_, index) => {
|
||||||
|
const t = index / 32;
|
||||||
|
return colorToCss(labelColorMap(t, new THREE.Color()));
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!viewerEl || !canvasEl || !overlayEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridRows = resolvedMatrixRows;
|
||||||
|
const gridCols = resolvedMatrixCols;
|
||||||
|
const { cellSpacing, boardWidth, boardDepth, boardPadding, gridSpan, gridDivisions, labelScale, labelFloatOffset } =
|
||||||
|
matrixLayout;
|
||||||
|
const instanceCount = gridRows * gridCols;
|
||||||
|
|
||||||
|
const overlayContext = overlayEl.getContext("2d");
|
||||||
|
if (!overlayContext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderer = new THREE.WebGLRenderer({
|
||||||
|
canvas: canvasEl,
|
||||||
|
antialias: true,
|
||||||
|
alpha: true,
|
||||||
|
powerPreference: "high-performance"
|
||||||
|
});
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||||
|
renderer.setClearColor(0x06080a, 1);
|
||||||
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
const camera = new THREE.PerspectiveCamera(CAMERA_FOV, 1, 0.1, 500);
|
||||||
|
const cameraElevation = THREE.MathUtils.degToRad(CAMERA_ELEVATION_DEG);
|
||||||
|
const updateCameraPlacement = (viewportWidth: number, viewportHeight: number) => {
|
||||||
|
const aspect = viewportWidth / Math.max(viewportHeight, 1);
|
||||||
|
const cameraDistance = fitCameraDistance(boardWidth, boardDepth, boardPadding, aspect);
|
||||||
|
const heightOffset = Math.sin(cameraElevation) * cameraDistance;
|
||||||
|
const depthOffset = Math.cos(cameraElevation) * cameraDistance;
|
||||||
|
camera.position.set(CAMERA_TARGET_X, CAMERA_TARGET_Y + heightOffset, CAMERA_TARGET_Z + depthOffset);
|
||||||
|
camera.lookAt(CAMERA_TARGET_X, CAMERA_TARGET_Y, CAMERA_TARGET_Z);
|
||||||
|
};
|
||||||
|
updateCameraPlacement(1, 1);
|
||||||
|
camera.lookAt(CAMERA_TARGET_X, CAMERA_TARGET_Y, CAMERA_TARGET_Z);
|
||||||
|
|
||||||
|
const controls = new OrbitControls(camera, canvasEl);
|
||||||
|
controls.enableRotate = false;
|
||||||
|
controls.enableZoom = false;
|
||||||
|
controls.enablePan = false;
|
||||||
|
controls.enableDamping = false;
|
||||||
|
controls.target.set(CAMERA_TARGET_X, CAMERA_TARGET_Y, CAMERA_TARGET_Z);
|
||||||
|
controls.enabled = false;
|
||||||
|
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.26);
|
||||||
|
const dirLight = new THREE.DirectionalLight(0xffffff, 0.34);
|
||||||
|
dirLight.position.set(50, 100, 50);
|
||||||
|
const sideLight = new THREE.DirectionalLight(0x7cffba, 0.16);
|
||||||
|
sideLight.position.set(-50, 50, -50);
|
||||||
|
scene.add(ambientLight, dirLight, sideLight);
|
||||||
|
|
||||||
|
const matrixGroup = new THREE.Group();
|
||||||
|
matrixGroup.position.set(0, MATRIX_OFFSET_Y, MATRIX_OFFSET_Z);
|
||||||
|
matrixGroup.rotation.y = MATRIX_ROTATION_Y;
|
||||||
|
scene.add(matrixGroup);
|
||||||
|
|
||||||
|
const board = new THREE.Mesh(
|
||||||
|
new THREE.PlaneGeometry(boardWidth + boardPadding * 2, boardDepth + boardPadding * 2),
|
||||||
|
new THREE.MeshBasicMaterial({
|
||||||
|
color: 0x05070a,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.12,
|
||||||
|
toneMapped: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
board.rotation.x = -Math.PI / 2;
|
||||||
|
board.position.y = -0.04;
|
||||||
|
matrixGroup.add(board);
|
||||||
|
|
||||||
|
const grid = new THREE.GridHelper(gridSpan, gridDivisions, 0x1a2630, 0x0a1015);
|
||||||
|
grid.position.y = 0;
|
||||||
|
const gridMaterial = grid.material;
|
||||||
|
if (Array.isArray(gridMaterial)) {
|
||||||
|
for (const material of gridMaterial) {
|
||||||
|
material.transparent = true;
|
||||||
|
material.opacity = 0.028;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gridMaterial.transparent = true;
|
||||||
|
gridMaterial.opacity = 0.028;
|
||||||
|
}
|
||||||
|
matrixGroup.add(grid);
|
||||||
|
|
||||||
|
const cellX = new Float32Array(instanceCount);
|
||||||
|
const cellZ = new Float32Array(instanceCount);
|
||||||
|
for (let row = 0; row < gridRows; row += 1) {
|
||||||
|
for (let col = 0; col < gridCols; col += 1) {
|
||||||
|
const index = row * gridCols + col;
|
||||||
|
cellX[index] = (col - gridCols / 2 + 0.5) * cellSpacing;
|
||||||
|
cellZ[index] = (row - gridRows / 2 + 0.5) * cellSpacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetField = new Float32Array(instanceCount);
|
||||||
|
const smoothedField = new Float32Array(instanceCount);
|
||||||
|
const normalizedField = new Float32Array(instanceCount);
|
||||||
|
const heightField = new Float32Array(instanceCount);
|
||||||
|
const compactField = new Uint16Array(instanceCount);
|
||||||
|
let lastFrameAt = performance.now();
|
||||||
|
|
||||||
|
const drawNumberOverlay = () => {
|
||||||
|
if (!viewerEl || !overlayEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = viewerEl.clientWidth;
|
||||||
|
const height = viewerEl.clientHeight;
|
||||||
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
|
const fontSize = clamp((Math.min(width, height) / 66) * labelScale + cellSpacing * 1.1, 6.4, 26);
|
||||||
|
|
||||||
|
overlayContext.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
overlayContext.clearRect(0, 0, width, height);
|
||||||
|
overlayContext.textAlign = "center";
|
||||||
|
overlayContext.textBaseline = "middle";
|
||||||
|
|
||||||
|
for (let index = 0; index < instanceCount; index += 1) {
|
||||||
|
labelVector.set(cellX[index], heightField[index] + labelFloatOffset, cellZ[index]);
|
||||||
|
labelVector.applyMatrix4(matrixGroup.matrixWorld);
|
||||||
|
labelVector.project(camera);
|
||||||
|
|
||||||
|
if (labelVector.z < -1 || labelVector.z > 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const screenX = (labelVector.x * 0.5 + 0.5) * width;
|
||||||
|
const screenY = (-labelVector.y * 0.5 + 0.5) * height;
|
||||||
|
|
||||||
|
if (screenX < -12 || screenX > width + 12 || screenY < -12 || screenY > height + 12) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = normalizedField[index];
|
||||||
|
const displayValue = compactField[index];
|
||||||
|
const displayText = String(displayValue);
|
||||||
|
const digitCount = displayText.length;
|
||||||
|
const digitScale = digitCount <= 2 ? 1 : digitCount === 3 ? 0.86 : digitCount === 4 ? 0.74 : 0.64;
|
||||||
|
const bucket = Math.min(32, Math.round(normalized * 32));
|
||||||
|
const baseGlyphSize = fontSize + smoothstep(0, 1, normalized) * (2.3 * labelScale + cellSpacing * 0.26);
|
||||||
|
const glyphSize = clamp(baseGlyphSize * digitScale, 5.2, 26);
|
||||||
|
const glowSizeFactor = digitCount >= 4 ? 0.76 : digitCount === 3 ? 0.88 : 1;
|
||||||
|
const glowBlur = (4.8 + smoothstep(0.08, 1, normalized) * (10.4 * Math.max(0.72, labelScale))) * glowSizeFactor;
|
||||||
|
|
||||||
|
overlayContext.font = `600 ${glyphSize.toFixed(1)}px "JetBrains Mono", "IBM Plex Mono", "Consolas", monospace`;
|
||||||
|
overlayContext.shadowBlur = glowBlur;
|
||||||
|
overlayContext.shadowColor = labelPalette[bucket];
|
||||||
|
|
||||||
|
overlayContext.fillStyle = labelPalette[bucket];
|
||||||
|
overlayContext.globalAlpha = displayValue === 0 ? 0.86 : 1;
|
||||||
|
overlayContext.fillText(displayText, screenX, screenY);
|
||||||
|
|
||||||
|
if (normalized >= 0.8) {
|
||||||
|
overlayContext.fillStyle = "rgb(255 245 220)";
|
||||||
|
overlayContext.globalAlpha = smoothstep(0.8, 1, normalized) * 0.34;
|
||||||
|
overlayContext.fillText(displayText, screenX, screenY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayContext.globalAlpha = 1;
|
||||||
|
overlayContext.shadowBlur = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
if (!viewerEl || !overlayEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = viewerEl.clientWidth;
|
||||||
|
const height = viewerEl.clientHeight;
|
||||||
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
|
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.setSize(width, height, false);
|
||||||
|
updateCameraPlacement(width, height);
|
||||||
|
camera.aspect = width / height;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
overlayEl.width = Math.round(width * dpr);
|
||||||
|
overlayEl.height = Math.round(height * dpr);
|
||||||
|
overlayEl.style.width = `${width}px`;
|
||||||
|
overlayEl.style.height = `${height}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
resize();
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
resize();
|
||||||
|
});
|
||||||
|
resizeObserver.observe(viewerEl);
|
||||||
|
|
||||||
|
renderer.setAnimationLoop((timestamp: number) => {
|
||||||
|
const deltaSeconds = Math.min((timestamp - lastFrameAt) / 1000, 0.06);
|
||||||
|
lastFrameAt = timestamp;
|
||||||
|
|
||||||
|
let shouldHardResetToZero = true;
|
||||||
|
if (pressureMatrix && pressureMatrix.length > 0) {
|
||||||
|
copyExternalField(targetField, pressureMatrix);
|
||||||
|
for (let index = 0; index < instanceCount; index += 1) {
|
||||||
|
if (targetField[index] > 0) {
|
||||||
|
shouldHardResetToZero = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targetField.fill(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldHardResetToZero) {
|
||||||
|
smoothedField.fill(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const smoothing = 1 - Math.exp(-deltaSeconds * SMOOTHING_SPEED);
|
||||||
|
for (let index = 0; index < instanceCount; index += 1) {
|
||||||
|
smoothedField[index] += (targetField[index] - smoothedField[index]) * smoothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxValue = normalizeField(smoothedField, normalizedField, resolvedRangeMin, resolvedRangeMax);
|
||||||
|
let total = 0;
|
||||||
|
let activeCount = 0;
|
||||||
|
|
||||||
|
for (let index = 0; index < instanceCount; index += 1) {
|
||||||
|
const normalized = normalizedField[index];
|
||||||
|
const heightValue = shapeHeightValue(normalized);
|
||||||
|
const height = BASE_HEIGHT + heightValue * HEIGHT_SCALE;
|
||||||
|
|
||||||
|
heightField[index] = height;
|
||||||
|
compactField[index] = compactDisplayValue(smoothedField[index], resolvedRangeMin, resolvedRangeMax);
|
||||||
|
|
||||||
|
total += smoothedField[index];
|
||||||
|
if (smoothedField[index] > 30) {
|
||||||
|
activeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
drawNumberOverlay();
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
total,
|
||||||
|
max: maxValue,
|
||||||
|
avg: activeCount > 0 ? total / activeCount : 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
renderer.setAnimationLoop(null);
|
||||||
|
controls.dispose();
|
||||||
|
board.geometry.dispose();
|
||||||
|
board.material.dispose();
|
||||||
|
if (Array.isArray(gridMaterial)) {
|
||||||
|
for (const item of gridMaterial) {
|
||||||
|
item.dispose();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gridMaterial.dispose();
|
||||||
|
}
|
||||||
|
renderer.dispose();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="viewer-root" bind:this={viewerEl}>
|
||||||
|
<canvas class="viewer-canvas" bind:this={canvasEl} aria-label="Pressure Matrix Viewer"></canvas>
|
||||||
|
<canvas class="viewer-overlay" bind:this={overlayEl} aria-hidden="true"></canvas>
|
||||||
|
|
||||||
|
<div class="viewer-vignette" aria-hidden="true"></div>
|
||||||
|
<div class="viewer-noise" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<div class="viewer-controls">
|
||||||
|
<section class="stats-panel" aria-label="Pressure Summary">
|
||||||
|
<p class="stats-label">Pressure Matrix</p>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<article class="stats-card stats-card-wide">
|
||||||
|
<span class="stats-key">Total Pressure</span>
|
||||||
|
<strong class="stats-value">{stats.total.toFixed(0)}</strong>
|
||||||
|
</article>
|
||||||
|
<article class="stats-card">
|
||||||
|
<span class="stats-key">Max</span>
|
||||||
|
<strong class="stats-value">{stats.max.toFixed(0)}</strong>
|
||||||
|
</article>
|
||||||
|
<article class="stats-card">
|
||||||
|
<span class="stats-key">Avg</span>
|
||||||
|
<strong class="stats-value">{stats.avg.toFixed(0)}</strong>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<p class="stats-note">{statsNote}</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.viewer-root {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 50% 58%, rgb(72 127 255 / 0.065), transparent 32%),
|
||||||
|
radial-gradient(circle at 50% 12%, rgb(151 231 255 / 0.05), transparent 26%),
|
||||||
|
linear-gradient(180deg, rgb(10 13 17 / 0.82), rgb(5 7 10 / 0.96));
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-canvas,
|
||||||
|
.viewer-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-overlay {
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-vignette,
|
||||||
|
.viewer-noise {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-vignette {
|
||||||
|
background: radial-gradient(circle at center, transparent 54%, rgb(0 0 0 / 0.18) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-noise {
|
||||||
|
background: repeating-linear-gradient(180deg, rgb(124 165 216 / 0.02) 0, rgb(124 165 216 / 0.02) 1px, transparent 1px, transparent 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: clamp(4.8rem, 10vh, 6.2rem);
|
||||||
|
left: clamp(2.6rem, 4vw, 3.4rem);
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
z-index: 2;
|
||||||
|
max-inline-size: min(18rem, 32vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.58rem;
|
||||||
|
padding: 0.74rem 0.84rem 0.82rem;
|
||||||
|
border: 1px solid rgb(86 151 118 / 0.32);
|
||||||
|
border-radius: 0.76rem;
|
||||||
|
background: linear-gradient(180deg, rgb(7 17 14 / 0.9), rgb(5 10 9 / 0.84));
|
||||||
|
box-shadow: inset 0 1px 0 rgb(171 224 197 / 0.08), 0 0 24px rgb(42 138 88 / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-label,
|
||||||
|
.stats-key,
|
||||||
|
.stats-note {
|
||||||
|
margin: 0;
|
||||||
|
color: rgb(165 212 187 / 0.84);
|
||||||
|
font-size: 0.58rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.46rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.24rem;
|
||||||
|
min-height: 4.2rem;
|
||||||
|
padding: 0.58rem 0.64rem;
|
||||||
|
border: 1px solid rgb(71 122 96 / 0.24);
|
||||||
|
border-radius: 0.56rem;
|
||||||
|
background: linear-gradient(180deg, rgb(8 16 14 / 0.88), rgb(5 9 8 / 0.84));
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card-wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-value {
|
||||||
|
color: rgb(240 246 255 / 0.98);
|
||||||
|
font-size: clamp(1.16rem, 1vw + 0.82rem, 1.56rem);
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.viewer-controls {
|
||||||
|
left: clamp(1rem, 2.4vw, 1.4rem);
|
||||||
|
max-inline-size: min(13.5rem, 42vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card-wide {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 760px) {
|
||||||
|
.viewer-controls {
|
||||||
|
top: clamp(4rem, 8vh, 4.8rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-panel {
|
||||||
|
padding: 0.62rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
min-height: 3.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
452
src/lib/components/SignalChart.svelte
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HudSignalPanel, SignalTone } from "$lib/types/hud";
|
||||||
|
|
||||||
|
export let panel: HudSignalPanel;
|
||||||
|
export let panelIndex = 0;
|
||||||
|
|
||||||
|
const viewportWidth = 100;
|
||||||
|
const viewportHeight = 36;
|
||||||
|
|
||||||
|
const toneColorMap: Record<SignalTone, string> = {
|
||||||
|
cyan: "62 232 255",
|
||||||
|
lime: "133 255 68",
|
||||||
|
orange: "255 91 63",
|
||||||
|
violet: "171 118 255",
|
||||||
|
gold: "255 206 84",
|
||||||
|
rose: "255 108 176"
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SeriesRenderShape {
|
||||||
|
id: string;
|
||||||
|
tone: SignalTone;
|
||||||
|
linePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMetric(value: number | null): string {
|
||||||
|
return value === null ? "--" : value.toFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBounds(seriesCollection: HudSignalPanel["series"]): { min: number; max: number } {
|
||||||
|
const allPoints = seriesCollection.flatMap((series) => series.points);
|
||||||
|
|
||||||
|
if (allPoints.length === 0) {
|
||||||
|
return { min: 0, max: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
let min = Math.min(...allPoints);
|
||||||
|
let max = Math.max(...allPoints);
|
||||||
|
|
||||||
|
if (Math.abs(max - min) < 0.001) {
|
||||||
|
const padding = Math.max(Math.abs(max) * 0.05, 1);
|
||||||
|
min -= padding;
|
||||||
|
max += padding;
|
||||||
|
} else {
|
||||||
|
const padding = Math.max((max - min) * 0.08, 0.5);
|
||||||
|
min -= padding;
|
||||||
|
max += padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { min, max };
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertPoints(rawPoints: number[], bounds: { min: number; max: number }): Array<{ x: number; y: number }> {
|
||||||
|
if (!rawPoints.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawPoints.length === 1) {
|
||||||
|
return [{ x: viewportWidth / 2, y: viewportHeight / 2 }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepX = viewportWidth / (rawPoints.length - 1);
|
||||||
|
const span = Math.max(bounds.max - bounds.min, 1);
|
||||||
|
|
||||||
|
return rawPoints.map((point, index) => ({
|
||||||
|
x: Math.round(index * stepX * 100) / 100,
|
||||||
|
y:
|
||||||
|
Math.round(
|
||||||
|
clamp(viewportHeight - ((point - bounds.min) / span) * viewportHeight, 1, viewportHeight - 1) * 100
|
||||||
|
) / 100
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLinePath(rawPoints: number[], bounds: { min: number; max: number }): string {
|
||||||
|
const points = convertPoints(rawPoints, bounds);
|
||||||
|
if (!points.length) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x} ${point.y}`).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
$: bounds = resolveBounds(panel.series);
|
||||||
|
$: renderedSeries = panel.series.map(
|
||||||
|
(series): SeriesRenderShape => ({
|
||||||
|
id: series.id,
|
||||||
|
tone: series.tone,
|
||||||
|
linePath: createLinePath(series.points, bounds)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
$: latestValue = formatMetric(panel.latest);
|
||||||
|
$: minValue = formatMetric(panel.min);
|
||||||
|
$: maxValue = formatMetric(panel.max);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<article
|
||||||
|
class="signal-panel side-{panel.side}"
|
||||||
|
class:is-active={panel.active}
|
||||||
|
aria-hidden={!panel.active}
|
||||||
|
style="--panel-index: {panelIndex};"
|
||||||
|
>
|
||||||
|
<header class="panel-head">
|
||||||
|
<div class="head-text">
|
||||||
|
<p class="panel-code">{panel.code}</p>
|
||||||
|
<p class="panel-title">{panel.title}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="icon-layer" aria-hidden="true">
|
||||||
|
{#each panel.icons as icon (icon.id)}
|
||||||
|
<span class="icon-chip tone-{icon.tone}">{icon.label}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="chart-stage">
|
||||||
|
<svg viewBox="0 0 {viewportWidth} {viewportHeight}" preserveAspectRatio="none" role="img" aria-label={panel.title}>
|
||||||
|
<g class="grid-line-group">
|
||||||
|
{#each [6, 12, 18, 24, 30] as y}
|
||||||
|
<line x1="0" y1={y} x2={viewportWidth} y2={y}></line>
|
||||||
|
{/each}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{#each renderedSeries as series (series.id)}
|
||||||
|
{#if series.linePath}
|
||||||
|
<path d={series.linePath} class="series-line tone-{series.tone}" />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div class="scan-haze" aria-hidden="true"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="panel-foot">
|
||||||
|
<p class="foot-item">
|
||||||
|
<span class="dot tone-cyan"></span>
|
||||||
|
<span class="metric-label">Now</span>
|
||||||
|
<span class="value">{latestValue}</span>
|
||||||
|
</p>
|
||||||
|
<p class="foot-item">
|
||||||
|
<span class="dot tone-lime"></span>
|
||||||
|
<span class="metric-label">Max</span>
|
||||||
|
<span class="value">{maxValue}</span>
|
||||||
|
</p>
|
||||||
|
<p class="foot-item">
|
||||||
|
<span class="dot tone-orange"></span>
|
||||||
|
<span class="metric-label">Min</span>
|
||||||
|
<span class="value">{minValue}</span>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.signal-panel {
|
||||||
|
--offset-x: 12%;
|
||||||
|
--enter-ms: 1800ms;
|
||||||
|
--fade-ms: 1000ms;
|
||||||
|
overflow: hidden;
|
||||||
|
inline-size: min(100%, clamp(16.8rem, 23vw, 22rem));
|
||||||
|
aspect-ratio: 1.44 / 1;
|
||||||
|
min-block-size: 11.8rem;
|
||||||
|
justify-self: start;
|
||||||
|
border: 1px solid rgb(130 174 202 / 0.42);
|
||||||
|
border-radius: 0.92rem;
|
||||||
|
padding: 0.56rem 0.62rem 0.58rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(160deg, rgb(10 18 26 / 0.76) 0%, rgb(3 8 12 / 0.62) 48%, rgb(1 2 4 / 0.76) 100%),
|
||||||
|
radial-gradient(circle at 12% 0, rgb(62 232 255 / 0.1), transparent 40%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgb(165 224 255 / 0.08),
|
||||||
|
inset 0 -24px 32px rgb(0 0 0 / 0.48),
|
||||||
|
0 0 14px rgb(62 232 255 / 0.14);
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0) scale(1) rotate(0);
|
||||||
|
transition:
|
||||||
|
opacity var(--fade-ms) cubic-bezier(0.18, 0.88, 0.3, 1),
|
||||||
|
transform var(--enter-ms) cubic-bezier(0.2, 0.9, 0.28, 1),
|
||||||
|
border-color 460ms ease,
|
||||||
|
filter 760ms ease;
|
||||||
|
transition-delay: calc(var(--panel-index) * 140ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-panel.side-left {
|
||||||
|
--offset-x: -132%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-panel.side-right {
|
||||||
|
--offset-x: 132%;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-panel:not(.is-active) {
|
||||||
|
border-color: transparent;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(var(--offset-x)) scale(0.98) rotate(-0.6deg);
|
||||||
|
filter: blur(1.3px);
|
||||||
|
pointer-events: none;
|
||||||
|
transition-delay: 0ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-block-end: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head-text {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-code {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.63rem;
|
||||||
|
color: rgb(153 188 211 / 0.88);
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
margin: 0.12rem 0 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: rgb(225 243 255 / 0.96);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-layer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.26rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chip {
|
||||||
|
border: 1px solid rgb(138 178 204 / 0.44);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.08rem 0.36rem;
|
||||||
|
font-size: 0.58rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: rgb(209 237 255 / 0.94);
|
||||||
|
background: rgb(5 13 20 / 0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chip.tone-cyan {
|
||||||
|
border-color: rgb(62 232 255 / 0.54);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chip.tone-lime {
|
||||||
|
border-color: rgb(133 255 68 / 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chip.tone-orange {
|
||||||
|
border-color: rgb(255 91 63 / 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chip.tone-violet {
|
||||||
|
border-color: rgb(171 118 255 / 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chip.tone-gold {
|
||||||
|
border-color: rgb(255 206 84 / 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chip.tone-rose {
|
||||||
|
border-color: rgb(255 108 176 / 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-stage {
|
||||||
|
position: relative;
|
||||||
|
block-size: clamp(6.4rem, 9vw, 8.2rem);
|
||||||
|
border: 1px solid rgb(132 174 200 / 0.32);
|
||||||
|
border-radius: 0.62rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(8 17 26 / 0.68), rgb(1 6 10 / 0.78)),
|
||||||
|
radial-gradient(circle at 50% 0, rgb(62 232 255 / 0.09), transparent 45%);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-line-group line {
|
||||||
|
stroke: rgb(138 184 210 / 0.16);
|
||||||
|
stroke-width: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-line {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 1.3;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
filter: drop-shadow(0 0 2px rgb(0 0 0 / 0.42));
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-line.tone-cyan {
|
||||||
|
stroke: rgb(62 232 255 / 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-line.tone-lime {
|
||||||
|
stroke: rgb(133 255 68 / 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-line.tone-orange {
|
||||||
|
stroke: rgb(255 91 63 / 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-line.tone-violet {
|
||||||
|
stroke: rgb(171 118 255 / 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-line.tone-gold {
|
||||||
|
stroke: rgb(255 206 84 / 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-line.tone-rose {
|
||||||
|
stroke: rgb(255 108 176 / 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-haze {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
repeating-linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgb(146 191 214 / 0.04) 0,
|
||||||
|
rgb(146 191 214 / 0.04) 1px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 3px
|
||||||
|
),
|
||||||
|
linear-gradient(180deg, transparent 0%, rgb(62 232 255 / 0.06) 50%, transparent 100%);
|
||||||
|
background-size: 100% 100%, 100% 100%;
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-foot {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin: 0.4rem 0 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foot-item {
|
||||||
|
margin: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
color: rgb(173 206 227 / 0.9);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
inline-size: 0.34rem;
|
||||||
|
block-size: 0.34rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.tone-cyan {
|
||||||
|
background: rgb(62 232 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.tone-lime {
|
||||||
|
background: rgb(133 255 68);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.tone-orange {
|
||||||
|
background: rgb(255 91 63);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
color: rgb(144 172 191 / 0.82);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
min-inline-size: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.signal-panel {
|
||||||
|
inline-size: min(100%, clamp(14rem, 30vw, 17rem));
|
||||||
|
aspect-ratio: 1.5 / 1;
|
||||||
|
min-block-size: 10.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 900px) {
|
||||||
|
.signal-panel {
|
||||||
|
inline-size: min(100%, clamp(15rem, 22vw, 18.5rem));
|
||||||
|
min-block-size: 10.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-stage {
|
||||||
|
block-size: clamp(5.7rem, 7.6vw, 6.9rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 760px) {
|
||||||
|
.signal-panel {
|
||||||
|
inline-size: min(100%, clamp(13.8rem, 20vw, 16.5rem));
|
||||||
|
min-block-size: 9.8rem;
|
||||||
|
padding: 0.46rem 0.5rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-foot {
|
||||||
|
margin-block-start: 0.28rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-stage {
|
||||||
|
block-size: clamp(5rem, 6.6vw, 6rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 680px) {
|
||||||
|
.signal-panel {
|
||||||
|
inline-size: min(100%, clamp(12.8rem, 18vw, 15rem));
|
||||||
|
min-block-size: 8.7rem;
|
||||||
|
padding: 0.4rem 0.46rem 0.44rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-head {
|
||||||
|
margin-block-end: 0.26rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-foot {
|
||||||
|
margin-block-start: 0.18rem;
|
||||||
|
gap: 0.56rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-stage {
|
||||||
|
block-size: clamp(4.4rem, 5.6vw, 5.4rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.signal-panel {
|
||||||
|
inline-size: 100%;
|
||||||
|
aspect-ratio: 1.7 / 1;
|
||||||
|
min-block-size: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
436
src/lib/components/SummaryCurve.svelte
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HudSummary } from "$lib/types/hud";
|
||||||
|
|
||||||
|
export let summary: HudSummary;
|
||||||
|
export let side: "left" | "right" = "right";
|
||||||
|
export let panelIndex = 0;
|
||||||
|
|
||||||
|
const viewportWidth = 100;
|
||||||
|
const viewportHeight = 36;
|
||||||
|
const verticalInset = 2;
|
||||||
|
|
||||||
|
interface PlotPoint {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatValue(value: number | null): string {
|
||||||
|
if (value === null) {
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.toFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBounds(points: number[]): { min: number; max: number } {
|
||||||
|
if (points.length === 0) {
|
||||||
|
return { min: 0, max: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = Math.min(...points);
|
||||||
|
const max = Math.max(...points);
|
||||||
|
|
||||||
|
if (Math.abs(max - min) < 0.001) {
|
||||||
|
return { min: min - 1, max: max + 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { min, max };
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertPoints(rawPoints: number[]): PlotPoint[] {
|
||||||
|
if (rawPoints.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawPoints.length === 1) {
|
||||||
|
return [{ x: viewportWidth / 2, y: viewportHeight / 2 }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { min, max } = resolveBounds(rawPoints);
|
||||||
|
const span = max - min;
|
||||||
|
const chartHeight = viewportHeight - verticalInset * 2;
|
||||||
|
const stepX = viewportWidth / (rawPoints.length - 1);
|
||||||
|
|
||||||
|
return rawPoints.map((point, index) => {
|
||||||
|
const ratio = span <= 0 ? 0.5 : (point - min) / span;
|
||||||
|
const y = viewportHeight - verticalInset - ratio * chartHeight;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: Math.round(index * stepX * 100) / 100,
|
||||||
|
y: Math.round(clamp(y, verticalInset, viewportHeight - verticalInset) * 100) / 100
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLinePath(points: PlotPoint[]): string {
|
||||||
|
if (points.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x} ${point.y}`).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAreaPath(points: PlotPoint[]): string {
|
||||||
|
if (points.length < 2) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const linePath = createLinePath(points);
|
||||||
|
const firstPoint = points[0];
|
||||||
|
const lastPoint = points[points.length - 1];
|
||||||
|
|
||||||
|
return `${linePath} L ${lastPoint.x} ${viewportHeight} L ${firstPoint.x} ${viewportHeight} Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: plotPoints = convertPoints(summary.points);
|
||||||
|
$: linePath = createLinePath(plotPoints);
|
||||||
|
$: areaPath = createAreaPath(plotPoints);
|
||||||
|
$: lastPoint = plotPoints.length > 0 ? plotPoints[plotPoints.length - 1] : null;
|
||||||
|
$: latestValue = formatValue(summary.latest);
|
||||||
|
$: minValue = formatValue(summary.min);
|
||||||
|
$: maxValue = formatValue(summary.max);
|
||||||
|
$: sampleCount = summary.points.length;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<article
|
||||||
|
class="signal-panel summary-panel side-{side}"
|
||||||
|
class:is-empty={sampleCount === 0}
|
||||||
|
aria-hidden={sampleCount === 0}
|
||||||
|
style="--panel-index: {panelIndex};"
|
||||||
|
>
|
||||||
|
<header class="panel-head">
|
||||||
|
<div class="head-text">
|
||||||
|
<p class="panel-code">TOT</p>
|
||||||
|
<p class="panel-title">{summary.label}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="icon-layer" aria-hidden="true">
|
||||||
|
<span class="icon-chip tone-cyan">NOW</span>
|
||||||
|
<span class="icon-chip tone-lime">MIN</span>
|
||||||
|
<span class="icon-chip tone-orange">MAX</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="chart-stage">
|
||||||
|
<svg viewBox="0 0 {viewportWidth} {viewportHeight}" preserveAspectRatio="none" role="img" aria-label={summary.label}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="summary-fill" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="rgb(62 232 255 / 0.28)" />
|
||||||
|
<stop offset="100%" stop-color="rgb(62 232 255 / 0.02)" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<g class="grid-lines" aria-hidden="true">
|
||||||
|
{#each [6, 12, 18, 24, 30] as y}
|
||||||
|
<line x1="0" y1={y} x2={viewportWidth} y2={y}></line>
|
||||||
|
{/each}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{#if areaPath}
|
||||||
|
<path d={areaPath} class="summary-area"></path>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if linePath}
|
||||||
|
<path d={linePath} class="summary-line"></path>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if lastPoint}
|
||||||
|
<circle cx={lastPoint.x} cy={lastPoint.y} r="1.7" class="summary-dot"></circle>
|
||||||
|
{/if}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{#if sampleCount === 0}
|
||||||
|
<div class="empty-state">
|
||||||
|
<span>Waiting</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="panel-foot">
|
||||||
|
<p class="foot-item">
|
||||||
|
<span class="dot tone-cyan"></span>
|
||||||
|
<span class="metric-text">Now</span>
|
||||||
|
<span class="value">{latestValue}</span>
|
||||||
|
</p>
|
||||||
|
<p class="foot-item">
|
||||||
|
<span class="dot tone-lime"></span>
|
||||||
|
<span class="metric-text">Min</span>
|
||||||
|
<span class="value">{minValue}</span>
|
||||||
|
</p>
|
||||||
|
<p class="foot-item">
|
||||||
|
<span class="dot tone-orange"></span>
|
||||||
|
<span class="metric-text">Max</span>
|
||||||
|
<span class="value">{maxValue}</span>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.signal-panel {
|
||||||
|
--offset-x: 12%;
|
||||||
|
--enter-ms: 1800ms;
|
||||||
|
--fade-ms: 1000ms;
|
||||||
|
overflow: hidden;
|
||||||
|
inline-size: min(100%, clamp(16.8rem, 23vw, 22rem));
|
||||||
|
aspect-ratio: 1.44 / 1;
|
||||||
|
min-block-size: 11.8rem;
|
||||||
|
justify-self: start;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.56rem 0.62rem 0.58rem;
|
||||||
|
border: 1px solid rgb(130 174 202 / 0.42);
|
||||||
|
border-radius: 0.92rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(160deg, rgb(10 18 26 / 0.76) 0%, rgb(3 8 12 / 0.62) 48%, rgb(1 2 4 / 0.76) 100%),
|
||||||
|
radial-gradient(circle at 12% 0, rgb(62 232 255 / 0.1), transparent 40%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgb(165 224 255 / 0.08),
|
||||||
|
inset 0 -24px 32px rgb(0 0 0 / 0.48),
|
||||||
|
0 0 14px rgb(62 232 255 / 0.14);
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0) scale(1) rotate(0);
|
||||||
|
transition:
|
||||||
|
opacity var(--fade-ms) cubic-bezier(0.18, 0.88, 0.3, 1),
|
||||||
|
transform var(--enter-ms) cubic-bezier(0.2, 0.9, 0.28, 1),
|
||||||
|
border-color 460ms ease,
|
||||||
|
filter 760ms ease;
|
||||||
|
transition-delay: calc(var(--panel-index) * 140ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-panel.side-left {
|
||||||
|
--offset-x: -132%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-panel.side-right {
|
||||||
|
--offset-x: 132%;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-panel.is-empty {
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-block-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head-text {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-code {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.63rem;
|
||||||
|
color: rgb(153 188 211 / 0.88);
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
margin: 0.12rem 0 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: rgb(225 243 255 / 0.96);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-layer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.26rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chip {
|
||||||
|
border: 1px solid rgb(138 178 204 / 0.44);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.08rem 0.36rem;
|
||||||
|
font-size: 0.58rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: rgb(209 237 255 / 0.94);
|
||||||
|
background: rgb(5 13 20 / 0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chip.tone-cyan {
|
||||||
|
border-color: rgb(62 232 255 / 0.54);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chip.tone-lime {
|
||||||
|
border-color: rgb(133 255 68 / 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-chip.tone-orange {
|
||||||
|
border-color: rgb(255 91 63 / 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-stage {
|
||||||
|
position: relative;
|
||||||
|
block-size: clamp(6.4rem, 9vw, 8.2rem);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgb(132 174 200 / 0.32);
|
||||||
|
border-radius: 0.62rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgb(8 17 26 / 0.68), rgb(1 6 10 / 0.78)),
|
||||||
|
radial-gradient(circle at 50% 0, rgb(62 232 255 / 0.09), transparent 45%);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-lines line {
|
||||||
|
stroke: rgb(138 184 210 / 0.16);
|
||||||
|
stroke-width: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-area {
|
||||||
|
fill: url(#summary-fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-line {
|
||||||
|
fill: none;
|
||||||
|
stroke: rgb(62 232 255 / 0.96);
|
||||||
|
stroke-width: 1.35;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
filter: drop-shadow(0 0 4px rgb(62 232 255 / 0.22));
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-dot {
|
||||||
|
fill: rgb(133 255 68 / 0.98);
|
||||||
|
filter: drop-shadow(0 0 6px rgb(133 255 68 / 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgb(155 186 204 / 0.76);
|
||||||
|
font-size: 0.66rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: linear-gradient(180deg, rgb(2 7 11 / 0.06), rgb(2 7 11 / 0.18));
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-foot {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foot-item {
|
||||||
|
margin: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.28rem;
|
||||||
|
color: rgb(173 206 227 / 0.9);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-text {
|
||||||
|
color: rgb(146 173 191 / 0.82);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
inline-size: 0.34rem;
|
||||||
|
block-size: 0.34rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.tone-cyan {
|
||||||
|
background: rgb(62 232 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.tone-lime {
|
||||||
|
background: rgb(133 255 68);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.tone-orange {
|
||||||
|
background: rgb(255 91 63);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
min-inline-size: 2.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.signal-panel {
|
||||||
|
inline-size: min(100%, clamp(14rem, 30vw, 17rem));
|
||||||
|
aspect-ratio: 1.5 / 1;
|
||||||
|
min-block-size: 10.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 900px) {
|
||||||
|
.signal-panel {
|
||||||
|
inline-size: min(100%, clamp(15rem, 22vw, 18.5rem));
|
||||||
|
min-block-size: 10.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-stage {
|
||||||
|
block-size: clamp(5.7rem, 7.6vw, 6.9rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 760px) {
|
||||||
|
.signal-panel {
|
||||||
|
inline-size: min(100%, clamp(13.8rem, 20vw, 16.5rem));
|
||||||
|
min-block-size: 9.8rem;
|
||||||
|
padding: 0.46rem 0.5rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-foot {
|
||||||
|
margin-block-start: 0.28rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-stage {
|
||||||
|
block-size: clamp(5rem, 6.6vw, 6rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 680px) {
|
||||||
|
.signal-panel {
|
||||||
|
inline-size: min(100%, clamp(12.8rem, 18vw, 15rem));
|
||||||
|
min-block-size: 8.7rem;
|
||||||
|
padding: 0.4rem 0.46rem 0.44rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-head {
|
||||||
|
margin-block-end: 0.26rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-foot {
|
||||||
|
margin-block-start: 0.18rem;
|
||||||
|
gap: 0.56rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-stage {
|
||||||
|
block-size: clamp(4.4rem, 5.6vw, 5.4rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.signal-panel {
|
||||||
|
inline-size: 100%;
|
||||||
|
aspect-ratio: 1.7 / 1;
|
||||||
|
min-block-size: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
57
src/lib/config/color-map.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { PressureColorMapPreset } from "$lib/types/hud";
|
||||||
|
|
||||||
|
export interface PressureColorPalette {
|
||||||
|
surfaceBase: string;
|
||||||
|
surfaceLow: string;
|
||||||
|
surfaceMid: string;
|
||||||
|
surfaceHigh: string;
|
||||||
|
surfaceHot: string;
|
||||||
|
labelZero: string;
|
||||||
|
labelLow: string;
|
||||||
|
labelMid: string;
|
||||||
|
labelHigh: string;
|
||||||
|
rangeStops: [string, string, string, string, string, string];
|
||||||
|
rangeGlow: [string, string, string];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pressureColorPalettes: Record<PressureColorMapPreset, PressureColorPalette> = {
|
||||||
|
emerald: {
|
||||||
|
surfaceBase: "#13201a",
|
||||||
|
surfaceLow: "#285338",
|
||||||
|
surfaceMid: "#3f8a66",
|
||||||
|
surfaceHigh: "#6dd3ad",
|
||||||
|
surfaceHot: "#d9fff0",
|
||||||
|
labelZero: "#2d8d59",
|
||||||
|
labelLow: "#54df8e",
|
||||||
|
labelMid: "#98e6ff",
|
||||||
|
labelHigh: "#ffab78",
|
||||||
|
rangeStops: ["#13201a", "#285338", "#3f8a66", "#6dd3ad", "#98e6ff", "#ffab78"],
|
||||||
|
rangeGlow: ["#54df8e", "#98e6ff", "#ffab78"]
|
||||||
|
},
|
||||||
|
arctic: {
|
||||||
|
surfaceBase: "#08141d",
|
||||||
|
surfaceLow: "#14354d",
|
||||||
|
surfaceMid: "#1f6690",
|
||||||
|
surfaceHigh: "#58bee8",
|
||||||
|
surfaceHot: "#f1fdff",
|
||||||
|
labelZero: "#3f87ae",
|
||||||
|
labelLow: "#6dc8ff",
|
||||||
|
labelMid: "#aef3ff",
|
||||||
|
labelHigh: "#ffffff",
|
||||||
|
rangeStops: ["#08141d", "#14354d", "#1f6690", "#58bee8", "#aef3ff", "#ffffff"],
|
||||||
|
rangeGlow: ["#5ea9ff", "#7fe5ff", "#ffffff"]
|
||||||
|
},
|
||||||
|
ember: {
|
||||||
|
surfaceBase: "#1b0c08",
|
||||||
|
surfaceLow: "#4a1f15",
|
||||||
|
surfaceMid: "#8f4124",
|
||||||
|
surfaceHigh: "#d9772f",
|
||||||
|
surfaceHot: "#fff1d8",
|
||||||
|
labelZero: "#b9582f",
|
||||||
|
labelLow: "#ff8a4e",
|
||||||
|
labelMid: "#ffd06a",
|
||||||
|
labelHigh: "#fff4df",
|
||||||
|
rangeStops: ["#1b0c08", "#4a1f15", "#8f4124", "#d9772f", "#ffd06a", "#fff4df"],
|
||||||
|
rangeGlow: ["#ff7247", "#ffb14d", "#fff4df"]
|
||||||
|
}
|
||||||
|
};
|
||||||
50
src/lib/styles/theme.css
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--hud-bg-00: #000000;
|
||||||
|
--hud-bg-10: #050607;
|
||||||
|
--hud-bg-20: #0b0e11;
|
||||||
|
--hud-bg-30: #030405;
|
||||||
|
|
||||||
|
--hud-cyan: #3ee8ff;
|
||||||
|
--hud-lime: #85ff44;
|
||||||
|
--hud-orange: #ff5b3f;
|
||||||
|
--hud-range-0: #13201a;
|
||||||
|
--hud-range-1: #285338;
|
||||||
|
--hud-range-2: #3f8a66;
|
||||||
|
--hud-range-3: #6dd3ad;
|
||||||
|
--hud-range-4: #98e6ff;
|
||||||
|
--hud-range-5: #ffab78;
|
||||||
|
|
||||||
|
--hud-text-main: #cfe7ff;
|
||||||
|
--hud-text-dim: #86a2b8;
|
||||||
|
|
||||||
|
/* Keep root surface close to the main board style to avoid visible drag-edge seams. */
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 18% 8%, rgb(62 232 255 / 0.05), transparent 38%),
|
||||||
|
radial-gradient(circle at 84% 14%, rgb(133 255 68 / 0.04), transparent 36%),
|
||||||
|
linear-gradient(165deg, var(--hud-bg-20) 0%, var(--hud-bg-10) 48%, var(--hud-bg-30) 100%);
|
||||||
|
background-color: var(--hud-bg-00);
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Rajdhani", "Segoe UI", "PingFang SC", sans-serif;
|
||||||
|
color: var(--hud-text-main);
|
||||||
|
background: inherit;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
144
src/lib/types/hud.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
export type LocaleCode = "zh-CN" | "en-US";
|
||||||
|
|
||||||
|
export type WindowControlAction = "minimize" | "toggle-maximize" | "close";
|
||||||
|
|
||||||
|
export type ConnectionState = "online" | "connecting" | "offline";
|
||||||
|
|
||||||
|
export type StageStatusTone = "ok" | "warn" | "idle";
|
||||||
|
export type HudNoticeTone = "ok" | "warn" | "info";
|
||||||
|
|
||||||
|
export type SignalTone = "cyan" | "lime" | "orange" | "violet" | "gold" | "rose";
|
||||||
|
export type PressureColorMapPreset = "emerald" | "arctic" | "ember";
|
||||||
|
|
||||||
|
export type SignalPanelSide = "left" | "right";
|
||||||
|
|
||||||
|
export type HudConfigTone = "neutral" | "cyan" | "lime" | "orange";
|
||||||
|
|
||||||
|
export interface HudSignalSeries {
|
||||||
|
id: string;
|
||||||
|
tone: SignalTone;
|
||||||
|
points: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HudSignalIcon {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
tone: SignalTone;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HudSignalPanel {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
title: string;
|
||||||
|
side: SignalPanelSide;
|
||||||
|
active: boolean;
|
||||||
|
series: HudSignalSeries[];
|
||||||
|
icons: HudSignalIcon[];
|
||||||
|
latest: number | null;
|
||||||
|
min: number | null;
|
||||||
|
max: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HudPacket {
|
||||||
|
ts: number;
|
||||||
|
panels: HudSignalPanel[];
|
||||||
|
summary: HudSummary;
|
||||||
|
pressureMatrix: number[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HudSummary {
|
||||||
|
label: string;
|
||||||
|
points: number[];
|
||||||
|
latest: number | null;
|
||||||
|
min: number | null;
|
||||||
|
max: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HudConfigLink {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
tone?: HudConfigTone;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HudColorMapOption {
|
||||||
|
id: PressureColorMapPreset;
|
||||||
|
label: string;
|
||||||
|
previewStops: [string, string, string];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HudCopy {
|
||||||
|
appName: string;
|
||||||
|
suiteName: string;
|
||||||
|
stageTitle: string;
|
||||||
|
stageHint: string;
|
||||||
|
configPanelTitle: string;
|
||||||
|
configPanelHint: string;
|
||||||
|
matrixSizeLabel: string;
|
||||||
|
matrixRowsLabel: string;
|
||||||
|
matrixColsLabel: string;
|
||||||
|
rangeLabel: string;
|
||||||
|
rangeMinLabel: string;
|
||||||
|
rangeMaxLabel: string;
|
||||||
|
colorMapLabel: string;
|
||||||
|
resetConfigLabel: string;
|
||||||
|
applyLiveHint: string;
|
||||||
|
runtimeReady: string;
|
||||||
|
runtimeFallback: string;
|
||||||
|
controlArea: string;
|
||||||
|
serialPortLabel: string;
|
||||||
|
connectionLabel: string;
|
||||||
|
deviceLabel: string;
|
||||||
|
sampleRateLabel: string;
|
||||||
|
channelsLabel: string;
|
||||||
|
configLinksLabel: string;
|
||||||
|
refreshPortsLabel: string;
|
||||||
|
connectActionLabel: string;
|
||||||
|
disconnectActionLabel: string;
|
||||||
|
exportActionLabel: string;
|
||||||
|
exportingActionLabel: string;
|
||||||
|
importActionLabel: string;
|
||||||
|
replaySectionLabel: string;
|
||||||
|
replayPlayLabel: string;
|
||||||
|
replayPauseLabel: string;
|
||||||
|
replayStopLabel: string;
|
||||||
|
replaySpeedLabel: string;
|
||||||
|
replayProgressLabel: string;
|
||||||
|
replayEmptyHint: string;
|
||||||
|
connectedLabel: string;
|
||||||
|
connectingLabel: string;
|
||||||
|
disconnectedLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HudMatrixConfig {
|
||||||
|
rows: number;
|
||||||
|
cols: number;
|
||||||
|
rangeMin: number;
|
||||||
|
rangeMax: number;
|
||||||
|
colorMapPreset: PressureColorMapPreset;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SerialConnectResult {
|
||||||
|
port: string;
|
||||||
|
connected: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SerialExportResult {
|
||||||
|
path: string;
|
||||||
|
frameCount: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SerialImportFrameResult {
|
||||||
|
data: number[];
|
||||||
|
dtsMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SerialImportResult {
|
||||||
|
fileName: string;
|
||||||
|
frameCount: number;
|
||||||
|
channelCount: number;
|
||||||
|
frames: SerialImportFrameResult[];
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
17
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const handleContextMenu = (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("contextmenu", handleContextMenu, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("contextmenu", handleContextMenu, true);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
||||||
5
src/routes/+layout.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Tauri doesn't have a Node.js server to do proper SSR
|
||||||
|
// so we use adapter-static with a fallback to index.html to put the site in SPA mode
|
||||||
|
// See: https://svelte.dev/docs/kit/single-page-apps
|
||||||
|
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
|
||||||
|
export const ssr = false;
|
||||||
1445
src/routes/+page.svelte
Normal file
BIN
static/favicon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
1
static/svelte.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
6
static/tauri.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||||
|
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
1
static/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
18
svelte.config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Tauri doesn't have a Node.js server to do proper SSR
|
||||||
|
// so we use adapter-static with a fallback to index.html to put the site in SPA mode
|
||||||
|
// See: https://svelte.dev/docs/kit/single-page-apps
|
||||||
|
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
|
||||||
|
import adapter from "@sveltejs/adapter-static";
|
||||||
|
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
fallback: "index.html",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
554
tauri-event.md
Normal file
@@ -0,0 +1,554 @@
|
|||||||
|
# Tauri Event Demo
|
||||||
|
|
||||||
|
这份笔记对应当前工程里的“串口后台任务 -> Tauri event -> 前端折线图”这条链路。
|
||||||
|
|
||||||
|
目标是把一类很常见的需求拆清楚:
|
||||||
|
|
||||||
|
- 前端点一次按钮,调用 Rust command
|
||||||
|
- Rust 启动一个长期运行的后台任务
|
||||||
|
- 后台任务持续读取数据
|
||||||
|
- 每拿到一帧数据,就通过 Tauri event 推给前端
|
||||||
|
- 前端监听 event,直接刷新图表
|
||||||
|
|
||||||
|
## 0. 2026-03-25 需求更新(WebGL 点阵改用真实数据)
|
||||||
|
|
||||||
|
这次改动的目标是:
|
||||||
|
|
||||||
|
- WebGL dot matrix 不再使用虚拟 demo 数据
|
||||||
|
- 直接消费串口解析后的真实通道值
|
||||||
|
- 默认矩阵大小改为 `row=12, col=7`
|
||||||
|
|
||||||
|
### 是否需要新建 event?
|
||||||
|
|
||||||
|
不需要。继续使用现有 `hud_stream` 即可,只扩展 payload 字段:
|
||||||
|
|
||||||
|
- 原来:`{ ts, panels, summary }`
|
||||||
|
- 现在:`{ ts, panels, summary, pressureMatrix }`
|
||||||
|
|
||||||
|
这样做的好处是:
|
||||||
|
|
||||||
|
- 前后端事件通道不变,兼容原有监听逻辑
|
||||||
|
- 折线图和点阵图保持同一时间基准
|
||||||
|
- 只需在 `HudPacket` 上增量扩展,不引入额外生命周期管理
|
||||||
|
|
||||||
|
### 本次代码落地点
|
||||||
|
|
||||||
|
- 后端 `HudPacket` 新增 `pressure_matrix`(序列化为 `pressureMatrix`):
|
||||||
|
- `src-tauri/src/serial_core/model.rs`
|
||||||
|
- 串口读循环在拿到解析值后写入 matrix 状态:
|
||||||
|
- `src-tauri/src/serial_core/serial.rs`
|
||||||
|
- 前端类型 `HudPacket` 新增 `pressureMatrix: number[] | null`:
|
||||||
|
- `src/lib/types/hud.ts`
|
||||||
|
- 页面把 `pressureMatrix` 透传到 `CenterStage -> PressureMatrixViewer`:
|
||||||
|
- `src/routes/+page.svelte`
|
||||||
|
- `PressureMatrixViewer` 去除 demo 生成逻辑;无真实数据时显示全 0:
|
||||||
|
- `src/lib/components/PressureMatrixViewer.svelte`
|
||||||
|
- 默认矩阵改为 `12 x 7`(含 reset 默认值):
|
||||||
|
- `src/routes/+page.svelte`
|
||||||
|
- `src/lib/components/CenterStage.svelte`
|
||||||
|
- `src/lib/components/ConfigPanel.svelte`
|
||||||
|
|
||||||
|
## 1. 什么时候用 command,什么时候用 event
|
||||||
|
|
||||||
|
先记一个最实用的判断:
|
||||||
|
|
||||||
|
- `command` 适合“请求一次,返回一次”
|
||||||
|
- `event` 适合“后台持续推送”
|
||||||
|
|
||||||
|
在这个项目里:
|
||||||
|
|
||||||
|
- `serial_enum` 是 command,因为它是“一次查询串口列表”
|
||||||
|
- `serial_connect` 是 command,因为它是“一次启动连接”
|
||||||
|
- `serial_disconnect` 是 command,因为它是“一次停止连接”
|
||||||
|
- `hud_stream` 是 event,因为图表数据会持续不断地到来
|
||||||
|
|
||||||
|
如果你用 command 去不停轮询图表数据,也能做,但会有这些问题:
|
||||||
|
|
||||||
|
- 前后端耦合更重
|
||||||
|
- 轮询频率不好拿捏
|
||||||
|
- 串口数据到了也不能立刻推到前端
|
||||||
|
- 后面做 TCP、日志流、状态流时会越来越别扭
|
||||||
|
|
||||||
|
所以像串口采样、TCP telemetry、日志输出、设备状态广播,这类都更适合 event。
|
||||||
|
|
||||||
|
## 2. 当前工程里各文件的职责
|
||||||
|
|
||||||
|
### Rust
|
||||||
|
|
||||||
|
- `src-tauri/src/lib.rs`
|
||||||
|
- 注册 command
|
||||||
|
- 注册全局状态 `SerialConnectionState`
|
||||||
|
|
||||||
|
- `src-tauri/src/commands/serial.rs`
|
||||||
|
- 给前端暴露 command
|
||||||
|
- 管理串口后台任务的生命周期
|
||||||
|
- 这里尽量保持“薄”
|
||||||
|
|
||||||
|
- `src-tauri/src/serial_core/serial.rs`
|
||||||
|
- 真正的串口读循环
|
||||||
|
- 解码 frame
|
||||||
|
- 发出 `hud_stream`
|
||||||
|
|
||||||
|
- `src-tauri/src/serial_core/model.rs`
|
||||||
|
- 定义发给前端的结构
|
||||||
|
- 把原始 frame 转成前端折线图直接能吃的 `HudPacket`
|
||||||
|
|
||||||
|
- `src-tauri/src/serial_core/codecs/test.rs`
|
||||||
|
- 协议解码
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- `src/routes/+page.svelte`
|
||||||
|
- 调用 `serial_connect` / `serial_disconnect`
|
||||||
|
- 监听 `hud_stream`
|
||||||
|
- 拿到 `HudPacket` 后刷新页面状态
|
||||||
|
|
||||||
|
- `src/lib/types/hud.ts`
|
||||||
|
- 前端使用的 HUD 类型定义
|
||||||
|
|
||||||
|
## 3. 完整数据流
|
||||||
|
|
||||||
|
当前链路可以按下面理解:
|
||||||
|
|
||||||
|
### 第一步:前端发起连接
|
||||||
|
|
||||||
|
前端点击按钮后:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await invoke("serial_connect", { port })
|
||||||
|
```
|
||||||
|
|
||||||
|
这一步只负责“启动”,不要指望它直接把流式数据返回给前端。
|
||||||
|
|
||||||
|
### 第二步:Rust command 启动后台任务
|
||||||
|
|
||||||
|
`serial_connect` 做的事应该很少:
|
||||||
|
|
||||||
|
1. 校验参数
|
||||||
|
2. 打开串口
|
||||||
|
3. 创建取消信号
|
||||||
|
4. `spawn` 后台任务
|
||||||
|
5. 把任务句柄保存到全局状态
|
||||||
|
6. 立刻返回前端
|
||||||
|
|
||||||
|
也就是说,command 只是“开机按钮”。
|
||||||
|
|
||||||
|
### 第三步:后台任务持续读串口
|
||||||
|
|
||||||
|
后台任务跑在 `serial_core/serial.rs` 里:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel.cancelled() => break,
|
||||||
|
read_result = port.read(&mut buffer) => {
|
||||||
|
let n = read_result?;
|
||||||
|
let frames = codec.decode(&buffer[..n])?;
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这里有两个关键点:
|
||||||
|
|
||||||
|
- 必须只解码 `&buffer[..n]`
|
||||||
|
- 长循环不要放在 command 里 `await` 到结束
|
||||||
|
|
||||||
|
第一点是为了避免把缓冲区后面没读到的 `0` 一起喂给解码器。
|
||||||
|
第二点是为了避免前端永远等不到 command 返回。
|
||||||
|
|
||||||
|
### 第四步:frame 转成前端友好的 packet
|
||||||
|
|
||||||
|
后台拿到 frame 后,不要把原始协议直接丢给前端,先整理成业务结构:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let packet = chart_state.apply_frame(&frame);
|
||||||
|
```
|
||||||
|
|
||||||
|
这里输出的是 `HudPacket`,它的结构是前端图表直接能渲染的形状。
|
||||||
|
|
||||||
|
### 第五步:Rust 发 event
|
||||||
|
|
||||||
|
```rust
|
||||||
|
app.emit("hud_stream", packet)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
这一步就是把数据从 Rust 主动推给前端。
|
||||||
|
|
||||||
|
### 第六步:前端监听 event
|
||||||
|
|
||||||
|
前端在页面挂载时注册监听:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const unlisten = await listen<HudPacket>("hud_stream", (event) => {
|
||||||
|
applyPacket(event.payload);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
拿到 payload 后直接更新 `signalPanels`,图表就会刷新。
|
||||||
|
|
||||||
|
## 4. 为什么要保存后台任务句柄
|
||||||
|
|
||||||
|
因为连接不是瞬时动作,而是一个持续运行的任务。
|
||||||
|
|
||||||
|
如果你只做:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
但不保存返回的 `JoinHandle`,那你后面其实不知道该停哪个任务。
|
||||||
|
|
||||||
|
所以当前项目里 `SerialConnectionState` 的职责是:
|
||||||
|
|
||||||
|
- 保存当前连接的端口
|
||||||
|
- 保存取消信号 `CancellationToken`
|
||||||
|
- 保存后台任务句柄 `JoinHandle`
|
||||||
|
|
||||||
|
断开时:
|
||||||
|
|
||||||
|
1. 从 state 里把当前 session 取出来
|
||||||
|
2. `cancel.cancel()`
|
||||||
|
3. 等任务退出
|
||||||
|
4. 清空 session
|
||||||
|
|
||||||
|
这就是“连接生命周期管理”。
|
||||||
|
|
||||||
|
## 5. 为什么我这里用 CancellationToken,而不是只 abort
|
||||||
|
|
||||||
|
`abort()` 可以硬停,但更像“直接掐掉线程”。
|
||||||
|
|
||||||
|
`CancellationToken` 更适合做长期任务,因为它允许你在循环里优雅退出:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel.cancelled() => break,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样后面如果你需要:
|
||||||
|
|
||||||
|
- 退出前写日志
|
||||||
|
- 退出前关闭资源
|
||||||
|
- 退出前发送状态事件
|
||||||
|
|
||||||
|
都更自然。
|
||||||
|
|
||||||
|
当前代码里依然保留了 `JoinHandle`,因为它可以让你等待任务真正结束。
|
||||||
|
|
||||||
|
## 6. 这个 demo 里 model 层在做什么
|
||||||
|
|
||||||
|
`src-tauri/src/serial_core/model.rs` 里现在做了两件事:
|
||||||
|
|
||||||
|
1. 定义前端消费的数据结构
|
||||||
|
2. 提供 `HudChartState`
|
||||||
|
|
||||||
|
`HudChartState` 的作用是把“一帧 frame”逐步积累成“可滚动折线图数据”。
|
||||||
|
|
||||||
|
你可以把它理解成一个很轻量的 view-model:
|
||||||
|
|
||||||
|
- 输入:`TestFrame`
|
||||||
|
- 内部:维护每条折线的点数组
|
||||||
|
- 输出:`HudPacket`
|
||||||
|
|
||||||
|
好处是前端不用知道:
|
||||||
|
|
||||||
|
- CRC
|
||||||
|
- 包头包尾
|
||||||
|
- payload 字节意义
|
||||||
|
- 缓冲和滑动窗口怎么维护
|
||||||
|
|
||||||
|
前端只知道:我收到了一份新的图表数据。
|
||||||
|
|
||||||
|
## 7. 前端这里做了什么
|
||||||
|
|
||||||
|
前端现在分成两层:
|
||||||
|
|
||||||
|
### 控制层
|
||||||
|
|
||||||
|
通过 `invoke` 调 Rust command:
|
||||||
|
|
||||||
|
- `serial_enum`
|
||||||
|
- `serial_connect`
|
||||||
|
- `serial_disconnect`
|
||||||
|
|
||||||
|
### 数据层
|
||||||
|
|
||||||
|
通过 `listen("hud_stream")` 收 Rust 推送的数据。
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- 控制动作走 command
|
||||||
|
- 连续数据走 event
|
||||||
|
|
||||||
|
这就是后面最好复用的模式。
|
||||||
|
|
||||||
|
## 8. 为什么页面里还保留了 mock feed
|
||||||
|
|
||||||
|
因为浏览器直接预览 UI 的时候,没有 Tauri runtime,也没有串口。
|
||||||
|
|
||||||
|
所以当前页面里做了两条分支:
|
||||||
|
|
||||||
|
- 在 Tauri 环境里:监听真实 `hud_stream`
|
||||||
|
- 在普通浏览器里:启动本地 mock feed
|
||||||
|
|
||||||
|
这样你开发样式时不用每次都连设备,效率会高很多。
|
||||||
|
|
||||||
|
## 9. 后面照着扩展时怎么套
|
||||||
|
|
||||||
|
如果你接下来做 TCP、日志流、设备状态流,基本可以照这个模板:
|
||||||
|
|
||||||
|
### TCP telemetry
|
||||||
|
|
||||||
|
- `tcp_connect` command
|
||||||
|
- `tcp_disconnect` command
|
||||||
|
- `tcp_stream` event
|
||||||
|
|
||||||
|
### 设备状态
|
||||||
|
|
||||||
|
- `device_get_status` command
|
||||||
|
- `device_status` event
|
||||||
|
|
||||||
|
### 日志输出
|
||||||
|
|
||||||
|
- `log_stream` event
|
||||||
|
|
||||||
|
只要记住一句话:
|
||||||
|
|
||||||
|
“一次性动作走 command,持续推送走 event。”
|
||||||
|
|
||||||
|
## 10. 一个最小可复用模板
|
||||||
|
|
||||||
|
### Rust command
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn start_task(app: AppHandle, state: State<'_, TaskState>) -> Result<(), TaskError> {
|
||||||
|
let cancel = CancellationToken::new();
|
||||||
|
let task_cancel = cancel.clone();
|
||||||
|
let task_app = app.clone();
|
||||||
|
|
||||||
|
let task = tauri::async_runtime::spawn(async move {
|
||||||
|
let _ = run_task(task_app, task_cancel).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
*state.task.lock().unwrap() = Some(TaskSession { cancel, task });
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rust 后台任务
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn run_task(app: AppHandle, cancel: CancellationToken) -> anyhow::Result<()> {
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel.cancelled() => break,
|
||||||
|
data = next_data() => {
|
||||||
|
let payload = build_payload(data?);
|
||||||
|
app.emit("some_stream", payload)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端监听
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const unlisten = await listen<Payload>("some_stream", (event) => {
|
||||||
|
applyPayload(event.payload);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. 这套方式最容易踩的坑
|
||||||
|
|
||||||
|
### 1. 在同步 command 里调用 async-only API
|
||||||
|
|
||||||
|
像 `open_native_async()` 这种需要 runtime 的 API,command 本身就应该写成 `async fn`。
|
||||||
|
|
||||||
|
### 2. 把长期循环直接写在 command 里
|
||||||
|
|
||||||
|
这样前端 `invoke` 会一直挂着,按钮像卡死一样。
|
||||||
|
|
||||||
|
### 3. 解码时传整个 buffer,而不是 `&buffer[..n]`
|
||||||
|
|
||||||
|
这会把未读取区域的垃圾值一并喂给解码器。
|
||||||
|
|
||||||
|
### 4. 没有保存任务句柄
|
||||||
|
|
||||||
|
这样断开时你并不知道要停哪个任务。
|
||||||
|
|
||||||
|
### 5. 原始协议直接发前端
|
||||||
|
|
||||||
|
短期快,长期维护会很痛苦。最好让 Rust 先转换成业务数据。
|
||||||
|
|
||||||
|
## 12. 你后面可以继续优化的方向
|
||||||
|
|
||||||
|
- 增加 `status_stream`,当串口异常退出时主动通知前端
|
||||||
|
- 把 `SerialConnectionState` 扩成 `HashMap<String, SerialSession>`,支持多串口
|
||||||
|
- 把 `HudChartState` 抽成更明确的 telemetry service
|
||||||
|
- 给不同协议定义不同 packet,而不是全部复用一个事件名
|
||||||
|
|
||||||
|
## 13. 现在这版如何支持动态 panel
|
||||||
|
|
||||||
|
这次我已经把 demo 从“固定四个 panel”改成了“按数据源 id 动态创建 panel”。
|
||||||
|
|
||||||
|
当前后端的行为是:
|
||||||
|
|
||||||
|
- 串口保持连接
|
||||||
|
- 收到某个 source id 的数据时,如果这个 id 还没有 panel,就新建
|
||||||
|
- 后面这个 id 持续有数据,就持续往它自己的折线里追加点
|
||||||
|
- 如果某个 id 超过一段时间没有新数据,就自动移除
|
||||||
|
- 移除后前端会走离场动画
|
||||||
|
|
||||||
|
### 当前 demo 约定的数据格式
|
||||||
|
|
||||||
|
现在 `model.rs` 里的 demo 约定是一帧 payload 可以写成多个 4 字节分组:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[source_id, value1, value2, value3, source_id, value1, value2, value3, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[0x41, 180, 120, 60, 0x42, 170, 80, 30]
|
||||||
|
```
|
||||||
|
|
||||||
|
这里会被解释成:
|
||||||
|
|
||||||
|
- `0x41` -> `A`
|
||||||
|
- `0x42` -> `B`
|
||||||
|
|
||||||
|
也就是说这一帧会同时更新两个 panel:
|
||||||
|
|
||||||
|
- A panel
|
||||||
|
- B panel
|
||||||
|
|
||||||
|
如果后续几帧只有 B 和 C,没有 A,那 A 超时之后就会自动被移除。
|
||||||
|
|
||||||
|
### source id 是怎么来的
|
||||||
|
|
||||||
|
当前 demo 里:
|
||||||
|
|
||||||
|
- 如果字节是字母或数字,比如 `A`、`B`、`3`
|
||||||
|
- 就直接把它当 source id
|
||||||
|
|
||||||
|
否则会变成:
|
||||||
|
|
||||||
|
- `CH01`
|
||||||
|
- `CH0A`
|
||||||
|
- `CHFF`
|
||||||
|
|
||||||
|
这种形式。
|
||||||
|
|
||||||
|
### 超时移除在哪里控制
|
||||||
|
|
||||||
|
在 `src-tauri/src/serial_core/model.rs` 里有一个常量:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
const PANEL_STALE_AFTER: Duration = Duration::from_millis(2400);
|
||||||
|
```
|
||||||
|
|
||||||
|
意思是某个 panel 2.4 秒没收到新数据,就认为它已经“消失”,会从当前 packet 里移除。
|
||||||
|
|
||||||
|
### 如果你的真实协议不是这个格式怎么办
|
||||||
|
|
||||||
|
那就只改 `expand_frame_updates(frame)` 这一层。
|
||||||
|
|
||||||
|
你真正要做的是把自己的协议,翻译成:
|
||||||
|
|
||||||
|
- source id
|
||||||
|
- 这个 source 对应的 3 个数值
|
||||||
|
|
||||||
|
例如如果你的协议里一帧只表示一个源:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn expand_frame_updates(frame: &TestFrame) -> Vec<HudPanelUpdate> {
|
||||||
|
vec![HudPanelUpdate {
|
||||||
|
source_id: "A".to_string(),
|
||||||
|
values: [12.3, 45.6, 78.9],
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果一帧里同时包含多个源:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn expand_frame_updates(frame: &TestFrame) -> Vec<HudPanelUpdate> {
|
||||||
|
vec![
|
||||||
|
HudPanelUpdate {
|
||||||
|
source_id: "A".to_string(),
|
||||||
|
values: [12.3, 45.6, 78.9],
|
||||||
|
},
|
||||||
|
HudPanelUpdate {
|
||||||
|
source_id: "C".to_string(),
|
||||||
|
values: [33.1, 29.8, 81.4],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
后面的动态创建、更新、超时移除,`HudChartState` 都会自动帮你处理掉。
|
||||||
|
|
||||||
|
## 14. TODO 提示词
|
||||||
|
|
||||||
|
如果你后面把 `handle` / 协议解析逻辑写完了,想让我把 demo 逻辑替换成真实业务逻辑,直接把下面这段话发给我就行:
|
||||||
|
|
||||||
|
```md
|
||||||
|
我已经把串口 frame 的业务含义梳理清楚了,请你把当前 tauri-demo 里的 demo 动态 panel 逻辑改成真实协议版本。
|
||||||
|
|
||||||
|
已知信息:
|
||||||
|
- frame 类型:`TestFrame`
|
||||||
|
- 数据来源文件:`src-tauri/src/serial_core/...`
|
||||||
|
- 当前 demo 映射入口:`src-tauri/src/serial_core/model.rs` 里的 `expand_frame_updates`
|
||||||
|
- 如果需要,也可以调整 `handler.on_frame(...)` 的职责
|
||||||
|
|
||||||
|
真实协议说明:
|
||||||
|
- source_id 怎么取:
|
||||||
|
- 一帧里有几路数据:
|
||||||
|
- 每一路数据对应哪些字段:
|
||||||
|
- 数值是 u8 / u16 / i16 / float:
|
||||||
|
- 字节序是大端还是小端:
|
||||||
|
- 缩放系数是多少:
|
||||||
|
- 哪些情况下表示“该路数据消失”:
|
||||||
|
- panel 标题 / 图例文案要如何显示:
|
||||||
|
|
||||||
|
请你帮我做这些事:
|
||||||
|
1. 去掉当前 demo 的 4 字节分组映射逻辑
|
||||||
|
2. 改成按真实协议生成动态 panel
|
||||||
|
3. 保留“有数据就创建,超时没数据就移除”的行为
|
||||||
|
4. 如有必要,重构 `model.rs` / `handler` / `serial.rs` 的职责边界
|
||||||
|
5. 更新 `tauri-event.md`,说明新的真实数据流
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你到时候还没完全确定协议,也可以先用这个简化版提示词:
|
||||||
|
|
||||||
|
```md
|
||||||
|
我已经知道 payload 里哪些字节对应哪几个通道了,请你帮我把 `src-tauri/src/serial_core/model.rs` 里的 demo 映射改成真实映射,并告诉我还缺哪些协议信息。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 到时候我最需要你提供的最小信息
|
||||||
|
|
||||||
|
- source id 怎么区分不同数据源
|
||||||
|
- 每个 source 对应几条曲线
|
||||||
|
- payload 各字段的字节位置
|
||||||
|
- 数值类型和缩放系数
|
||||||
|
- 多久没数据算“消失”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
如果你后面要继续照这个模式扩展,最推荐的顺序是:
|
||||||
|
|
||||||
|
1. 先定义前端真正需要的 payload 结构
|
||||||
|
2. 再写 Rust 的 model/service 层
|
||||||
|
3. 最后只让 command 负责启动和停止
|
||||||
|
|
||||||
|
这样结构通常都会比较干净。
|
||||||
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
||||||
32
vite.config.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
|
|
||||||
|
// @ts-expect-error process is a nodejs global
|
||||||
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig(async () => ({
|
||||||
|
plugins: [sveltekit()],
|
||||||
|
|
||||||
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
|
//
|
||||||
|
// 1. prevent Vite from obscuring rust errors
|
||||||
|
clearScreen: false,
|
||||||
|
// 2. tauri expects a fixed port, fail if that port is not available
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
host: host || false,
|
||||||
|
hmr: host
|
||||||
|
? {
|
||||||
|
protocol: "ws",
|
||||||
|
host,
|
||||||
|
port: 1421,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
watch: {
|
||||||
|
// 3. tell Vite to ignore watching `src-tauri`
|
||||||
|
ignored: ["**/src-tauri/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||