siplot 0.1.0

silx-style scientific plotting for egui, rendered with wgpu
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# siplot — 설계 문서 (Design Doc v0)

egui + wgpu 위에 silx 스타일의 과학 시각화 라이브러리를 만든다. silx가
`BackendBase`(추상) ↔ `BackendOpenGL`/`BackendPygfx`(구현)로 렌더러를 갈아끼우듯,
이 라이브러리는 **고수준 Plot API ↔ `Backend` trait ↔ wgpu 렌더러**로 분리하고
wgpu 렌더러를 `egui_wgpu` paint callback 안에 끼워 넣는다.

이식의 1:1 레퍼런스는 트리 안의 두 파일이다:

- `silx/src/silx/gui/plot/backends/BackendBase.py` — 미러링할 추상 인터페이스
- `silx/src/silx/gui/plot/backends/BackendPygfx.py` — pygfx(=wgpu) 구현, 줄 단위 포팅 대상

> 본 문서의 결정: **1차 수직 슬라이스는 image+curve 공통 백엔드 동시**. 즉
> 공통 좌표계/`Backend` trait/wgpu 렌더 패스를 먼저 세우고 그 위에 `addImage`> `addCurve`를 같이 올린다.

---

## 0. 용어와 역할 매핑

| silx (Python/Qt/pygfx) | siplot (Rust/egui/wgpu) |
|---|---|
| `PlotWidget` / `Plot1D` / `Plot2D` (고수준) | `Plot` + `PlotWidget` (egui widget) |
| `BackendBase` (추상 렌더 프리미티브) | `trait Backend` |
| `BackendPygfx` (pygfx/wgpu 구현) | `WgpuBackend` (`CallbackTrait` 구현) |
| Qt `QRenderWidget` 호스트 | egui `Ui` + eframe `RenderState` |
| pygfx 씬 그래프 (retained) | `callback_resources`에 사는 retained GPU state |
| pygfx `OrthographicCamera.show_rect` | uniform MVP 행렬 (ortho) |
| `silx.gui.colors.Colormap` | `Colormap` (256색 1D LUT + clim + norm) |
| GraphicFeature dirty-range 업로드 | `DirtyRange` 부분 `queue.write_buffer/texture` |

핵심 비대칭 하나: egui는 **immediate-mode**다. silx/fastplotlib는 retained 씬
그래프를 들고 있지만, 우리는 **데이터(GPU 버퍼·텍스처·파이프라인)만 프레임 간
유지**(`Renderer.callback_resources`, TypeMap)하고, egui UI 코드는 매 프레임
rect를 새로 할당해 paint callback을 다시 등록한다. callback 자체는
"이 plot을 그려라(+현재 transform)"만 담은 가벼운 값이고, 무거운 GPU state는
callback_resources에서 id로 조회한다.

---

## 1. 레이어 구조

```
┌─────────────────────────────────────────────────────────────┐
│ widget   egui 위젯: PlotWidget(ui).show(&mut plot)           │
│          - chrome 그리기(축/눈금/그리드/제목/컬러바/ROI 핸들) │
│          - 상호작용(pan/zoom/box-zoom/hover/pick) → 상태 갱신 │
│          - 데이터 영역 rect에 wgpu paint callback 등록        │
├─────────────────────────────────────────────────────────────┤
│ core     Plot 모델 + Backend trait + 공용 타입               │
│          - Plot: 아이템 목록, 축 상태(limits/log/inverted),  │
│            margins, title/labels, aspect-ratio               │
│          - trait Backend: addCurve/addImage/.../dataToPixel   │
│          - Colormap, Transform, ItemHandle, 좌표계            │
├─────────────────────────────────────────────────────────────┤
│ render   WgpuBackend: CallbackTrait 구현                     │
│          - GPU 리소스(파이프라인/버퍼/텍스처/LUT) 소유·유지   │
│          - prepare: transform uniform 갱신 + dirty 업로드     │
│          - paint:    set_viewport + 드로우 콜                 │
└─────────────────────────────────────────────────────────────┘
```

