open-plant 1.4.3 → 1.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -220
- package/dist/index.cjs +8 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1074 -1065
- package/dist/index.js.map +1 -1
- package/dist/types/wsi/image-info.d.ts +2 -0
- package/dist/types/wsi/image-info.d.ts.map +1 -1
- package/dist/types/wsi/types.d.ts +1 -1
- package/dist/types/wsi/types.d.ts.map +1 -1
- package/dist/types/wsi/wsi-input-handlers.d.ts +1 -1
- package/dist/types/wsi/wsi-input-handlers.d.ts.map +1 -1
- package/dist/types/wsi/wsi-interaction.d.ts +1 -1
- package/dist/types/wsi/wsi-interaction.d.ts.map +1 -1
- package/dist/types/wsi/wsi-renderer-types.d.ts +1 -0
- package/dist/types/wsi/wsi-renderer-types.d.ts.map +1 -1
- package/dist/types/wsi/wsi-tile-renderer.d.ts +0 -1
- package/dist/types/wsi/wsi-tile-renderer.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
WebGL2 기반 고성능 WSI(Whole Slide Image) 뷰어 라이브러리<br/>
|
|
7
|
-
고사양 PC가 아니어도, 고작, 고~오작 iPhone 15에서
|
|
7
|
+
고사양 PC가 아니어도, 고작, 고~오작 iPhone 15에서 수백~수천만 cell을 부드럽게 렌더링
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
@@ -29,6 +29,78 @@ https://github.com/user-attachments/assets/5a6b5deb-7442-4389-908f-bf2c69348824
|
|
|
29
29
|
범용 시각화 프레임워크 위에 병리 뷰어를 올리면 추상화 비용을 그대로 떠안게 됩니다.
|
|
30
30
|
Open Plant는 WSI 렌더링 **한 가지만** 하도록 설계되었고, 그래서 아래가 가능합니다.
|
|
31
31
|
|
|
32
|
+
### Open Plant vs deck.gl vs OpenLayers — 숫자로 증명하는 압도적 차이
|
|
33
|
+
|
|
34
|
+
같은 **합성 포인트 데이터**(랜덤 2D 좌표 + 16-term 팔레트)를 **3개 엔진의 각각 최적 경로**로 나란히 측정했습니다. (Apple M-시리즈, Chrome 실측)
|
|
35
|
+
|
|
36
|
+
- **deck.gl v9** — `ScatterplotLayer` binary accessor (`data.attributes`에 TypedArray 직전달, JS 객체 0개)
|
|
37
|
+
- **OpenLayers v10** — `WebGLVectorLayer` + `RenderFeature` (가장 가벼운 Feature 모델)
|
|
38
|
+
- 벤치마크 소스 & 재현: [`benchmark/`](./benchmark/)
|
|
39
|
+
|
|
40
|
+
#### 렌더 프레임 타임 (ms) — 실측
|
|
41
|
+
|
|
42
|
+
| 포인트 수 | Open Plant avg | deck.gl avg | 배율 | OpenLayers avg | 배율 |
|
|
43
|
+
|---|---|---|---|---|---|
|
|
44
|
+
| **500K** | **6.04 ms** | 69.81 ms | 11.6× | 17.06 ms | 2.8× |
|
|
45
|
+
| **1M** | **11.17 ms** | 139.72 ms | 12.5× | 18.43 ms | 1.7× |
|
|
46
|
+
| **2M** | **12.64 ms** | 265.96 ms | 21.0× | 32.04 ms | 2.5× |
|
|
47
|
+
| **5M** | **29.24 ms** | 639.59 ms | 21.9× | — | — |
|
|
48
|
+
| **10M** | **61.45 ms** | 1,203.25 ms | 19.6× | — | — |
|
|
49
|
+
|
|
50
|
+
> Open Plant는 모든 구간에서 deck.gl 대비 **12~22×** 빠릅니다. 2M까지 avg **12ms대**(~80fps), 5M에서 **29ms**(~34fps), 10M에서도 **61ms**(~16fps)를 유지합니다. deck.gl은 1M에서 이미 **140ms/frame** — 7fps도 안 나옵니다.
|
|
51
|
+
> OpenLayers는 2M까지는 측정 가능하지만 5M 이상에서는 Feature 빌드 단계에서 실패합니다.
|
|
52
|
+
|
|
53
|
+
#### First draw (초기화 + 첫 렌더)
|
|
54
|
+
|
|
55
|
+
| 포인트 수 | Open Plant | deck.gl | OpenLayers |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| **500K** | 48.1 ms | **4.3 ms** | 466.3 ms |
|
|
58
|
+
| **1M** | 22.6 ms | **20.9 ms** | 1,151.1 ms |
|
|
59
|
+
| **2M** | 26.8 ms | **15.6 ms** | 2,442.8 ms |
|
|
60
|
+
| **5M** | 44.5 ms | **16.7 ms** | — |
|
|
61
|
+
| **10M** | 90.0 ms | **21.5 ms** | — |
|
|
62
|
+
|
|
63
|
+
> deck.gl binary accessor는 first draw가 4~21ms로 가장 빠르지만, 이후 **프레임마다** 인스턴스 파이프라인 비용이 누적되어 sustained fps가 급락합니다.
|
|
64
|
+
> OpenLayers는 first draw에 RenderFeature 빌드 + WebGL 버퍼 구성이 포함되어, 2M에서 **2.4초**, 5M 이상은 실행 불가.
|
|
65
|
+
|
|
66
|
+
#### p99 프레임 타임 (worst-case jank)
|
|
67
|
+
|
|
68
|
+
| 포인트 수 | Open Plant p99 | deck.gl p99 | OpenLayers p99 |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| **500K** | **12.78 ms** | 119.40 ms | 41.70 ms |
|
|
71
|
+
| **1M** | **13.96 ms** | 287.30 ms | 48.00 ms |
|
|
72
|
+
| **2M** | **15.74 ms** | 322.20 ms | 109.40 ms |
|
|
73
|
+
| **5M** | **37.00 ms** | 1,384.30 ms | — |
|
|
74
|
+
| **10M** | **76.09 ms** | 2,495.20 ms | — |
|
|
75
|
+
|
|
76
|
+
#### 메모리 & 파이프라인
|
|
77
|
+
|
|
78
|
+
| 지표 | Open Plant | deck.gl (binary) | OpenLayers (RenderFeature) |
|
|
79
|
+
|---|---|---|---|
|
|
80
|
+
| **GPU 버퍼/점** | **10 B** (xy + palette idx) | 16 B (xyz + RGBA) | ~320 B (RenderFeature obj) |
|
|
81
|
+
| **Draw calls** | **1** (`gl.POINTS`) | N (인스턴스 배치 분할) | N |
|
|
82
|
+
| **JS 객체** | **0개** | **0개** | count개 RenderFeature |
|
|
83
|
+
| **빌드 비용 (2M)** | **0 ms** | ~16 ms | **152 ms** |
|
|
84
|
+
| **색상 변경** | 팔레트 텍스처 **64 B** 재업로드 | 전체 컬러 버퍼 재전송 | 스타일 재평가 + 버퍼 리빌드 |
|
|
85
|
+
| **hit-index** | **0 ms** main (Worker 오프로드) | 메인 스레드 피킹 빌드 | 메인 스레드 피킹 |
|
|
86
|
+
|
|
87
|
+
#### 왜 이렇게 차이가 나는가
|
|
88
|
+
|
|
89
|
+
| Open Plant 설계 | 범용 엔진이 버릴 수 없는 비용 |
|
|
90
|
+
|---|---|
|
|
91
|
+
| WSI 전용 직교 투영 → 투영 분기 없음 | 지리 좌표·투영·줌 레벨 범용 처리 |
|
|
92
|
+
| 팔레트 인덱스 1장 → 색상 버퍼 자체가 없음 | 피처별 RGBA + 스타일 콜백/파이프라인 |
|
|
93
|
+
| `gl.POINTS` 1-draw → 인스턴스 분할 없음 | 레이어 추상화 + 데이터 분할 |
|
|
94
|
+
| TypedArray 직통 GPU → JS 객체 0개 | Feature/RenderFeature 객체 모델 |
|
|
95
|
+
| hit-index/ROI 클립 전량 Worker | 메인 스레드 피킹 + 스타일 resolve |
|
|
96
|
+
|
|
97
|
+
deck.gl binary accessor(JS 객체 0개)를 써도 **1M 포인트에서 140ms/frame** — 7fps도 안 나옵니다.
|
|
98
|
+
OpenLayers는 가장 가벼운 `RenderFeature`를 써도 **5M 이상에서 실행 자체가 불가능**.
|
|
99
|
+
**Open Plant는 10M 포인트를 61ms/frame(~16fps)로 렌더** — deck.gl 대비 **19.6×** 빠릅니다.
|
|
100
|
+
|
|
101
|
+
> **직접 재현:** `cd benchmark && npm i && npx vite` → 브라우저에서 포인트 수 선택. Open Plant은 GPU timer query(또는 readPixels fence) 측정, deck.gl/OL은 rAF-to-rAF 측정.
|
|
102
|
+
> 내부 최적화 히스토리: [`perf-optimization-report.md`](./perf-optimization-report.md) · 운영 기본값: [`performance-optimization.md`](./performance-optimization.md)
|
|
103
|
+
|
|
32
104
|
### 모바일 실전 성능 (iPhone 15)
|
|
33
105
|
|
|
34
106
|
Open Plant는 “고사양 PC에서만 빠른 뷰어”가 아니라, iPhone 15 같은 일반 플래그십 모바일에서도
|
|
@@ -45,8 +117,8 @@ Open Plant는 `Float32Array`(x, y) 8바이트 + `Uint16Array`(palette index) 2
|
|
|
45
117
|
|
|
46
118
|
### 프래그먼트 셰이더 안에서 끝나는 링 렌더링
|
|
47
119
|
|
|
48
|
-
`gl.POINTS` + `gl_PointCoord`로 원형 마스킹하고, ring
|
|
49
|
-
|
|
120
|
+
`gl.POINTS` + `gl_PointCoord`로 원형 마스킹하고, ring 두께는 `uPointStrokeScale * mix(0.18, 0.35, smoothstep(3.0, 16.0, uPointSize))`로 줌·스케일에 따라 적응합니다.
|
|
121
|
+
가장자리 안티앨리어싱은 `aa = 1.5 / max(1.0, uPointSize)`와 `smoothstep`으로 프래그먼트 셰이더 안에서 처리합니다.
|
|
50
122
|
고배율에서는 얇은 링으로 개별 세포를 구분하고, 저배율에서는 두꺼운 링으로 밀집 영역이 묻히지 않습니다.
|
|
51
123
|
별도 geometry 없이 draw call **1회**로 전체 포인트를 그립니다.
|
|
52
124
|
|
|
@@ -77,7 +149,7 @@ draw mode에 진입하면 `setPointerCapture`로 입력을 독점해 팬(드래
|
|
|
77
149
|
| **회전 인터랙션** | `WsiViewState.rotationDeg`, `Ctrl/Cmd + drag` 회전, `resetRotation` 경로 |
|
|
78
150
|
| **줌 범위 제어 + 전환 애니메이션** | `minZoom`/`maxZoom` clamp + `viewTransition`(duration/easing) |
|
|
79
151
|
| **배율 스냅 줌** | `zoomSnaps`, `zoomSnapFitAsMin`으로 wheel/더블클릭을 표준 배율 단계에 맞춰 이동 |
|
|
80
|
-
| **포인트 오버레이** | WebGL2 `gl.POINTS
|
|
152
|
+
| **포인트 오버레이** | WebGL2 `gl.POINTS` + 팔레트 텍스처. `WsiPointData`: `positions`/`paletteIndices` 필수, 선택 `fillModes`, `ids`, `drawIndices`(부분 draw) |
|
|
81
153
|
| **포인트 크기 커스터마이즈** | `pointSizeByZoom` 객체로 zoom별 셀(px) 크기 지정 + 내부 선형 보간 |
|
|
82
154
|
| **포인트 내부 채움 제어** | `pointInnerFillOpacity`로 ring 내부 채움 강도 제어 |
|
|
83
155
|
| **포인트 렌더 모드 제어** | `pointData.fillModes`로 ring/solid 렌더링 제어 |
|
|
@@ -91,10 +163,10 @@ draw mode에 진입하면 `setPointerCapture`로 입력을 독점해 팬(드래
|
|
|
91
163
|
| **ROI 포인트 클리핑** | `clipMode`: `sync` / `worker` / `hybrid-webgpu` (실험) |
|
|
92
164
|
| **ROI 통계 API** | `computeRoiPointGroups()` + `onRoiPointGroups` 콜백 |
|
|
93
165
|
| **ROI 커스텀 오버레이** | `resolveRegionStrokeStyle`, `overlayShapes` |
|
|
94
|
-
| **포인트 Hit-Test** | `
|
|
166
|
+
| **포인트 Hit-Test** | `PointLayer`의 `onHover`/`onClick`, `ref.queryAt(coord)`로 좌표→cell 매핑 |
|
|
95
167
|
| **WebGPU 연산 경로** | WebGPU capability 체크 + ROI bbox prefilter compute(실험) |
|
|
96
168
|
| **오버뷰 미니맵** | 썸네일 + 현재 뷰포트 인디케이터, 클릭/드래그 네비게이션 |
|
|
97
|
-
| **React 바인딩** | `<
|
|
169
|
+
| **React 바인딩** | `<WsiViewer>` + 레이어(`PointLayer`, `RegionLayer`, `DrawingLayer` 등), `useViewerContext`, `<DrawLayer>`, `<OverviewMap>`, `<TileViewerCanvas>` |
|
|
98
170
|
| **좌표 변환** | `screenToWorld()` / `worldToScreen()` 양방향 좌표 변환 |
|
|
99
171
|
| **인증 지원** | Bearer 토큰 패스스루로 프라이빗 타일/포인트 엔드포인트 접근 |
|
|
100
172
|
|
|
@@ -107,253 +179,183 @@ npm run dev:example
|
|
|
107
179
|
|
|
108
180
|
브라우저에서 `http://localhost:5174` 접속.
|
|
109
181
|
|
|
182
|
+
같은 LAN의 다른 기기에서 접속하려면 `example/vite.config.ts`에 `server.host: true`가 설정되어 있어야 하며(현재 예제 기본값), 방화벽에서 해당 포트를 허용합니다.
|
|
183
|
+
|
|
184
|
+
## 좌표계 (카메라)
|
|
185
|
+
|
|
186
|
+
- **World**: WSI 이미지 픽셀 좌표 (원점은 이미지 정의에 따름, 일반적으로 좌상단).
|
|
187
|
+
- **Screen**: 캔버스 내부 좌표 (`OrthoCamera`의 viewport = 캔버스 CSS 픽셀 × devicePixelRatio에 맞춘 내부 크기).
|
|
188
|
+
- **Clip (NDC)**: WebGL 클립 공간 −1~1. `OrthoCamera.getMatrix()`가 World → Clip 3×3 동차 변환을 반환하며, 타일/포인트 vertex 셰이더의 `uCamera`에 전달됩니다.
|
|
189
|
+
- **JS 변환**: `screenToWorld` / `worldToScreen`은 휠 줌 피벗, 히트 테스트 등 CPU 측 단일 점 변환에 사용됩니다.
|
|
190
|
+
|
|
110
191
|
## Project Structure
|
|
111
192
|
|
|
112
193
|
```
|
|
113
194
|
src/
|
|
114
|
-
├── core/
|
|
115
|
-
│ ├── gl-utils.ts
|
|
116
|
-
│ ├── ortho-camera.ts
|
|
117
|
-
│
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
│ ├──
|
|
121
|
-
│ ├──
|
|
122
|
-
│ ├──
|
|
123
|
-
│ ├── point-
|
|
124
|
-
│ ├──
|
|
125
|
-
│ ├──
|
|
126
|
-
│ ├──
|
|
127
|
-
│
|
|
195
|
+
├── core/
|
|
196
|
+
│ ├── gl-utils.ts # 셰이더 컴파일·프로그램 링크
|
|
197
|
+
│ ├── ortho-camera.ts # 2D 직교 카메라 (pan / zoom / rotation, World↔Screen, getMatrix)
|
|
198
|
+
│ ├── m1-tile-renderer.ts # 단순 타일 그리드 데모용 렌더러
|
|
199
|
+
│ └── types.ts
|
|
200
|
+
├── wsi/
|
|
201
|
+
│ ├── wsi-tile-renderer.ts # WebGL2 멀티티어 타일 + 포인트, 입력, 애니메이션
|
|
202
|
+
│ ├── wsi-render-pass.ts # 프레임당 draw 순서: fallback 타일 → visible 타일 → 포인트
|
|
203
|
+
│ ├── wsi-shaders.ts # 타일·포인트 GLSL 프로그램 초기화
|
|
204
|
+
│ ├── wsi-point-data.ts # 포인트 VBO 업로드 (positions / terms / fillModes / drawIndices)
|
|
205
|
+
│ ├── wsi-interaction.ts # 포인터·휠·스냅 줌 이벤트 처리
|
|
206
|
+
│ ├── wsi-input-handlers.ts # interaction lock 등 래핑
|
|
207
|
+
│ ├── wsi-zoom-snap.ts # 배율 스냅 애니메이션
|
|
208
|
+
│ ├── wsi-view-ops.ts # zoomBy, fit, clamp 등 뷰 수학
|
|
209
|
+
│ ├── wsi-view-animation.ts # 일반 뷰 보간 애니메이션
|
|
210
|
+
│ ├── wsi-tile-visibility.ts # 뷰 바운드·타일 가시성
|
|
211
|
+
│ ├── wsi-tile-cache.ts # 타일 텍스처 LRU 트림
|
|
212
|
+
│ ├── wsi-canvas-lifecycle.ts # 컨텍스트 lost/restored
|
|
213
|
+
│ ├── tile-scheduler.ts # 타일 fetch 큐, createImageBitmap, 재시도
|
|
214
|
+
│ ├── point-clip.ts # 메인스레드 ROI 클리핑
|
|
215
|
+
│ ├── point-clip-worker-client.ts / point-clip-worker-protocol.ts
|
|
216
|
+
│ ├── point-clip-hybrid.ts # WebGPU bbox prefilter (실험)
|
|
217
|
+
│ ├── point-hit-index-*.ts # 포인트 공간 해시 인덱스 (워커)
|
|
218
|
+
│ ├── roi-geometry.ts / roi-term-stats.ts / brush-stroke.ts
|
|
219
|
+
│ ├── image-info.ts / types.ts / utils.ts / wkt.ts / webgpu.ts / constants.ts
|
|
220
|
+
│ └── …
|
|
128
221
|
├── workers/
|
|
129
|
-
│ ├── roi-clip-worker.ts
|
|
130
|
-
│ └── point-hit-index-worker.ts
|
|
131
|
-
└── react/
|
|
132
|
-
├── wsi-viewer
|
|
133
|
-
├──
|
|
134
|
-
|
|
222
|
+
│ ├── roi-clip-worker.ts
|
|
223
|
+
│ └── point-hit-index-worker.ts
|
|
224
|
+
└── react/
|
|
225
|
+
├── wsi-viewer.tsx # WsiTileRenderer + ViewerContext (권장 엔트리)
|
|
226
|
+
├── viewer-context.ts # rendererRef, worldToScreen, 오버레이 등록
|
|
227
|
+
├── point-layer.tsx # 포인트 데이터·클리핑·히트 테스트
|
|
228
|
+
├── region-layer.tsx # ROI Canvas2D
|
|
229
|
+
├── drawing-layer.tsx # 드로잉 툴 진입 시 오버레이
|
|
230
|
+
├── draw-layer.tsx # 실제 Canvas2D 드로잉 구현
|
|
231
|
+
├── patch-layer.tsx / overlay-layer.tsx
|
|
232
|
+
├── overview-map.tsx
|
|
233
|
+
├── tile-viewer-canvas.tsx # 타일만 있는 경량 뷰어
|
|
234
|
+
├── use-point-clipping.ts / use-point-hit-test.ts / …
|
|
235
|
+
└── wsi-viewer-canvas-types.ts # 포인트/포인터 이벤트 타입
|
|
135
236
|
```
|
|
136
237
|
|
|
137
238
|
## React Components
|
|
138
239
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
전체 기능을 갖춘 WSI 뷰어 컴포넌트. 실사용 시 대부분의 기능은 이 컴포넌트 하나로 제어합니다.
|
|
142
|
-
|
|
143
|
-
```jsx
|
|
144
|
-
import { WsiViewerCanvas } from "open-plant";
|
|
145
|
-
|
|
146
|
-
<WsiViewerCanvas
|
|
147
|
-
source={imageSource}
|
|
148
|
-
viewState={viewState}
|
|
149
|
-
imageColorSettings={{ brightness: 0, contrast: 0, saturation: 0 }}
|
|
150
|
-
ctrlDragRotate
|
|
151
|
-
rotationResetNonce={rotationResetNonce}
|
|
152
|
-
minZoom={0.25} // 미지정 시 fitZoom * 0.5
|
|
153
|
-
maxZoom={1} // 미지정 시 fitZoom * 8
|
|
154
|
-
viewTransition={{ duration: 300 }}
|
|
155
|
-
zoomSnaps={[1.25, 2.5, 5, 10, 20, 40]}
|
|
156
|
-
zoomSnapFitAsMin
|
|
157
|
-
authToken={bearerToken}
|
|
158
|
-
pointData={pointPayload}
|
|
159
|
-
pointPalette={termPalette.colors}
|
|
160
|
-
pointSizeByZoom={{
|
|
161
|
-
1: 2.8,
|
|
162
|
-
6: 8.4,
|
|
163
|
-
10: 17.5,
|
|
164
|
-
12: 28,
|
|
165
|
-
}}
|
|
166
|
-
pointInnerFillOpacity={0.15}
|
|
167
|
-
clipPointsToRois
|
|
168
|
-
clipMode="worker"
|
|
169
|
-
onClipStats={(s) => console.log(s.mode, s.durationMs)}
|
|
170
|
-
drawTool={drawTool}
|
|
171
|
-
drawFillColor="transparent"
|
|
172
|
-
activeRegionId={selectedRoiId} // controlled: 외부에서 active ROI 제어
|
|
173
|
-
onActiveRegionChange={setSelectedRoiId} // 내부 클릭/탭 선택 변경 알림
|
|
174
|
-
resolveRegionLabelStyle={({ zoom }) => ({
|
|
175
|
-
offsetY: zoom > 4 ? -20 : -10,
|
|
176
|
-
})}
|
|
177
|
-
autoLiftRegionLabelAtMaxZoom
|
|
178
|
-
drawAreaTooltip={{
|
|
179
|
-
enabled: true,
|
|
180
|
-
cursorOffset: { x: 16, y: -24 },
|
|
181
|
-
format: (areaMm2) => `${areaMm2.toFixed(3)} mm²`,
|
|
182
|
-
}}
|
|
183
|
-
brushOptions={{
|
|
184
|
-
radius: 32, // HTML/CSS px (zoom이 바뀌어도 화면에서 고정)
|
|
185
|
-
edgeDetail: 1.6, // 값이 클수록 더 둥글고 섬세한 브러시 경계
|
|
186
|
-
edgeSmoothing: 2, // 계단 현상 감소(0~4)
|
|
187
|
-
clickSelectRoi: true, // brush에서 "클릭만" 하면 ROI 선택 우선
|
|
188
|
-
}}
|
|
189
|
-
stampOptions={{
|
|
190
|
-
rectangleAreaMm2: 2,
|
|
191
|
-
circleAreaMm2: 0.2, // HPF 예시
|
|
192
|
-
rectanglePixelSize: 4096,
|
|
193
|
-
}}
|
|
194
|
-
patchRegions={patchRegions}
|
|
195
|
-
patchStrokeStyle={{ color: "#8ad8ff", lineDash: [10, 8], width: 2 }}
|
|
196
|
-
customLayers={[
|
|
197
|
-
{
|
|
198
|
-
id: "patch-labels",
|
|
199
|
-
render: ({ worldToScreen }) => {
|
|
200
|
-
/* host overlay */
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
]}
|
|
204
|
-
onPointerWorldMove={(e) => console.log(e.coordinate)}
|
|
205
|
-
onRoiPointGroups={(stats) => console.log(stats.groups)}
|
|
206
|
-
onDrawComplete={(result) => {
|
|
207
|
-
if (result.intent === "roi") handleRoi(result);
|
|
208
|
-
if (result.intent === "brush") handleBrush(result);
|
|
209
|
-
}}
|
|
210
|
-
onPatchComplete={(patch) => {
|
|
211
|
-
// stamp-rectangle-4096px 전용
|
|
212
|
-
handlePatch(patch);
|
|
213
|
-
}}
|
|
214
|
-
overviewMapConfig={{
|
|
215
|
-
show: true,
|
|
216
|
-
options: {
|
|
217
|
-
viewportBorderStyle: "dash",
|
|
218
|
-
viewportBorderColor: "rgba(255, 106, 61, 0.95)",
|
|
219
|
-
viewportFillColor: "rgba(255, 106, 61, 0.08)",
|
|
220
|
-
},
|
|
221
|
-
}}
|
|
222
|
-
onViewStateChange={handleViewChange}
|
|
223
|
-
onStats={setStats}
|
|
224
|
-
/>
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
#### 동작 규약 (중요)
|
|
228
|
-
|
|
229
|
-
- `mpp`(microns per pixel)는 스탬프 mm² 환산에 사용됩니다. 미지정 시 물리 크기는 근사치입니다.
|
|
230
|
-
- `imageColorSettings`는 타일 레이어에만 적용됩니다. 포인트/ROI/드로잉은 영향받지 않습니다.
|
|
231
|
-
- ROI hit-test는 **contour + nametag 영역** 기준입니다. ROI 내부 fill은 클릭/hover 영역에서 제외됩니다.
|
|
232
|
-
- `activeRegionId`를 주면 controlled mode, 생략하면 uncontrolled mode로 동작합니다.
|
|
233
|
-
- `minZoom`/`maxZoom`은 휠/더블클릭/`setViewState`/`fitToImage` 전 경로에 동일 clamp가 적용됩니다.
|
|
234
|
-
- `viewTransition`은 `setViewState`/`fitToImage`/`zoomBy` 전환에 적용되며 `duration` 최대값은 `2000ms`입니다.
|
|
235
|
-
- `zoomSnaps`는 배율(magnification) 배열 입력이며 `source.mpp`를 기준으로 내부 zoom 값으로 정규화됩니다.
|
|
236
|
-
- `zoomSnapFitAsMin=true`이면 snap-out 경로에서 fit zoom을 하한으로 취급합니다.
|
|
237
|
-
- `drawFillColor` 기본값은 `transparent`입니다.
|
|
238
|
-
- `brushOptions.radius`는 HTML/CSS px 기준이며, 줌이 바뀌어도 on-screen 크기는 고정됩니다.
|
|
239
|
-
- `brushOptions.clickSelectRoi=true`이면 브러시 탭(드래그 없음) 시 ROI를 먼저 선택하고, ROI 외부 탭은 일반 브러시 결과를 반환합니다.
|
|
240
|
-
- `autoLiftRegionLabelAtMaxZoom=true`이면 `maxZoom` 도달 시 라벨이 위로 `20px` 애니메이션 이동하고, 이탈 시 원위치로 내려옵니다.
|
|
241
|
-
- `drawAreaTooltip.enabled=true`이면 freehand/rectangle/circular 그리기 중 커서 근처에 실시간 면적(mm²)을 표시합니다.
|
|
242
|
-
- `roiRegions[].coordinates`는 ring / polygon(with holes) / multipolygon을 모두 지원합니다.
|
|
240
|
+
**v1.4+** 공개 API는 `<WsiViewer>`와 **자식 레이어 컴포지션**입니다.
|
|
241
|
+
과거 단일 컴포넌트 `WsiViewerCanvas`는 **이 저장소·npm 패키지에서 제거**되었습니다. props 매핑표는 [`docs/migration-1.4.0.md`](./docs/migration-1.4.0.md)와 [정적 문서](https://frorong.github.io/open-plant/)를 참고하세요.
|
|
243
242
|
|
|
244
|
-
|
|
243
|
+
### `<WsiViewer>`
|
|
245
244
|
|
|
246
|
-
|
|
245
|
+
`WsiTileRenderer` + WebGL 캔버스 + Canvas2D 오버레이 + `ViewerContextProvider`를 구성합니다. 타일·카메라·줌 스냅·포인터 world 스트림 등은 여기서 제어하고, 포인트/ROI/드로잉은 **자식 레이어**로 붙입니다.
|
|
247
246
|
|
|
248
247
|
| Prop | Type | Notes |
|
|
249
248
|
|---|---|---|
|
|
250
|
-
| `source` | `WsiImageSource \| null` | 필수
|
|
251
|
-
| `viewState` | `Partial<WsiViewState> \| null` | 외부 제어
|
|
252
|
-
| `onViewStateChange` | `(next) => void` |
|
|
253
|
-
| `
|
|
254
|
-
| `
|
|
249
|
+
| `source` | `WsiImageSource \| null` | 필수 메타데이터 |
|
|
250
|
+
| `viewState` | `Partial<WsiViewState> \| null` | 외부 제어 |
|
|
251
|
+
| `onViewStateChange` | `(next: WsiViewState) => void` | 뷰 변경 통지 |
|
|
252
|
+
| `imageColorSettings` | `WsiImageColorSettings \| null` | 타일에만 적용 (BC/S) |
|
|
253
|
+
| `fitNonce` | `number` | 증가 시 fit 재실행 |
|
|
254
|
+
| `rotationResetNonce` | `number` | 증가 시 회전 0° |
|
|
255
|
+
| `authToken` | `string` | 타일 fetch Bearer 등 |
|
|
255
256
|
| `ctrlDragRotate` | `boolean` | 기본 `true` |
|
|
256
257
|
| `minZoom` / `maxZoom` | `number` | 미지정 시 `fitZoom*0.5` / `fitZoom*8` |
|
|
257
|
-
| `viewTransition` | `
|
|
258
|
-
| `zoomSnaps` | `number[]` |
|
|
259
|
-
| `zoomSnapFitAsMin` | `boolean` | 스냅 아웃 시 fit
|
|
260
|
-
| `
|
|
261
|
-
| `
|
|
258
|
+
| `viewTransition` | `WsiViewTransitionOptions` | `setViewState`/`fitToImage` 등 전환 |
|
|
259
|
+
| `zoomSnaps` | `number[]` | 배율 배열 → `mpp` 기준 내부 zoom으로 정규화 |
|
|
260
|
+
| `zoomSnapFitAsMin` | `boolean` | 스냅 아웃 시 fit을 하한으로 |
|
|
261
|
+
| `onStats` | `(WsiRenderStats) => void` | 프레임 통계 |
|
|
262
|
+
| `onTileError` | `(WsiTileErrorEvent) => void` | 타일 로드 실패 |
|
|
263
|
+
| `onContextLost` / `onContextRestored` | `() => void` | WebGL 컨텍스트 |
|
|
264
|
+
| `onPointerWorldMove` | `(PointerWorldMoveEvent) => void` | 포인터 world 좌표 |
|
|
265
|
+
| `debugOverlay` | `boolean` | 간단한 디버그 패널 |
|
|
266
|
+
| `className` / `style` | — | 루트 컨테이너 |
|
|
267
|
+
| `children` | `ReactNode` | 아래 레이어 컴포넌트 |
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
import {
|
|
271
|
+
DrawingLayer,
|
|
272
|
+
OverviewMap,
|
|
273
|
+
OverlayLayer,
|
|
274
|
+
PatchLayer,
|
|
275
|
+
PointLayer,
|
|
276
|
+
RegionLayer,
|
|
277
|
+
useViewerContext,
|
|
278
|
+
WsiViewer,
|
|
279
|
+
} from "open-plant";
|
|
280
|
+
|
|
281
|
+
<WsiViewer source={source} viewState={vs} onViewStateChange={setVs} authToken={token} zoomSnaps={[...]} zoomSnapFitAsMin>
|
|
282
|
+
<PointLayer data={pointData} palette={palette} sizeByZoom={sizes} clipEnabled clipToRegions={rois} clipMode="worker" />
|
|
283
|
+
<RegionLayer regions={rois} ... />
|
|
284
|
+
<DrawingLayer tool={drawTool} ... />
|
|
285
|
+
<PatchLayer regions={patches} ... />
|
|
286
|
+
<OverlayLayer shapes={overlayShapes} />
|
|
287
|
+
</WsiViewer>
|
|
288
|
+
```
|
|
262
289
|
|
|
263
|
-
|
|
290
|
+
미니맵은 보통 `WsiViewer` **밖**에서 `useViewerContext()`로 `rendererRef`를 넘기거나, 예제처럼 래퍼 컴포넌트로 `<OverviewMap projectorRef={rendererRef} invalidateRef={overviewInvalidateRef} ... />`를 둡니다 (`example/src/App.tsx` 참고).
|
|
264
291
|
|
|
265
|
-
|
|
266
|
-
|---|---|---|
|
|
267
|
-
| `imageColorSettings` | `WsiImageColorSettings \| null` | brightness/contrast/saturation 입력 범위 `[-100, 100]` |
|
|
268
|
-
| `pointData` | `WsiPointData \| null` | `positions`, `paletteIndices` 필수 |
|
|
269
|
-
| `pointPalette` | `Uint8Array \| null` | RGBA 팔레트 텍스처 |
|
|
270
|
-
| `pointSizeByZoom` | `Record<number, number>` | continuous zoom stop |
|
|
271
|
-
| `pointStrokeScale` | `number` | point ring 두께 스케일 |
|
|
272
|
-
| `pointInnerFillOpacity` | `number` | 포인트 내부 채움 불투명도 (`0..1`) |
|
|
273
|
-
| `clipPointsToRois` | `boolean` | ROI 외부 포인트 필터 |
|
|
274
|
-
| `clipMode` | `"sync" \| "worker" \| "hybrid-webgpu"` | 기본 `"worker"` |
|
|
275
|
-
| `onClipStats` | `(event) => void` | clip 실행 통계 |
|
|
276
|
-
| `onRoiPointGroups` | `(stats) => void` | ROI term 통계 |
|
|
277
|
-
| `roiPaletteIndexToTermId` | `ReadonlyMap<number,string> \| readonly string[]` | ROI term 매핑 |
|
|
278
|
-
|
|
279
|
-
**ROI / Draw / Overlay**
|
|
292
|
+
### 레이어 컴포넌트 (요약)
|
|
280
293
|
|
|
281
|
-
|
|
|
282
|
-
|
|
283
|
-
| `
|
|
284
|
-
|
|
|
285
|
-
|
|
|
286
|
-
|
|
|
287
|
-
|
|
|
288
|
-
|
|
|
289
|
-
| `drawFillColor` | `string` | draw preview fill, 기본 `transparent` |
|
|
290
|
-
| `regionStrokeStyle` / `regionStrokeHoverStyle` / `regionStrokeActiveStyle` | `Partial<RegionStrokeStyle>` | ROI 외곽선 스타일 |
|
|
291
|
-
| `patchStrokeStyle` | `Partial<RegionStrokeStyle>` | patch 선 스타일 |
|
|
292
|
-
| `resolveRegionStrokeStyle` | `RegionStrokeStyleResolver` | 상태별 동적 stroke |
|
|
293
|
-
| `regionLabelStyle` | `Partial<RegionLabelStyle>` | 기본 배지 스타일 override |
|
|
294
|
-
| `regionLabelAnchor` | `RegionLabelAnchorMode` | 라벨 anchor 모드 (`top-center`, `centroid`) |
|
|
295
|
-
| `resolveRegionLabelStyle` | `RegionLabelStyleResolver` | 줌/region별 동적 라벨 스타일 |
|
|
296
|
-
| `autoLiftRegionLabelAtMaxZoom` | `boolean` | max zoom 도달 시 라벨 auto-lift |
|
|
297
|
-
| `clampRegionLabelToViewport` | `boolean` | 라벨 화면 경계 clamp 적용 |
|
|
298
|
-
| `drawAreaTooltip` | `DrawAreaTooltipOptions` | draw 중 실시간 mm² tooltip |
|
|
299
|
-
| `overlayShapes` | `DrawOverlayShape[]` | 커스텀 도형/반전 마스크 |
|
|
300
|
-
| `customLayers` | `WsiCustomLayer[]` | host React 오버레이 슬롯 |
|
|
301
|
-
| `activeRegionId` | `string \| number \| null` | controlled active ROI |
|
|
302
|
-
|
|
303
|
-
**Events / Refs**
|
|
294
|
+
| 컴포넌트 | 역할 |
|
|
295
|
+
|---|---|
|
|
296
|
+
| **PointLayer** | `WsiPointData` + 팔레트, zoom별 크기, ROI 클리핑(`clipMode`: `sync` / `worker` / `hybrid-webgpu`). `ref` → `queryAt(coord)` imperative hit |
|
|
297
|
+
| **RegionLayer** | ROI contour/라벨 Canvas2D, hover/active, hit-test |
|
|
298
|
+
| **DrawingLayer** | 드로잉 모드 진입·휠 줌 전달 등; 내부에서 **DrawLayer** 사용 |
|
|
299
|
+
| **DrawLayer** | 실제 freehand/rect/circle/brush/stamp 구현 |
|
|
300
|
+
| **PatchLayer** | 패치 ROI 전용 스트로크 |
|
|
301
|
+
| **OverlayLayer** | `DrawOverlayShape` 반전 채움 등 |
|
|
304
302
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
| `getCellByCoordinatesRef` | `MutableRefObject<(coord)=>PointHitEvent \| null>` | imperative 좌표 hit-test |
|
|
313
|
-
| `onRegionHover` / `onRegionClick` | `(event) => void` | region hit 이벤트 |
|
|
314
|
-
| `onActiveRegionChange` | `(regionId) => void` | active 변경 통지 |
|
|
315
|
-
| `onDrawComplete` | `(result: DrawResult) => void` | `intent: "roi" \| "patch" \| "brush"` |
|
|
316
|
-
| `onPatchComplete` | `(result: PatchDrawResult) => void` | `stamp-rectangle-4096px` 전용 |
|
|
317
|
-
| `className` / `style` | `string` / `CSSProperties` | 컨테이너 스타일 |
|
|
318
|
-
|
|
319
|
-
### `<DrawLayer>`
|
|
320
|
-
|
|
321
|
-
독립 오버레이 드로잉 컴포넌트입니다. `WsiViewerCanvas` 내부에서 자동 사용되지만, 필요하면 별도로 직접 사용할 수 있습니다.
|
|
322
|
-
|
|
323
|
-
- 지원 툴: `freehand`, `rectangle`, `circular`, `brush`, `stamp-*`
|
|
324
|
-
- 브러시는 화면 픽셀 기준 반경 + `edgeDetail`/`edgeSmoothing` 옵션을 사용합니다.
|
|
325
|
-
- `Esc`로 현재 드로잉 세션을 취소할 수 있습니다.
|
|
303
|
+
### `useViewerContext()`
|
|
304
|
+
|
|
305
|
+
`rendererRef`, `rendererSerial`, `source`, `worldToScreen`, `registerDrawOverlay`, `overviewInvalidateRef`, interaction lock 등 — 커스텀 오버레이·미니맵에서 사용합니다.
|
|
306
|
+
|
|
307
|
+
### `<DrawLayer>` (단독 사용)
|
|
308
|
+
|
|
309
|
+
`WsiViewer` 없이도 마운트 가능합니다. 툴: `freehand`, `rectangle`, `circular`, `brush`, `stamp-*`. 브러시는 화면 px 반경 + `edgeDetail` / `edgeSmoothing`. `Esc`로 세션 취소.
|
|
326
310
|
|
|
327
311
|
### `<OverviewMap>`
|
|
328
312
|
|
|
329
|
-
|
|
313
|
+
뷰포트 인디케이터 + (옵션) 썸네일 타일. `WsiViewer`에 prop으로 붙지 않고 **`projectorRef`에 `WsiTileRenderer` ref**를 넘겨 동기화합니다.
|
|
330
314
|
|
|
331
315
|
| Option | Type | Notes |
|
|
332
316
|
|---|---|---|
|
|
333
317
|
| `width` / `height` | `number` | 미니맵 캔버스 크기 |
|
|
334
|
-
| `margin`, `position`, `borderRadius` |
|
|
335
|
-
| `backgroundColor`, `borderColor` | `string` | 배경/테두리
|
|
336
|
-
| `viewportBorderStyle` | `"stroke" \| "dash"` |
|
|
337
|
-
| `viewportBorderColor`, `viewportFillColor` | `string` |
|
|
338
|
-
| `interactive` | `boolean` | 클릭/드래그로
|
|
339
|
-
| `showThumbnail`, `maxThumbnailTiles` |
|
|
340
|
-
| `onClose`, `closeIcon`, `closeButtonStyle` |
|
|
318
|
+
| `margin`, `position`, `borderRadius` | — | 배치 |
|
|
319
|
+
| `backgroundColor`, `borderColor` | `string` | 배경/테두리 |
|
|
320
|
+
| `viewportBorderStyle` | `"stroke" \| "dash"` | 뷰포트 테두리 |
|
|
321
|
+
| `viewportBorderColor`, `viewportFillColor` | `string` | 뷰포트 선/채움 |
|
|
322
|
+
| `interactive` | `boolean` | 클릭/드래그로 이동 |
|
|
323
|
+
| `showThumbnail`, `maxThumbnailTiles` | — | 썸네일 |
|
|
324
|
+
| `onClose`, `closeIcon`, `closeButtonStyle` | — | 닫기 UI |
|
|
325
|
+
|
|
326
|
+
### `<TileViewerCanvas>`
|
|
327
|
+
|
|
328
|
+
타일만 필요한 경량 뷰어 (포인트/ROI 없음).
|
|
329
|
+
|
|
330
|
+
### 동작 규약 (공통)
|
|
331
|
+
|
|
332
|
+
- `mpp`는 스탬프 mm² 환산 등에 사용; 없으면 물리 크기는 근사치입니다.
|
|
333
|
+
- `imageColorSettings`는 **타일**에만 적용됩니다. 포인트/ROI/드로잉은 별도입니다.
|
|
334
|
+
- ROI hit-test는 **contour + nametag** 기준입니다 (내부 fill 제외).
|
|
335
|
+
- `minZoom`/`maxZoom`은 휠·더블클릭·`setViewState`·`fitToImage` 전 경로에 동일하게 clamp됩니다.
|
|
336
|
+
- `zoomSnaps` 입력은 **배율**이며 `source.mpp`로 내부 zoom으로 변환됩니다.
|
|
337
|
+
- `brushOptions.radius`는 CSS px, 줌과 무관하게 화면에서 고정 크기입니다.
|
|
338
|
+
- `roiRegions[].coordinates`는 ring / polygon(holes) / multipolygon을 지원합니다.
|
|
341
339
|
|
|
342
340
|
## API
|
|
343
341
|
|
|
342
|
+
`src/index.ts` 기준 공개 export 요약입니다.
|
|
343
|
+
|
|
344
344
|
| Export | 설명 |
|
|
345
345
|
|---|---|
|
|
346
|
-
| `
|
|
347
|
-
| `
|
|
348
|
-
| `
|
|
349
|
-
| `
|
|
350
|
-
| `
|
|
351
|
-
| `filterPointIndicesByPolygons`, `filterPointIndicesByPolygonsInWorker`, `terminateRoiClipWorker` |
|
|
352
|
-
| `buildPointSpatialIndexAsync`, `lookupCellIndex`, `terminatePointHitIndexWorker` | 포인트 공간 인덱스
|
|
346
|
+
| `WsiViewer`, `PointLayer`, `RegionLayer`, `DrawingLayer`, `DrawLayer`, `PatchLayer`, `OverlayLayer`, `OverviewMap`, `TileViewerCanvas` | React |
|
|
347
|
+
| `useViewerContext` | 컨텍스트 (`rendererRef`, `worldToScreen`, …) |
|
|
348
|
+
| `WsiTileRenderer`, `M1TileRenderer`, `TileScheduler` | 코어 렌더러·타일 큐 |
|
|
349
|
+
| `normalizeImageInfo`, `toTileUrl`, `toRoiGeometry`, `parseWkt` | 이미지/ROI/WKT |
|
|
350
|
+
| `buildTermPalette`, `calcScaleResolution`, `calcScaleLength`, `toBearerToken`, `clamp`, `hexToRgba`, `isSameViewState` | 유틸 |
|
|
351
|
+
| `filterPointDataByPolygons`, `filterPointIndicesByPolygons`, `filterPointDataByPolygonsInWorker`, `filterPointIndicesByPolygonsInWorker`, `terminateRoiClipWorker`, `filterPointDataByPolygonsHybrid` | ROI 클리핑 |
|
|
352
|
+
| `buildPointSpatialIndexAsync`, `lookupCellIndex`, `terminatePointHitIndexWorker` | 포인트 공간 인덱스(워커) |
|
|
353
353
|
| `computeRoiPointGroups` | ROI term 통계 |
|
|
354
|
-
| `getWebGpuCapabilities`, `prefilterPointsByBoundsWebGpu` | WebGPU
|
|
355
|
-
| `closeRing`, `createRectangle`, `createCircle` | 도형
|
|
356
|
-
| 타입
|
|
354
|
+
| `getWebGpuCapabilities`, `prefilterPointsByBoundsWebGpu` | WebGPU(실험) |
|
|
355
|
+
| `closeRing`, `createRectangle`, `createCircle` | 도형 |
|
|
356
|
+
| 타입 (`WsiViewerProps`, `WsiImageSource`, `WsiPointData`, `WsiViewState`, `DrawTool`, `PointHitEvent`, …) | TS |
|
|
357
|
+
|
|
358
|
+
레거시 `WsiViewerCanvas` / `WsiViewerCanvasProps`는 패키지에 **포함되지 않습니다**.
|
|
357
359
|
|
|
358
360
|
## Scripts
|
|
359
361
|
|