처음엔 단일 크레이트 내부 모듈(`core`/`render`/`widget`)로 두고, API가 안정되면
`siplot-core` / `siplot-wgpu` / `siplot` 로 분리한다. `Backend`를 trait로
두는 이유는 silx와 동일: 고수준 API를 고정한 채 렌더러(테스트용 CPU mock, 다른
백엔드)를 갈아끼우기 위함이다.

---

## 2. `Backend` trait — `BackendBase` 미러

`BackendBase.py`의 공개 메서드를 Rust로 옮긴다. 1차 슬라이스에서 **반드시** 구현할
것에 ✅, 이후 마일스톤은 ◻︎.

```rust
/// 백엔드에 등록된 아이템 핸들. silx의 item handle과 동일 역할.
pub type ItemHandle = u64;

pub trait Backend {
    // ── 아이템 생성 ──────────────────────────────────────────
    ✅ fn add_curve(&mut self, curve: &CurveSpec) -> ItemHandle;   // BackendBase:90
    ✅ fn add_image(&mut self, image: &ImageSpec) -> ItemHandle;   // BackendBase:150
    ◻︎ fn add_triangles(&mut self, tris: &TriangleSpec) -> ItemHandle; // :168
    ◻︎ fn add_shape(&mut self, shape: &ShapeSpec) -> ItemHandle;    // :181
    ◻︎ fn add_marker(&mut self, marker: &MarkerSpec) -> ItemHandle; // :211
    ✅ fn remove(&mut self, item: ItemHandle);                     // :271

    // ── 축 / limits ──────────────────────────────────────────
    ✅ fn set_limits(&mut self, xmin: f64, xmax: f64,
                     ymin: f64, ymax: f64,
                     y2: Option<(f64, f64)>);                      // :410
    ✅ fn x_limits(&self) -> (f64, f64);                           // :425
    ✅ fn y_limits(&self, axis: YAxis) -> (f64, f64);              // :440
    ◻︎ fn set_x_log(&mut self, on: bool);                          // :491
    ◻︎ fn set_y_log(&mut self, on: bool);                          // :498
    ◻︎ fn set_x_inverted(&mut self, on: bool);                     // :505
    ◻︎ fn set_y_inverted(&mut self, on: bool);                     // :516
    ◻︎ fn set_keep_data_aspect_ratio(&mut self, on: bool);         // :535

    // ── 좌표 변환 (chrome ↔ 데이터 정합의 핵심) ──────────────
    ✅ fn data_to_pixel(&self, x: f64, y: f64, axis: YAxis) -> Option<Pos2>; // :553
    ✅ fn pixel_to_data(&self, p: Pos2, axis: YAxis) -> Option<(f64, f64)>;  // :569
    ✅ fn plot_bounds_in_pixels(&self) -> Rect;                    // :583
    ✅ fn set_axes_margins(&mut self, l: f32, t: f32, r: f32, b: f32); // :590

    // ── 라벨 / 색 ────────────────────────────────────────────
    ◻︎ fn set_title(&mut self, s: &str);                           // :386
    ◻︎ fn set_x_label(&mut self, s: &str);                         // :393
    ◻︎ fn set_y_label(&mut self, s: &str, axis: YAxis);            // :400
    ◻︎ fn set_foreground_colors(&mut self, fg: Color32, grid: Color32); // :602
    ◻︎ fn set_background_colors(&mut self, bg: Color32, data_bg: Color32); // :610

    // ── 피킹 ────────────────────────────────────────────────
    ◻︎ fn pick_item(&self, p: Pos2, item: ItemHandle) -> Option<PickResult>; // :335
    ◻︎ fn items_back_to_front(&self) -> Vec<ItemHandle>;           // :312

    // ── 라이프사이클 ─────────────────────────────────────────
    ✅ fn replot(&mut self);          // dirty 플래그 → 다음 프레임 재업로드 // :367
    ◻︎ fn save_graph(&self, path: &Path, fmt: ImageFormat, dpi: u32); // :372
}
```

`add_curve`/`add_image`는 1차 대상이므로 `BackendBase`의 전체 파라미터를 보존한다.

```rust
pub struct CurveSpec<'a> {            // BackendBase.addCurve (90-148)
    pub x: &'a [f64],
    pub y: &'a [f64],
    pub color: CurveColor,            // 단색 or per-vertex RGBA (N,4)
    pub gap_color: Option<Color32>,   // dashed gap 색
    pub symbol: Option<Symbol>,       // o . , + x d s
    pub line_width: f32,
    pub line_style: LineStyle,        // '-' '--' '-.' ':' none | custom dash
    pub y_axis: YAxis,                // Left | Right
    pub x_error: Option<ErrorBars>,
    pub y_error: Option<ErrorBars>,
    pub fill: bool,                   // baseline까지 채움
    pub alpha: f32,
    pub symbol_size: f32,
    pub baseline: Baseline,           // scalar | per-point
}

pub struct ImageSpec<'a> {            // BackendBase.addImage (150-166)
    pub data: ImageData<'a>,          // Scalar(&[f32], w, h) | Rgba(&[u8], w, h)
    pub origin: (f64, f64),           // 데이터 좌표 (ox, oy)
    pub scale: (f64, f64),            // 픽셀당 데이터 단위 (sx, sy)
    pub colormap: Option<Colormap>,   // scalar일 때만 적용 (RGBA는 무시)
    pub alpha: f32,
}
```

---

## 3. wgpu 렌더링 모델 — `egui_wgpu` 안에 끼우기

(근거: `egui/crates/egui-wgpu/src/renderer.rs:87` `CallbackTrait`,
`egui/crates/egui_demo_app/src/apps/custom3d_wgpu.rs` 패턴.)

### 3.1 GPU 리소스의 거처

모든 영속 GPU state는 eframe가 만든 `RenderState.renderer.write().callback_resources`
(타입맵)에 단 하나의 타입으로 넣는다. 여러 plot을 지원하기 위해 내부에
`HashMap<PlotId, PlotGpuState>`를 둔다.

```rust
struct WgpuResources {
    // 파이프라인은 plot 무관하게 공유
    image_pipeline: wgpu::RenderPipeline,
    line_pipeline:  wgpu::RenderPipeline,
    point_pipeline: wgpu::RenderPipeline,
    sampler: wgpu::Sampler,
    plots: HashMap<PlotId, PlotGpuState>,   // plot별 데이터
}

struct PlotGpuState {
    transform: wgpu::Buffer,            // MVP uniform (ortho)
    images: HashMap<ItemHandle, GpuImage>,
    curves: HashMap<ItemHandle, GpuCurve>,
}

struct GpuImage {
    tiles: Vec<GpuImageTile>,           // max-texture-dim 초과 시 분할 (§6)
    lut:   wgpu::Texture,               // 256x1 RGBA8 컬러맵 LUT
    params: wgpu::Buffer,               // clim(vmin,vmax), norm mode, gamma, alpha
    bind_group: wgpu::BindGroup,
    dirty: DirtySet,                    // 데이터/LUT/params 중 무엇이 변했나
}
```

`WgpuResources`는 앱 시작 시(`CreationContext.wgpu_render_state`) 한 번 생성해
`callback_resources.insert()` 한다. 파이프라인 컴파일도 이때 1회.

### 3.2 callback 값과 3단 라이프사이클

UI 코드가 매 프레임 등록하는 callback은 가볍다:

```rust
struct PlotCallback {
    plot_id: PlotId,
    transform: Mat4,    // 이번 프레임의 데이터→NDC ortho 행렬 (현재 pan/zoom 반영)
    uploads: Vec<PendingUpload>,  // 이번 프레임에 반영할 dirty 데이터(있으면)
}

impl CallbackTrait for PlotCallback {
    // renderer.rs:88 — Device/Queue/Encoder 접근 가능. 업로드는 여기서.
    fn prepare(&self, device, queue, _desc, _enc, res: &mut CallbackResources)
        -> Vec<CommandBuffer>
    {
        let r: &mut WgpuResources = res.get_mut().unwrap();
        let plot = r.plots.get_mut(&self.plot_id).unwrap();
        queue.write_buffer(&plot.transform, 0, bytemuck::bytes_of(&self.transform));
        for up in &self.uploads { up.apply(device, queue, plot); } // dirty-range만
        Vec::new()
    }

    // renderer.rs:114 — 활성 RenderPass. 드로우만. set_viewport는 egui가 이미 해줌.
    fn paint(&self, info: PaintCallbackInfo, rp: &mut wgpu::RenderPass<'static>,
             res: &CallbackResources)
    {
        let r: &WgpuResources = res.get().unwrap();
        let plot = &r.plots[&self.plot_id];
        // back-to-front: 이미지 먼저, 곡선 위에
        for img in plot.images.values()  { img.draw(rp, &r.image_pipeline); }
        for cur in plot.curves.values()  { cur.draw(rp, &r.line_pipeline, &r.point_pipeline); }
    }
}
```

뷰포트 클리핑은 egui 쪽이 callback의 `rect`로부터 `set_viewport`를 호출해 준다
(`renderer.rs:567` 근방). 따라서 우리 셰이더는 **데이터 영역 rect = NDC 전체**로
간주하고 ortho 행렬만 맞추면 된다. 단, `PaintCallbackInfo.pixels_per_point`로
물리픽셀 변환이 필요한 계산(라인 두께 px 등)은 우리가 직접 처리한다
(`epaint/src/viewport.rs:4` `ViewportInPixels`).

### 3.3 immediate-mode와 retained의 결합 (dirty 흐름)

fastplotlib의 GraphicFeature dirty-range 패턴을 그대로 가져온다
(`fastplotlib/graphics/features/_base.py:135` BufferManager, `_update_range`).

1. 고수준 API (`plot.add_image(...)`, `image.set_data(...)`)가 `core` 모델을
   수정하고 해당 아이템에 **dirty range**를 기록한다 (전체 or 부분 슬라이스).
2. `PlotWidget::show()`가 프레임마다:
   - 상호작용 처리 → 축 limits/pan/zoom 갱신 → ortho `transform` 재계산
   - dirty 아이템을 모아 `PendingUpload`로 패키징
   - `Callback::new_paint_callback(data_rect, PlotCallback{..})` 등록
     (`renderer.rs:31`)
   - dirty 플래그 클리어
3. `prepare`에서 변경 구간만 `write_buffer`/`write_texture`. 변경 없으면 업로드 0,
   유지된 GPU 버퍼로 바로 `paint`.

즉 "씬 그래프는 매 프레임 새로, GPU 데이터는 유지, 바뀐 곳만 업로드". 라이브
파형/카메라 스트림에서 핵심 성능 특성이다.

---

## 4. 좌표계 — chrome와 셰이더의 단일 진실원

silx의 변환 사슬(`_PlotFrameCore.dataToPixel`, 1121-1172)을 그대로 따른다:
`data → (log/skew) → [bounds 정규화] → (margins/inverted) → pixel`.

데이터→NDC 변환은 **한 곳**(`Transform`)에서 만들고 두 소비자에게 동일하게 먹인다:

- **셰이더**: `Transform`에서 ortho MVP `Mat4`를 만들어 uniform으로. pygfx
  `camera.show_rect(xmin,xmax,ymin,ymax)`와 동치 (`BackendPygfx:1679`).
- **chrome(egui)**: 동일 limits/margins로 `emath::RectTransform::from_to(
  data_rect, pixel_rect)`를 만들어 축·눈금·마커·ROI 핸들을 egui `Painter`로 그린다
  (`egui/crates/emath/src/rect_transform.rs`). `data_to_pixel`/`pixel_to_data`  `RectTransform`(+log/inverted 후처리)으로 구현.

> MUST: 두 변환은 같은 (limits, margins, inverted, log, pixels_per_point)에서
> 파생되어야 한다. 두 곳에서 따로 계산하면 이미지와 축이 1px씩 어긋난다 — silx도
> 같은 함정을 `_PlotFrameCore` 단일 변환으로 막았다. 단일 owner = `Transform`.

축 종류는 `YAxis::{Left, Right}` (silx의 y/y2)와 X축. log/inverted/skew는
`Transform` 단계의 플래그로 처리하고 1차 슬라이스에서는 linear/non-inverted만 구현,
이후 마일스톤에서 켠다.

---

## 5. 컬러맵 모델

(근거: `BackendPygfx.py:365-480`, `fastplotlib/utils/functions.py:141-208`.)

```rust
pub struct Colormap {
    pub lut: [[u8; 4]; 256],    // 256색 RGBA8 → 256x1 GPU 텍스처
    pub norm: Normalization,
    pub vmin: Option<f64>,      // None = 데이터에서 autoscale(GPU min/max)
    pub vmax: Option<f64>,
    pub gamma: f32,
    pub nan_color: [u8; 4],
}

pub enum Normalization { Linear, Log, Sqrt, Gamma, Arcsinh }
```

- **셰이더 적용**: scalar 텍스처(f32 2D) + 1D LUT 텍스처. 셰이더가
  `t = clamp((value - vmin)/(vmax - vmin), 0, 1)``LUT[t]`. clim/gamma/norm은
  params uniform으로 전달. (pygfx `ImageBasicMaterial(map=lut, clim=...)` 동치,
  `BackendPygfx:1194`.)
- **정규화**: linear는 셰이더에서 바로. log/sqrt/arcsinh는 셰이더에서
  `value`를 변환하거나(권장) silx처럼 업로드 전 CPU 변환 후 clim 조정
  (`BackendPygfx:411-428`). gamma는 `1/gamma` 지수.
- **NaN sentinel**: silx 방식(`:435-480`) — NaN을 vmin 아래 sentinel로 치환,
  `LUT[0] = nan_color`, 원본 LUT는 1..255로 압축, clim 확장. 셰이더 `clamp`  sentinel이 index 0에 매핑. 1차 구현에서 채택.
- **컬러바**: 데이터 이미지와 **같은 LUT·clim·norm**으로 chrome에 별도 작은
  사각형 + 눈금. 이미지와 정확히 일치시키는 게 silx 정합성의 핵심.
- **카탈로그**: fastplotlib는 외부 `cmap` 라이브러리. Rust에서는 `colorous`
  또는 자체 테이블(viridis/plasma/inferno/magma/gray/jet 등 핵심 셋)로 시작.

---

## 6. 이미지 경로 (Plot2D)

(근거: `fastplotlib/graphics/features/_image.py:16-164` TextureArray 타일링,
`BackendPygfx` `_PygfxImageItem` 1023-1219.)

- scalar `f32``R32Float` 텍스처 1장. clim/LUT는 §5.
- **타일링**: `device.limits().max_texture_dimension_2d`(wgpu 기본 8192) 초과 시
  W/H를 청크 격자로 분할, 타일마다 텍스처 + 월드 오프셋. **LUT/params는 모든
  타일이 공유**(vmin/vmax/cmap 변경이 원자적으로 반영) — fastplotlib의 shared
  material과 동일 이유.
- 부분 갱신: `set_data` 시 변경 영역만 `write_texture`(dirty-range).
- `origin`/`scale`로 데이터 좌표에 배치 → quad 정점은 `Transform`이 NDC로.
- RGB(A) 이미지: LUT 없이 텍스처 직결.

---

## 7. 곡선 경로 (Plot1D)

(근거: `fastplotlib/graphics/line.py:25`, `scatter.py:25`,
`features/_positions.py:220`.)

- positions: `vec2<f32>` 버퍼(영속). per-point 갱신은 dirty-range 업로드.
- color: `UniformColor`(단색 uniform) vs `VertexColors`(per-vertex RGBA 버퍼) 분기.
  per-point colormap은 보조값을 LUT로 매핑(fastplotlib `VertexCmap`).
- line_width: 1차는 1px line-list, 두꺼운 선은 이후 quad-expansion 셰이더로.
- symbol(scatter): point_pipeline로 instanced quad + symbol SDF.
- **decimation**: fastplotlib엔 없다(전수 렌더). silx-급 대용량 파형은 별도
  마일스톤에서 x-bin별 min/max decimation을 GPU/CPU로 추가(요구 시).

---

## 8. 축·프레임·상호작용 (chrome, egui-side)

silx의 frame은 screen-space로 그려진다(`_PlotFrameCore`, `BackendPygfx`
`_updateFrame`/`_updateMarkers` 1644-1970). 우리는 이를 **egui `Painter`로 직접**
그린다(wgpu 아님): 축선/눈금/그리드/제목/라벨/범례/컬러바/마커/ROI 핸들.

- 눈금 위치 계산: nice-number 알고리즘(자체). log축은 데케이드 눈금.
- 상호작용: `ui.allocate_rect(data_rect, Sense::drag())`  - drag: pan (limits 평행이동)
  - scroll: zoom (커서 데이터좌표 고정 확대)
  - 박스 줌, 더블클릭 리셋, hover crosshair
- 피킹: 픽셀 ±3px 허용(silx `pickItem`, 2388-2553). 커서 픽셀을 `pixel_to_data`  데이터 박스로 바꿔 곡선/이미지 인덱스 산출.
- ROI/selector: fastplotlib처럼 "selector도 아이템", 선택 bounds는 상태이고
  egui 핸들 드래그로 갱신, 변경 시 콜백/이벤트 emit (이후 마일스톤).

---

## 9. 크레이트 구조 (초기: 단일 크레이트, 모듈 분리)

```
siplot/
  Cargo.toml            # egui/eframe/egui-wgpu = 0.34, wgpu = 29, bytemuck
  src/
    lib.rs
    core/
      plot.rs           # Plot 모델, 축 상태, 아이템 목록, dirty 관리
      backend.rs        # trait Backend + 스펙 타입(CurveSpec/ImageSpec/...)
      transform.rs      # Transform: limits/margins → Mat4 + RectTransform
      colormap.rs       # Colormap, Normalization, 내장 LUT 테이블
      items.rs          # ItemHandle, Symbol, LineStyle, ErrorBars ...
    render/
      backend_wgpu.rs   # WgpuBackend: CallbackTrait + WgpuResources
      pipelines.rs      # image/line/point 파이프라인 생성
      gpu_image.rs      # GpuImage + 타일링 + LUT/params
      gpu_curve.rs      # GpuCurve
      shaders/*.wgsl
    widget/
      plot_widget.rs    # PlotWidget(ui).show(&mut plot): chrome+interaction+callback
      chrome.rs         # 축/눈금/그리드/컬러바 egui 드로잉
      interaction.rs    # pan/zoom/pick
  examples/
    image.rs            # 1차 슬라이스 데모: 이미지 + 컬러맵 + 컬러바
    curve.rs            # 1차 슬라이스 데모: 곡선 + 축
    image_and_curve.rs  # 둘 다 한 plot에
  doc/design.md         # 이 문서
```

eframe 앱 부트스트랩 시 `NativeOptions`에서 wgpu 백엔드를 선택하고
(`renderer: Renderer::Wgpu`), `CreationContext.wgpu_render_state`로 `WgpuResources`를
1회 초기화한다.

---

## 10. 버전 고정과 결합 (유지보수 세금)

- 워크스페이스 핀: **egui 0.34.3 / eframe 0.34.3 / egui-wgpu 0.34.3 / wgpu 29.0.1**,
  edition 2024, MSRV 1.92. 이 넷은 **반드시 한 버전축**으로 움직인다 — egui
  마이너 올릴 때 egui-wgpu·wgpu 동반 상향 필수. `Cargo.toml`에 정확 버전 핀.
- wgpu 29 → 셰이더는 WGSL, `RenderPass<'static>`(renderer.rs:114) 시그니처 준수.
- `bytemuck`으로 uniform POD 직렬화.

---

## 11. 1차 수직 슬라이스 — "image + curve 동시" 마일스톤

목표(verifiable): `cargo run --example image_and_curve`가 (a) 컬러맵+컬러바가
달린 2D scalar 이미지와 (b) 같은 plot 위 곡선을 동시에 띄우고, (c) pan/zoom 시
이미지·곡선·축이 1px 어긋남 없이 함께 움직인다.

순서:

1. **부트스트랩**: eframe + wgpu, 빈 `PlotWidget`이 데이터 rect에 단색 클리어
   콜백만 그림. `callback_resources``WgpuResources` 삽입 검증.
2. **Transform 단일화**: `core::transform` — limits/margins → ortho `Mat4` +
   `RectTransform`. `data_to_pixel`/`pixel_to_data` 단위테스트(왕복 항등).
3. **이미지 파이프라인**: `R32Float` 텍스처 + 256x1 LUT + clim uniform 셰이더.
   `add_image` → quad 드로우. viridis LUT 내장. (타일링은 단일 텍스처 한도
   내에서는 생략, 한도 초과 케이스만 후속.)
4. **컬러바 + 축 chrome**: egui `Painter`로 축/눈금/그리드/컬러바. 이미지와
   동일 LUT·clim 사용 검증.
5. **곡선 파이프라인**: positions 버퍼 + 단색 line-list. `add_curve` → 드로우.
6. **상호작용**: pan/zoom/box-zoom/reset. transform 갱신이 셰이더·chrome 동시 반영.
7. **dirty-range 업로드**: `set_data`로 이미지/곡선 일부만 갱신, `prepare`에서
   부분 `write_*` 검증(라이브 갱신 데모).

이후 마일스톤(2차 페이즈): log/inverted/aspect-ratio, 마커/shape,
피킹, ROI selector, 대용량 decimation, 이미지 타일링, save_graph, y2 축,
두꺼운 라인/심볼 SDF, 컬러맵 카탈로그 확장. → **§13에서 의존성 순서와
단계별 검증 기준으로 단계화한다.**

---

## 12. 리스크 / 열린 질문

- **chrome ↔ 셰이더 정합**: §4의 단일 Transform 규칙을 깨면 재발하는 버그
  계열. 왕복 변환 단위테스트로 봉인.
- **pixels_per_point / DPI**: 라인 두께·심볼 크기·±3px 피킹은 물리픽셀 기준.
  `ViewportInPixels`로 일관 처리.
- **log축 정규화 위치** (결정됨, §13 A3): 좌표를 **업로드 전 CPU에서 log10
  변환**한다(silx와 동일). `Transform`은 축당 `scale ∈ {Linear, Log10}`  들고, 데이터 값 `v`를 정규화 `t∈[0,1]`로 보내는 단일 함수에서 log를
  적용한다(곡선/chrome 모두 같은 경로). ortho `Mat4`는 항상 정규화 공간을
  선형 매핑하므로 affine으로 충분하다. **이미지+log 한계**: 텍셀이 데이터
  등간격이라 log 공간에서 왜곡된다. 1차에는 곡선·축만 log를 지원하고,
  이미지+log는 프래그먼트 셰이더 역매핑으로 후속 처리한다(§13 A3 한계).
- **egui_plot 의존?**: 미사용 결정. chrome을 자체 구현해 wgpu transform과 강결합.
  필요하면 외부 `egui_plot` repo를 참고 구현으로만 본다.
- **멀티-plot**: `callback_resources`는 전역 TypeMap이라 `HashMap<PlotId,_>`  분리. PlotId 발급/회수(위젯 소멸 시 GPU 리소스 정리) 정책 필요.

---

## 13. 2차 페이즈 — 백로그 단계 계획

§11 슬라이스 1(1~7단계) 완료 후의 백로그를 의존성 순서로 웨이브화한다.
규칙은 1차와 동일: 코드 주석 영어, 항목마다 fmt/clippy/nextest 통과 + 단계별
커밋, 가능한 한 순수 함수에 단위테스트. 각 항목에 "done = …" 검증 기준을 둔다.

### Wave A — 좌표계 일반화 (`core::transform`; 가장 많은 기능이 의존)

단일 진실원 규칙(§4)을 유지하기 위해, 축을 `Axis { min, max, scale, inverted }`
로 일반화한다(`scale ∈ {Linear, Log10}`). 데이터 값 `v`를 정규화 `t∈[0,1]`로
보내는 **단일 함수** `norm(axis, v)`와 그 역 `denorm`을 두고, `data_to_pixel` /
`pixel_to_data` / `ortho_matrix`가 전부 이 한 경로에서 파생되게 한다.

- **A1. 축 모델 일반화**: 위 `Axis`/`norm` 도입, 기존 linear 동작 보존.
  done = 기존 transform 단위테스트 전부 통과 + `norm`/`denorm` 왕복 항등 테스트.
- **A2. inverted 축 (X/Y)**: 정규화에서 `t → 1−t`. affine이라 ortho로도 표현됨.
  done = 뒤집힌 축에서 이미지·곡선·눈금이 1px 정합.
- **A3. log 축 (X/Y)**: `norm`이 log10 적용(`min>0` 전제), chrome은 데케이드
  눈금. 좌표는 CPU에서 변환(§12 결정). done = 로그 축 곡선 + 데케이드 눈금
  데모. **한계**: 이미지+log는 텍셀 왜곡 때문에 후속(프래그먼트 역매핑).
- **A4. aspect-ratio lock**: 데이터 단위를 정사각으로 유지하도록 한계를 확장
  보정(이미지 왜곡 방지). done = 정사각 픽셀에서 원이 원으로 보임.
- **A5. y2 축**: 두 번째 Y 한계 + 우측 눈금, 곡선이 `y`/`y2`에 바인딩.
  done = 좌/우 다른 스케일의 두 곡선이 각자 축에 정합.

### Wave B — 1D 비주얼 (`render::gpu_curve` + 새 점 파이프라인)

- **B1. 두꺼운 라인**: line-strip → quad-expansion(triangle-strip) 셰이더,
  픽셀 폭 uniform(물리픽셀 기준, §12 DPI). done = 폭 ≥1px 가변 라인.
- **B2. 마커/심볼**: 점 인스턴싱 + SDF 프래그먼트(circle/square/cross/plus/
  triangle). done = 곡선 정점에 심볼, 크기 픽셀 기준.

### Wave C — 상호작용/피킹 (`widget::interaction`)

- **C1. hover crosshair**: 데이터 좌표 십자선 + 좌표 라벨.
- **C2. 피킹(±3px)**: pixel→data 박스로 곡선 최근접 점/이미지 인덱스 산출,
  결과를 `Response`/콜백으로 반환. done = 클릭 지점의 점/픽셀 인덱스 정확.
- **C3. ROI selector**: 사각/수평/수직 영역 아이템 + egui 핸들 드래그, 변경
  이벤트 emit. done = 핸들 드래그로 bounds 갱신·이벤트 수신.

### Wave D — 스케일/대용량

- **D1. decimation**: 큰 곡선을 픽셀 칼럼당 min/max로 다운샘플해 드로우(시각
  동일, 정점 수 ↓). done = N≫픽셀에서 시각 동일 + 정점 감소 측정.
- **D2. 이미지 타일링**: `max_texture_dimension_2d` 초과 이미지를 타일 분할
  업로드/드로우. done = 한도 초과 이미지가 정상 표시.

### Wave E — 운영

- **E1. save_graph(PNG)**: 오프스크린 타깃 렌더 후 리드백 저장.
  done = 현재 뷰가 PNG로 저장됨.
- **E2. 컬러맵 카탈로그**: viridis 외 magma/inferno/plasma/cividis/gray 등 +
  reverse. done = 이름으로 컬러맵 선택 + 컬러바 반영.

순서 메모: A가 토대(B/C가 좌표·픽셀폭에 의존), E2는 독립적이라 어느 시점에나
가능(워밍업으로 먼저 처리). 이미지+log, 두꺼운 라인 join/cap, ROI 전체 종류는
각 항목 내 후속으로 표시한다.

---

## 부록 A. BackendPygfx 포팅 앵커 (줄 단위)

| 기능 | BackendPygfx.py | 이식 위치 |
|---|---|---|
| addCurve 아이템 | 115-299 | `render::gpu_curve` |
| addImage 아이템 | 1023-1219 | `render::gpu_image` |
| 컬러맵 LUT/정규화/NaN | 365-480 | `core::colormap` + 셰이더 |
| dataToPixel/pixelToData | (_PlotFrameCore) 1121-1219 | `core::transform` |
| ortho 카메라 show_rect | 1517, 1679-1707 | `core::transform`(Mat4) |
| 렌더 루프(2-pass) | 1644-1813 | `widget`(chrome) + `render`(data) |
| 마커 screen-space | 1814-1970 | `widget::chrome` |
| 피킹 dispatch | 2388-2553 | `widget::interaction` |
| 이미지 풀링/재사용 | 2231-2242 | `WgpuResources` 핸들 맵 |