diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3bac2df..95cbea9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -102,13 +102,13 @@ and this project follows [Semantic Versioning](https://semver.org/).
- Region style resolver API: `resolveRegionStrokeStyle`.
- Custom overlay shape API for patch/dashed guides: `overlayShapes`.
- Fixed-pixel stamp tool: `stamp-rectangle-4096px` and `stampOptions.rectanglePixelSize`.
-- ROI term-group utility and callback path: `computeRoiPointGroups`, `onRoiPointGroups`.
+- ROI class-group utility and callback path: `computeRoiPointGroups`, `onRoiPointGroups`.
- Release gate workflow: `.github/workflows/release-gate.yml`.
- PR template: `.github/pull_request_template.md`.
- Contribution guides: root `CONTRIBUTING.md`, docs EN/KO `contributing.html`.
- Hybrid WebGPU draw bridge payload support via `WsiPointData.drawIndices`.
- Hybrid clip option `bridgeToDraw` and clip stat flag `bridgedToDraw`.
-- Unit test coverage for ROI term stats with draw-index bridge input.
+- Unit test coverage for ROI class stats with draw-index bridge input.
- Patch-intent draw path for `stamp-rectangle-4096px` with dedicated `onPatchComplete` callback.
- Patch overlay channel on viewer (`patchRegions`, `patchStrokeStyle`) separated from ROI hover/active interaction.
- Custom React overlay layer slots via `customLayers` for host-owned rendering pipelines.
@@ -124,7 +124,7 @@ and this project follows [Semantic Versioning](https://semver.org/).
- Publish gate now enforces `npm run release:gate` via `prepublishOnly`.
### Docs
-- Updated EN/KO API and guides for rotation, pointer world callbacks, overlay shapes, 4096px patch intent flow, custom layers, and ROI term stats.
+- Updated EN/KO API and guides for rotation, pointer world callbacks, overlay shapes, 4096px patch intent flow, custom layers, and ROI class stats.
- Updated `todo.md` gap table with current support status and code-path references.
- Added EN/KO migration guides with API stability/deprecation policy and release-gate contract.
- Added EN/KO contributing pages and linked them across docs navigation.
diff --git a/README.md b/README.md
index 4f311ea..5ac8a63 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ Open Plant는 WSI 렌더링 **한 가지만** 하도록 설계되었고, 그래
### Open Plant vs deck.gl vs OpenLayers — 숫자로 증명하는 압도적 차이
-같은 **합성 포인트 데이터**(랜덤 2D 좌표 + 16-term 팔레트)를 **3개 엔진의 각각 최적 경로**로 나란히 측정했습니다. (Apple M-시리즈, Chrome 실측)
+같은 **합성 포인트 데이터**(랜덤 2D 좌표 + 16-class 팔레트)를 **3개 엔진의 각각 최적 경로**로 나란히 측정했습니다. (Apple M-시리즈, Chrome 실측)
- **deck.gl v9** — `ScatterplotLayer` binary accessor (`data.attributes`에 TypedArray 직전달, JS 객체 0개)
- **OpenLayers v10** — `WebGLVectorLayer` + `RenderFeature` (가장 가벼운 Feature 모델)
@@ -112,7 +112,7 @@ Open Plant는 “고사양 PC에서만 빠른 뷰어”가 아니라, iPhone 15
범용 라이브러리는 포인트마다 인스턴스 버퍼에 position + RGBA를 넣어 **20바이트 이상** 씁니다.
Open Plant는 `Float32Array`(x, y) 8바이트 + `Uint16Array`(palette index) 2바이트 = **10바이트**입니다.
-색상은 1×N 팔레트 텍스처 1장에 들어가므로, term 색상을 바꿀 때 수백 바이트짜리 텍스처만 재업로드하면 됩니다.
+색상은 1×N 팔레트 텍스처 1장에 들어가므로, class 색상을 바꿀 때 수백 바이트짜리 텍스처만 재업로드하면 됩니다.
50만 셀 기준 GPU 메모리가 **절반 이하**로 줄어듭니다.
### 프래그먼트 셰이더 안에서 끝나는 링 렌더링
@@ -202,7 +202,7 @@ src/
│ ├── wsi-tile-renderer.ts # WebGL2 멀티티어 타일 + 포인트, 입력, 애니메이션
│ ├── wsi-render-pass.ts # 프레임당 draw 순서: fallback 타일 → visible 타일 → 포인트
│ ├── wsi-shaders.ts # 타일·포인트 GLSL 프로그램 초기화
-│ ├── wsi-point-data.ts # 포인트 VBO 업로드 (positions / terms / fillModes / drawIndices)
+│ ├── wsi-point-data.ts # 포인트 VBO 업로드 (positions / classes / fillModes / drawIndices)
│ ├── wsi-interaction.ts # 포인터·휠·스냅 줌 이벤트 처리
│ ├── wsi-input-handlers.ts # interaction lock 등 래핑
│ ├── wsi-zoom-snap.ts # 배율 스냅 애니메이션
@@ -216,7 +216,7 @@ src/
│ ├── point-clip-worker-client.ts / point-clip-worker-protocol.ts
│ ├── point-clip-hybrid.ts # WebGPU bbox prefilter (실험)
│ ├── point-hit-index-*.ts # 포인트 공간 해시 인덱스 (워커)
-│ ├── roi-geometry.ts / roi-term-stats.ts / brush-stroke.ts
+│ ├── roi-geometry.ts / roi-class-stats.ts / brush-stroke.ts
│ ├── image-info.ts / types.ts / utils.ts / wkt.ts / webgpu.ts / constants.ts
│ └── …
├── workers/
@@ -383,10 +383,10 @@ import {
| `useViewerContext` | 컨텍스트 (`rendererRef`, `worldToScreen`, …) |
| `WsiTileRenderer`, `M1TileRenderer`, `TileScheduler` | 코어 렌더러·타일 큐 |
| `normalizeImageInfo`, `toTileUrl`, `toRoiGeometry`, `parseWkt` | 이미지/ROI/WKT |
-| `buildTermPalette`, `calcScaleResolution`, `calcScaleLength`, `toBearerToken`, `clamp`, `hexToRgba`, `isSameViewState` | 유틸 |
+| `buildClassPalette`, `calcScaleResolution`, `calcScaleLength`, `toBearerToken`, `clamp`, `hexToRgba`, `isSameViewState` | 유틸 |
| `filterPointDataByPolygons`, `filterPointIndicesByPolygons`, `filterPointDataByPolygonsInWorker`, `filterPointIndicesByPolygonsInWorker`, `terminateRoiClipWorker`, `filterPointDataByPolygonsHybrid` | ROI 클리핑 |
| `buildPointSpatialIndexAsync`, `lookupCellIndex`, `terminatePointHitIndexWorker` | 포인트 공간 인덱스(워커) |
-| `computeRoiPointGroups` | ROI term 통계 |
+| `computeRoiPointGroups` | ROI class 통계 |
| `getWebGpuCapabilities`, `prefilterPointsByBoundsWebGpu` | WebGPU(실험) |
| `closeRing`, `createRectangle`, `createCircle` | 도형 |
| 타입 (`WsiViewerProps`, `WsiImageSource`, `WsiPointData`, `WsiViewState`, `DrawTool`, `PointHitEvent`, …) | TS |
diff --git a/benchmark/main.js b/benchmark/main.js
index 3510d0e..b8fa4c9 100644
--- a/benchmark/main.js
+++ b/benchmark/main.js
@@ -60,10 +60,10 @@ function initOP(canvas, data) {
const vs = `#version 300 es
precision highp float;
-in vec2 aPos; in uint aTerm;
+in vec2 aPos; in uint aClass;
uniform mat3 uCam; uniform float uSz;
flat out uint vT;
-void main(){ vec3 c=uCam*vec3(aPos,1.); gl_Position=vec4(c.xy,0.,1.); gl_PointSize=uSz; vT=aTerm; }`;
+void main(){ vec3 c=uCam*vec3(aPos,1.); gl_Position=vec4(c.xy,0.,1.); gl_PointSize=uSz; vT=aClass; }`;
const fs = `#version 300 es
precision highp float;
flat in uint vT;
@@ -100,7 +100,7 @@ void main(){
const tb = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, tb);
gl.bufferData(gl.ARRAY_BUFFER, data.idx, gl.STATIC_DRAW);
- const tl = gl.getAttribLocation(pg, "aTerm");
+ const tl = gl.getAttribLocation(pg, "aClass");
gl.enableVertexAttribArray(tl);
gl.vertexAttribIPointer(tl, 1, gl.UNSIGNED_SHORT, 0, 0);
const pt = gl.createTexture();
diff --git a/docs/en/api-reference.html b/docs/en/api-reference.html
index bb282f0..ecaf406 100644
--- a/docs/en/api-reference.html
+++ b/docs/en/api-reference.html
@@ -242,7 +242,7 @@
Legacy WsiViewerCanvas removed in v1.
Historical prop list: preserved in git history and in migration-1.4.0.md mapping tables.
- ROI term groups: call computeRoiPointGroups() from app code instead of onRoiPointGroups. customLayers has no direct replacement — use useViewerContext + a host React overlay.
+ ROI class groups: call computeRoiPointGroups() from app code instead of onRoiPointGroups. customLayers has no direct replacement — use useViewerContext + a host React overlay.
@@ -299,10 +299,15 @@ Core types
maxTierZoom: number;
tilePath: string;
tileBaseUrl: string;
- terms: WsiTerm[];
tileUrlBuilder?: (tier: number, x: number, y: number) => string;
}
+interface WsiClass {
+ classId: string;
+ className: string;
+ classColor: string;
+}
+
interface WsiPointData {
count: number;
positions: Float32Array;
@@ -374,14 +379,15 @@ Utility exports
normalizeImageInfo(raw, tileBaseUrl)Converts backend payload + tile base URL to WsiImageSource.
toTileUrl(source, tier, x, y)Builds IMS tile URL.
- buildTermPalette(terms)Builds termId to palette mapping and RGBA texture.
+ normalizeImageClasses(raw)Normalizes class metadata from backend payloads.
+ buildClassPalette(classes)Builds classId to palette mapping and RGBA texture.
filterPointDataByPolygons(data, polygons)Filters points inside ROI polygons only.
filterPointDataByPolygonsInWorker(data, polygons)Runs ROI clipping in a dedicated worker thread.
terminateRoiClipWorker()Terminates the shared ROI clip worker instance (useful on teardown/tests).
filterPointIndicesByPolygons(data, polygons)Returns original point indices inside polygons for patch JSON export pipelines.
filterPointIndicesByPolygonsInWorker(data, polygons)Worker variant returning point indices + timing metadata.
filterPointDataByPolygonsHybrid(data, polygons, options?)Experimental hybrid clipping (WebGPU bbox prefilter + exact polygon test). Use { bridgeToDraw: true } to return full buffers + drawIndices bridge payload.
- computeRoiPointGroups(pointData, regions, options)Computes per-ROI term counts (roiPointGroups-style) from typed point buffers.
+ computeRoiPointGroups(pointData, regions, options)Computes per-ROI class counts (roiPointGroups-style) from typed point buffers.
getWebGpuCapabilities()Returns WebGPU support/adapter/features for runtime decisions.
prefilterPointsByBoundsWebGpu(positions, count, bounds)Experimental compute pass to classify points by ROI bounding boxes.
calcScaleResolution(imageMpp, imageZoom, currentZoom)Returns microns-per-screen-pixel for the current zoom.
diff --git a/docs/en/architecture.html b/docs/en/architecture.html
index bf895ef..909dfb3 100644
--- a/docs/en/architecture.html
+++ b/docs/en/architecture.html
@@ -117,7 +117,7 @@ Point pipeline
GPU upload: positions and palette index buffers.
Shader render: palette-based point fragments in ring/solid mode via fillModes.
ROI filter: optional polygon clip before upload/render via clipMode.
- ROI stats: optional per-ROI term aggregation via computeRoiPointGroups / onRoiPointGroups.
+ ROI stats: optional per-ROI class aggregation via computeRoiPointGroups / onRoiPointGroups.
@@ -160,7 +160,7 @@ WebGPU expansion path (compute-focused)
ROI culling Started with bbox prefilter compute pass, then exact polygon phase.
LOD aggregation Low-zoom density aggregation and binning on GPU.
-
Term histogram ROI-level term counts and positivity stats in parallel.
+
Class histogram ROI-level class counts and positivity stats in parallel.
Interop Pack compute output for direct WebGL buffer upload.
diff --git a/docs/en/draw-and-roi.html b/docs/en/draw-and-roi.html
index e3e8af6..fb009ea 100644
--- a/docs/en/draw-and-roi.html
+++ b/docs/en/draw-and-roi.html
@@ -42,7 +42,7 @@
ROI Draw
Persisted regions
Custom styles
Point clipping
-
ROI term stats
+
ROI class stats
@@ -293,13 +293,13 @@ 7. Render cells only inside drawn regions
- 8. ROI term-count stats API
+ 8. ROI class-count stats API
import { computeRoiPointGroups } from "open-plant";
const stats = computeRoiPointGroups(pointData, regions, {
- paletteIndexToTermId: ["bg", "negative", "positive"],
+ paletteIndexToClassId: ["bg", "negative", "positive"],
});
-// stats.groups -> [{ regionId, totalCount, termCounts[] }]
+// stats.groups -> [{ regionId, totalCount, classCounts[] }]
Call computeRoiPointGroups when pointData or regions change — it replaces the removed onRoiPointGroups prop from legacy WsiViewerCanvas.
diff --git a/docs/en/getting-started.html b/docs/en/getting-started.html
index 477a1ab..849bbe9 100644
--- a/docs/en/getting-started.html
+++ b/docs/en/getting-started.html
@@ -41,7 +41,7 @@ Quick Start
Camera + Color
Load Points
ROI Clip
- ROI Term Stats
+ ROI Class Stats
ROI Acceleration
@@ -138,12 +138,12 @@ 5. Provide point data and build palette
Point loading and parsing (e.g. ZST/MVT decoding) is not part of the library.
Parse points externally, then pass typed arrays to the viewer.
- import { buildTermPalette } from "open-plant";
+ import { buildClassPalette } from "open-plant";
// positions: Float32Array [x0,y0,x1,y1,...] (your own loader)
-// paletteIndices: Uint16Array (mapped from your term table)
+// paletteIndices: Uint16Array (mapped from your class table)
-const termPalette = buildTermPalette(source.terms);
+const classPalette = buildClassPalette(classes);
const pointData = {
count: positions.length / 2,
@@ -155,7 +155,7 @@ Composition API
import { WsiViewer, PointLayer } from "open-plant";
<WsiViewer source={source} authToken={toBearerToken(token)}>
- <PointLayer data={pointData} palette={termPalette.colors} />
+ <PointLayer data={pointData} palette={classPalette.colors} />
</WsiViewer>
@@ -178,7 +178,7 @@ 6. ROI polygon clipping
<WsiViewer source={source}>
<PointLayer
data={pointData}
- palette={termPalette.colors}
+ palette={classPalette.colors}
clipEnabled
clipToRegions={roiRegions}
/>
@@ -187,16 +187,16 @@ 6. ROI polygon clipping
- 7. ROI term stats
+ 7. ROI class stats
onRoiPointGroups was removed with WsiViewerCanvas. Call computeRoiPointGroups when your data changes:
import { computeRoiPointGroups } from "open-plant";
const stats = computeRoiPointGroups(pointData, regions, {
- paletteIndexToTermId: new Map([[1, "negative"], [2, "positive"]]),
+ paletteIndexToClassId: new Map([[1, "negative"], [2, "positive"]]),
});
-// stats.groups -> per-ROI term counts
+// stats.groups -> per-ROI class counts
@@ -208,7 +208,7 @@ 8. ROI acceleration mode (worker / hybrid-webgpu)
<WsiViewer source={source}>
<PointLayer
data={pointData}
- palette={termPalette.colors}
+ palette={classPalette.colors}
clipEnabled
clipToRegions={regions}
clipMode={clipMode}
diff --git a/docs/en/index.html b/docs/en/index.html
index ab9ee23..42d4218 100644
--- a/docs/en/index.html
+++ b/docs/en/index.html
@@ -69,7 +69,7 @@ Tile Renderer
Point Renderer
-
Accepts pre-parsed typed arrays, term-color palette texture rendering, and ROI clipping.
+
Accepts pre-parsed typed arrays, class-color palette texture rendering, and ROI clipping.
Draw Layer
diff --git a/docs/en/migration-guide.html b/docs/en/migration-guide.html
index 4de9338..7001663 100644
--- a/docs/en/migration-guide.html
+++ b/docs/en/migration-guide.html
@@ -174,7 +174,7 @@
New DrawTool types
No direct layer props (app-side)
customLayers → useViewerContext + host React overlay
- onRoiPointGroups / roiPaletteIndexToTermId → computeRoiPointGroups()
+ onRoiPointGroups / roiPaletteIndexToClassId → computeRoiPointGroups()
diff --git a/docs/ko/api-reference.html b/docs/ko/api-reference.html
index ec2eb1f..bdea7b8 100644
--- a/docs/ko/api-reference.html
+++ b/docs/ko/api-reference.html
@@ -245,7 +245,7 @@
레거시 WsiViewerCanvas v1.4.4+ 에
과거 Props 표: git 히스토리 및 migration-1.4.0.md 매핑표에 보존됩니다.
- ROI term 통계: onRoiPointGroups 대신 앱 코드에서 computeRoiPointGroups()를 호출하세요. customLayers는 직접 대체 없음 — useViewerContext + 호스트 React 오버레이를 사용하세요.
+ ROI class 통계: onRoiPointGroups 대신 앱 코드에서 computeRoiPointGroups()를 호출하세요. customLayers는 직접 대체 없음 — useViewerContext + 호스트 React 오버레이를 사용하세요.
@@ -306,10 +306,15 @@ 핵심 타입
maxTierZoom: number;
tilePath: string;
tileBaseUrl: string;
- terms: WsiTerm[];
tileUrlBuilder?: (tier: number, x: number, y: number) => string;
}
+interface WsiClass {
+ classId: string;
+ className: string;
+ classColor: string;
+}
+
interface WsiPointData {
count: number;
positions: Float32Array; // [x0,y0,x1,y1,...]
@@ -383,14 +388,15 @@ 유틸 함수
normalizeImageInfo(raw, tileBaseUrl)백엔드 응답 + 타일 베이스 URL을 `WsiImageSource`로 변환.
toTileUrl(source, tier, x, y)IMS 타일 URL 생성.
- buildTermPalette(terms)termId → palette index 매핑 + RGBA 팔레트 생성.
+ normalizeImageClasses(raw)백엔드 payload에서 class 메타데이터를 정규화합니다.
+ buildClassPalette(classes)classId → palette index 매핑 + RGBA 팔레트 생성.
filterPointDataByPolygons(data, polygons)ROI 다각형 기반 포인트 필터링.
filterPointDataByPolygonsInWorker(data, polygons)ROI 필터를 워커 스레드에서 실행합니다.
terminateRoiClipWorker()공유 ROI clip worker 인스턴스를 종료합니다(teardown/테스트에서 유용).
filterPointIndicesByPolygons(data, polygons)polygon 내부 원본 point 인덱스를 반환해 patch JSON export 파이프라인을 구성합니다.
filterPointIndicesByPolygonsInWorker(data, polygons)point 인덱스 반환의 워커 버전(+duration 메타).
filterPointDataByPolygonsHybrid(data, polygons, options?)실험적 하이브리드 경로(WebGPU bbox prefilter + polygon 정밀 판정). { bridgeToDraw: true }를 주면 전체 버퍼 + drawIndices 브리지를 반환합니다.
- computeRoiPointGroups(pointData, regions, options)TypedArray 포인트 버퍼로 ROI별 term count(roiPointGroups)를 계산합니다.
+ computeRoiPointGroups(pointData, regions, options)TypedArray 포인트 버퍼로 ROI별 class count(roiPointGroups)를 계산합니다.
getWebGpuCapabilities()런타임에서 WebGPU 지원/어댑터/기능 정보를 조회합니다.
prefilterPointsByBoundsWebGpu(positions, count, bounds)ROI bounding box 기준 점 분류를 WebGPU compute로 수행합니다(실험적).
calcScaleResolution(imageMpp, imageZoom, currentZoom)현재 줌 기준 μm/스크린픽셀 해상도 계산.
diff --git a/docs/ko/architecture.html b/docs/ko/architecture.html
index 5892147..fa56454 100644
--- a/docs/ko/architecture.html
+++ b/docs/ko/architecture.html
@@ -120,7 +120,7 @@ 포인트 파이프라인
업로드: positions/paletteIndices를 GPU 버퍼로 전송.
셰이더 렌더: fillModes 기반 ring/solid 모드로 palette 색상을 렌더합니다.
ROI clip: 필요 시 clipMode에 따라 polygon 내부 포인트만 필터링.
- ROI 통계: computeRoiPointGroups / onRoiPointGroups로 ROI별 term 집계.
+ ROI 통계: computeRoiPointGroups / onRoiPointGroups로 ROI별 class 집계.
@@ -170,8 +170,8 @@ LOD Aggregation
저배율에서 cell density 집계를 compute pass로 사전 생성.
-
Term Histogram
-
ROI 단위 term count/positivity 통계를 GPU에서 병렬 계산.
+
Class Histogram
+
ROI 단위 class count/positivity 통계를 GPU에서 병렬 계산.
Interop
diff --git a/docs/ko/draw-and-roi.html b/docs/ko/draw-and-roi.html
index a00122e..72d85c8 100644
--- a/docs/ko/draw-and-roi.html
+++ b/docs/ko/draw-and-roi.html
@@ -42,7 +42,7 @@
ROI 그리기
영역 유지/라벨
스타일 커스터마이징
ROI 기반 포인트 필터
-
ROI term 통계
+
ROI class 통계
@@ -302,13 +302,13 @@ 7. Draw 영역에만 셀 렌더
- 8. ROI term count 통계 API
+ 8. ROI class count 통계 API
import { computeRoiPointGroups } from "open-plant";
const stats = computeRoiPointGroups(pointData, regions, {
- paletteIndexToTermId: ["bg", "negative", "positive"],
+ paletteIndexToClassId: ["bg", "negative", "positive"],
});
-// stats.groups -> [{ regionId, totalCount, termCounts[] }]
+// stats.groups -> [{ regionId, totalCount, classCounts[] }]
pointData나 regions가 바뀔 때 computeRoiPointGroups를 호출하세요. 레거시 WsiViewerCanvas의 onRoiPointGroups를 대체합니다.
diff --git a/docs/ko/getting-started.html b/docs/ko/getting-started.html
index 825cb2f..ab08d4a 100644
--- a/docs/ko/getting-started.html
+++ b/docs/ko/getting-started.html
@@ -41,7 +41,7 @@ 빠른 시작
카메라 + 색상 보정
포인트 로드/팔레트
ROI clip 적용
- ROI term 통계
+ ROI class 통계
ROI 가속 모드
@@ -143,17 +143,17 @@ 4. 카메라 제한/전환 + 타일 색상 보정
- 5. 포인트 데이터 전달 + term 팔레트 매핑
+ 5. 포인트 데이터 전달 + class 팔레트 매핑
포인트 로딩/파싱(ZST/MVT 디코딩 등)은 라이브러리 외부 에서 처리합니다.
파싱 완료된 TypedArray를 뷰어에 전달하세요.
- import { buildTermPalette } from "open-plant";
+ import { buildClassPalette } from "open-plant";
// positions: Float32Array [x0,y0,x1,y1,...] (직접 구현한 로더)
-// paletteIndices: Uint16Array (term 테이블에서 매핑)
+// paletteIndices: Uint16Array (class 테이블에서 매핑)
-const termPalette = buildTermPalette(source.terms);
+const classPalette = buildClassPalette(classes);
const pointData = {
count: positions.length / 2,
@@ -165,7 +165,7 @@ 컴포지션 API
import { WsiViewer, PointLayer } from "open-plant";
<WsiViewer source={source} authToken={toBearerToken(token)}>
- <PointLayer data={pointData} palette={termPalette.colors} />
+ <PointLayer data={pointData} palette={classPalette.colors} />
</WsiViewer>
@@ -188,7 +188,7 @@ 6. ROI 폴리곤 클리핑
<WsiViewer source={source}>
<PointLayer
data={pointData}
- palette={termPalette.colors}
+ palette={classPalette.colors}
clipEnabled
clipToRegions={roiRegions}
/>
@@ -197,16 +197,16 @@ 6. ROI 폴리곤 클리핑
- 7. ROI term 통계
+ 7. ROI class 통계
onRoiPointGroups는 WsiViewerCanvas와 함께 제거되었습니다. 데이터가 바뀔 때 computeRoiPointGroups를 호출하세요.
import { computeRoiPointGroups } from "open-plant";
const stats = computeRoiPointGroups(pointData, regions, {
- paletteIndexToTermId: new Map([[1, "negative"], [2, "positive"]]),
+ paletteIndexToClassId: new Map([[1, "negative"], [2, "positive"]]),
});
-// stats.groups -> ROI별 term count
+// stats.groups -> ROI별 class count
@@ -218,7 +218,7 @@ 8. ROI 가속 모드(worker / hybrid-webgpu)
<WsiViewer source={source}>
<PointLayer
data={pointData}
- palette={termPalette.colors}
+ palette={classPalette.colors}
clipEnabled
clipToRegions={regions}
clipMode={clipMode}
diff --git a/docs/ko/index.html b/docs/ko/index.html
index c402640..63b22f2 100644
--- a/docs/ko/index.html
+++ b/docs/ko/index.html
@@ -76,7 +76,7 @@ Tile Renderer
Point Renderer
- 파싱된 TypedArray 수신, term 팔레트 텍스처 렌더링, 줌 레벨 기반 링 크기
+ 파싱된 TypedArray 수신, class 팔레트 텍스처 렌더링, 줌 레벨 기반 링 크기
보간, ROI clip.
diff --git a/docs/ko/migration-guide.html b/docs/ko/migration-guide.html
index 4251feb..12d0efb 100644
--- a/docs/ko/migration-guide.html
+++ b/docs/ko/migration-guide.html
@@ -174,7 +174,7 @@ 신규 DrawTool 타입
레이어에 없는 기능(앱에서 처리)
customLayers → useViewerContext + 호스트 React 오버레이
- onRoiPointGroups / roiPaletteIndexToTermId → computeRoiPointGroups()
+ onRoiPointGroups / roiPaletteIndexToClassId → computeRoiPointGroups()
diff --git a/docs/migration-1.4.0.md b/docs/migration-1.4.0.md
index 6f13f07..44661a2 100644
--- a/docs/migration-1.4.0.md
+++ b/docs/migration-1.4.0.md
@@ -121,7 +121,7 @@ v1.4.0에서 컴포지션 API가 도입되었고, 당시에는 `WsiViewerCanvas`
| 기존 prop | 대안 (v1.4.4) |
|---|---|
| `customLayers` | `useViewerContext()` + 호스트 React 오버레이 (`worldToScreen` 등) |
-| `onRoiPointGroups` / `roiPaletteIndexToTermId` | `computeRoiPointGroups(pointData, regions, options)` 를 뷰어 밖에서 호출 |
+| `onRoiPointGroups` / `roiPaletteIndexToClassId` | `computeRoiPointGroups(pointData, regions, options)` 를 뷰어 밖에서 호출 |
`WsiViewerCanvas`는 패키지에서 제거되었으므로, 위 기능은 **반드시** 새 API 또는 유틸로 이전해야 합니다.
diff --git a/docs/prompts/refactoring-review.md b/docs/prompts/refactoring-review.md
index 5ca6610..46579c1 100644
--- a/docs/prompts/refactoring-review.md
+++ b/docs/prompts/refactoring-review.md
@@ -101,7 +101,7 @@
- `wsi/time-utils.ts` (`nowMs`)
- `wsi/view-utils.ts` (`isSameViewState`, scale 관련)
-- `wsi/color-utils.ts` (`hexToRgba`, `buildTermPalette`)
+- `wsi/color-utils.ts` (`hexToRgba`, `buildClassPalette`)
- `wsi/auth-utils.ts` (`toBearerToken`)
완료 기준:
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 8e0bf3f..ba12f69 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -24,6 +24,7 @@ import {
WsiViewer,
} from "../../src";
import { DrawToolbar } from "./components/DrawToolbar";
+import { ClassColorControls } from "./components/ClassColorControls";
import { PointControls } from "./components/PointControls";
import { StatusBar } from "./components/StatusBar";
import { StatusOverlay } from "./components/StatusOverlay";
@@ -79,27 +80,27 @@ function ViewerOverviewMap({ authToken, show, options }: { authToken: string; sh
return ;
}
-function resolvePositivePaletteIndex(terms: { termId?: string | null; termName?: string | null }[] | undefined, termToPaletteIndex: Map): number {
- if (!Array.isArray(terms) || terms.length === 0) {
+function resolvePositivePaletteIndex(classes: { classId?: string | null; className?: string | null }[] | undefined, classToPaletteIndex: Map): number {
+ if (!Array.isArray(classes) || classes.length === 0) {
return 0;
}
let fallback = 0;
- for (let i = 0; i < terms.length; i += 1) {
- const term = terms[i];
- const paletteIndex = termToPaletteIndex.get(String(term?.termId ?? "")) ?? 0;
+ for (let i = 0; i < classes.length; i += 1) {
+ const item = classes[i];
+ const paletteIndex = classToPaletteIndex.get(String(item?.classId ?? "")) ?? 0;
if (!paletteIndex) continue;
- const termId = String(term?.termId ?? "")
+ const classId = String(item?.classId ?? "")
.trim()
.toLowerCase();
- const termName = String(term?.termName ?? "")
+ const className = String(item?.className ?? "")
.trim()
.toLowerCase();
- if (!fallback && (termName.includes("positive") || termId === "positive" || termId === "pos" || termId === "4" || termId === "p")) {
+ if (!fallback && (className.includes("positive") || classId === "positive" || classId === "pos" || classId === "4" || classId === "p")) {
fallback = paletteIndex;
}
- if (termName === "positive" || termName === "ki-67 positive") {
+ if (className === "positive" || className === "ki-67 positive") {
return paletteIndex;
}
}
@@ -198,10 +199,10 @@ export default function App() {
}, [resetInteraction]);
const imageLoader = useImageLoader(bearerToken, onResetAll);
- const { source } = imageLoader;
+ const { source, classes } = imageLoader;
- const pointData = usePointLoader(source, imageLoader.pointZstUrl, bearerToken);
- const draw = useDrawState(source, pointData.pointPayload);
+ const pointData = usePointLoader(source, classes, imageLoader.pointZstUrl, bearerToken);
+ const draw = useDrawState(source, classes, pointData.pointPayload);
const viewer = useViewerControls(source);
const currentHeatmapZoom = useMemo(() => {
if (!source) return undefined;
@@ -347,7 +348,7 @@ export default function App() {
[]
);
- const positivePaletteIndex = useMemo(() => resolvePositivePaletteIndex(source?.terms, pointData.termPalette.termToPaletteIndex), [source, pointData.termPalette.termToPaletteIndex]);
+ const positivePaletteIndex = useMemo(() => resolvePositivePaletteIndex(classes, pointData.classPalette.classToPaletteIndex), [classes, pointData.classPalette.classToPaletteIndex]);
const positiveHeatmapData = useMemo(() => {
const payload = pointData.pointPayload;
@@ -470,6 +471,12 @@ export default function App() {
onInnerBlackFillChange={setPointInnerBlackFill}
/>
+
+
@@ -500,7 +507,7 @@ export default function App() {
>
void;
+}
+
+function toColorInputValue(color: string): string {
+ return /^#([0-9a-f]{6})$/i.test(color) ? color : "#808080";
+}
+
+export function ClassColorControls({ classes, disabled = false, onClassColorChange }: ClassColorControlsProps) {
+ if (!classes.length) return null;
+
+ return (
+
+
Class Colors
+
+ {classes.map(item => (
+
+
+
+ {item.className || "(unnamed)"}
+ {item.classId || "-"}
+
+ onClassColorChange(item.classId, event.target.value)}
+ />
+
+ ))}
+
+
+ );
+}
diff --git a/example/src/components/StatusBar.tsx b/example/src/components/StatusBar.tsx
index 7b5b995..9ff8346 100644
--- a/example/src/components/StatusBar.tsx
+++ b/example/src/components/StatusBar.tsx
@@ -20,7 +20,7 @@ export function StatusBar({ error, imageSummary, scaleSummary, pointStatus, webG
? `points warn: ${pointStatus.error}`
: pointStatus.loading
? "points loading..."
- : `points ${pointStatus.count.toLocaleString()} | terms ${pointStatus.terms} | nt ${pointStatus.hasNt ? "yes" : "no"} | stain ${pointStatus.hasPositivityRank ? "yes" : "no"}`}
+ : `points ${pointStatus.count.toLocaleString()} | classes ${pointStatus.classes} | nt ${pointStatus.hasNt ? "yes" : "no"} | stain ${pointStatus.hasPositivityRank ? "yes" : "no"}`}
diff --git a/example/src/hooks/useDrawState.ts b/example/src/hooks/useDrawState.ts
index 8ce3342..7da30b7 100644
--- a/example/src/hooks/useDrawState.ts
+++ b/example/src/hooks/useDrawState.ts
@@ -5,12 +5,14 @@ import {
type DrawTool,
filterPointIndicesByPolygons,
type PatchDrawResult,
+ type WsiClass,
type WsiPointData,
type WsiRegion,
} from "../../../src";
export function useDrawState(
- source: { id: string; name: string; width: number; height: number; terms: { termId: string; termName: string; termColor: string }[] } | null,
+ source: { id: string; name: string; width: number; height: number } | null,
+ classes: WsiClass[],
pointPayload: WsiPointData | null,
) {
const [drawTool, setDrawTool] = useState
("cursor");
@@ -125,10 +127,10 @@ export function useDrawState(
areaPx: lastPatch.areaPx,
},
images: [{ id: source.id, name: source.name, width: source.width, height: source.height }],
- categories: source.terms.map(term => ({
- id: term.termId,
- name: term.termName,
- color: term.termColor,
+ categories: classes.map(item => ({
+ id: item.classId,
+ name: item.className,
+ color: item.classColor,
})),
annotations,
};
@@ -140,7 +142,7 @@ export function useDrawState(
a.download = `patch-${source.id}-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
- }, [source, pointPayload, lastPatch, lastPatchIndices]);
+ }, [source, classes, pointPayload, lastPatch, lastPatchIndices]);
const handleStampRectChange = useCallback((e: ChangeEvent) => {
const next = Number(e.target.value);
diff --git a/example/src/hooks/useImageLoader.ts b/example/src/hooks/useImageLoader.ts
index 6535085..34c3d45 100644
--- a/example/src/hooks/useImageLoader.ts
+++ b/example/src/hooks/useImageLoader.ts
@@ -1,12 +1,13 @@
import { useCallback, useEffect, useRef, useState } from "react";
-import { normalizeImageInfo, type WsiImageSource } from "../../../src";
+import { normalizeImageClasses, normalizeImageInfo, type WsiClass, type WsiImageSource } from "../../../src";
import { DEFAULT_INFO_URL, S3_BASE_URL } from "../utils/constants";
-import { createDemoSource } from "../utils/demo-source";
+import { createDemoClasses, createDemoSource } from "../utils/demo-source";
export interface ImageLoaderState {
loading: boolean;
error: string;
source: WsiImageSource | null;
+ classes: WsiClass[];
pointZstUrl: string;
fitNonce: number;
}
@@ -14,6 +15,7 @@ export interface ImageLoaderState {
export interface ImageLoaderActions {
loadImageInfo: (url: string) => void;
loadDemo: () => void;
+ updateClassColor: (classId: string, color: string) => void;
setFitNonce: React.Dispatch>;
}
@@ -26,6 +28,7 @@ export function useImageLoader(
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [source, setSource] = useState(null);
+ const [classes, setClasses] = useState([]);
const [pointZstUrl, setPointZstUrl] = useState("");
const [fitNonce, setFitNonce] = useState(0);
@@ -33,7 +36,9 @@ export function useImageLoader(
setLoading(true);
setError("");
const demoSource = createDemoSource();
+ const demoClasses = createDemoClasses();
setSource(demoSource);
+ setClasses(demoClasses);
setPointZstUrl("/sample/10000000cells.zst");
setFitNonce(prev => prev + 1);
onReset();
@@ -46,6 +51,7 @@ export function useImageLoader(
if (!trimmedUrl) {
setError("image info URL이 비어 있습니다.");
setSource(null);
+ setClasses([]);
return;
}
@@ -69,13 +75,16 @@ export function useImageLoader(
return `${tileBaseUrl}${p}/${tier}/${y}_${x}.webp`;
},
}, `${S3_BASE_URL}/ims`);
+ const nextClasses = normalizeImageClasses(raw);
setSource(nextSource);
+ setClasses(nextClasses);
setPointZstUrl(raw?.mvtPath ? String(raw.mvtPath) : "");
setFitNonce(prev => prev + 1);
onReset();
})
.catch((err: Error) => {
setSource(null);
+ setClasses([]);
setPointZstUrl("");
setError(err.message || "알 수 없는 오류");
onReset();
@@ -87,6 +96,14 @@ export function useImageLoader(
[bearerToken, onReset],
);
+ const updateClassColor = useCallback((classId: string, color: string): void => {
+ const nextColor = String(color || "").trim();
+ if (!classId || !nextColor) return;
+ setClasses(prev =>
+ prev.map(item => (item.classId === classId ? { ...item, classColor: nextColor } : item))
+ );
+ }, []);
+
useEffect(() => {
if (initialLoadDoneRef.current) return;
initialLoadDoneRef.current = true;
@@ -97,5 +114,5 @@ export function useImageLoader(
}
}, [loadImageInfo, loadDemo]);
- return { loading, error, source, pointZstUrl, fitNonce, loadImageInfo, loadDemo, setFitNonce };
+ return { loading, error, source, classes, pointZstUrl, fitNonce, loadImageInfo, loadDemo, updateClassColor, setFitNonce };
}
diff --git a/example/src/hooks/usePointLoader.ts b/example/src/hooks/usePointLoader.ts
index 90a524d..b2c2484 100644
--- a/example/src/hooks/usePointLoader.ts
+++ b/example/src/hooks/usePointLoader.ts
@@ -1,14 +1,14 @@
import { useEffect, useMemo, useState } from "react";
-import { buildTermPalette, type WsiImageSource, type WsiPointData } from "../../../src";
+import { buildClassPalette, type WsiClass, type WsiImageSource, type WsiPointData } from "../../../src";
import { S3_BASE_URL } from "../utils/constants";
-import { createTermAliasResolver } from "../utils/term-resolver";
+import { createClassAliasResolver } from "../utils/class-resolver";
import { type LoadedPointData, loadPointsFromZst } from "../point-loader";
export interface PointStatus {
loading: boolean;
error: string;
count: number;
- terms: number;
+ classes: number;
hasNt: boolean;
hasPositivityRank: boolean;
}
@@ -17,23 +17,23 @@ export const INITIAL_POINT_STATUS: PointStatus = {
loading: false,
error: "",
count: 0,
- terms: 0,
+ classes: 0,
hasNt: false,
hasPositivityRank: false,
};
export function usePointLoader(
source: WsiImageSource | null,
+ classes: WsiClass[],
pointZstUrl: string,
bearerToken: string,
) {
const [pointPayload, setPointPayload] = useState(null);
const [pointStatus, setPointStatus] = useState(INITIAL_POINT_STATUS);
- const termPalette = useMemo(() => {
- if (!source?.terms?.length) return buildTermPalette([]);
- return buildTermPalette(source.terms);
- }, [source]);
+ const classPalette = useMemo(() => {
+ return buildClassPalette(classes);
+ }, [classes]);
useEffect(() => {
if (!pointZstUrl || !source) {
@@ -42,19 +42,20 @@ export function usePointLoader(
...prev,
loading: false,
count: 0,
- terms: source?.terms?.length || 0,
+ classes: classes.length,
}));
return;
}
let cancelled = false;
const currentSource = source;
+ const currentClasses = classes;
setPointStatus({
loading: true,
error: "",
count: 0,
- terms: currentSource.terms.length,
+ classes: currentClasses.length,
hasNt: false,
hasPositivityRank: false,
});
@@ -69,24 +70,24 @@ export function usePointLoader(
.then((result: LoadedPointData) => {
if (cancelled) return;
- const localTermIndex = result.localTermIndex || new Uint16Array(0);
- const termTable = Array.isArray(result.termTable) ? result.termTable : [""];
- const resolveTermPaletteIndex = createTermAliasResolver(currentSource.terms, termPalette.termToPaletteIndex);
+ const localClassIndex = result.localClassIndex || new Uint16Array(0);
+ const classTable = Array.isArray(result.classTable) ? result.classTable : [""];
+ const resolveClassPaletteIndex = createClassAliasResolver(currentClasses, classPalette.classToPaletteIndex);
- const lut = new Uint16Array(Math.max(1, termTable.length));
- const unmatchedTerms: string[] = [];
- for (let i = 0; i < termTable.length; i += 1) {
- const key = String(termTable[i] ?? "");
- const mapped = resolveTermPaletteIndex(key);
+ const lut = new Uint16Array(Math.max(1, classTable.length));
+ const unmatchedClasses: string[] = [];
+ for (let i = 0; i < classTable.length; i += 1) {
+ const key = String(classTable[i] ?? "");
+ const mapped = resolveClassPaletteIndex(key);
lut[i] = mapped;
if (mapped === 0 && key && key !== "0") {
- unmatchedTerms.push(key);
+ unmatchedClasses.push(key);
}
}
- const paletteIndices = new Uint16Array(localTermIndex.length);
- for (let i = 0; i < localTermIndex.length; i += 1) {
- paletteIndices[i] = lut[localTermIndex[i]] ?? 0;
+ const paletteIndices = new Uint16Array(localClassIndex.length);
+ for (let i = 0; i < localClassIndex.length; i += 1) {
+ paletteIndices[i] = lut[localClassIndex[i]] ?? 0;
}
const ids = new Uint32Array(result.count);
for (let i = 0; i < ids.length; i += 1) {
@@ -102,11 +103,11 @@ export function usePointLoader(
setPointStatus({
loading: false,
- error: unmatchedTerms.length
- ? `term unmatched: ${unmatchedTerms.slice(0, 5).join(", ")}${unmatchedTerms.length > 5 ? " ..." : ""}`
+ error: unmatchedClasses.length
+ ? `class unmatched: ${unmatchedClasses.slice(0, 5).join(", ")}${unmatchedClasses.length > 5 ? " ..." : ""}`
: "",
count: result.count || 0,
- terms: termTable.length,
+ classes: classTable.length,
hasNt: Boolean(result.hasNt),
hasPositivityRank: Boolean(result.hasPositivityRank),
});
@@ -118,7 +119,7 @@ export function usePointLoader(
loading: false,
error: err.message || "point load failed",
count: 0,
- terms: 0,
+ classes: 0,
hasNt: false,
hasPositivityRank: false,
});
@@ -127,12 +128,12 @@ export function usePointLoader(
return () => {
cancelled = true;
};
- }, [pointZstUrl, source, bearerToken, termPalette]);
+ }, [pointZstUrl, source, classes, bearerToken, classPalette]);
const reset = () => {
setPointPayload(null);
setPointStatus(INITIAL_POINT_STATUS);
};
- return { pointPayload, pointStatus, termPalette, reset };
+ return { pointPayload, pointStatus, classPalette, reset };
}
diff --git a/example/src/point-loader.ts b/example/src/point-loader.ts
index ea0b98a..f4acd1c 100644
--- a/example/src/point-loader.ts
+++ b/example/src/point-loader.ts
@@ -1,10 +1,10 @@
export interface LoadedPointData {
count: number;
- termTable: string[];
+ classTable: string[];
hasNt: boolean;
hasPositivityRank: boolean;
positions: Float32Array;
- localTermIndex: Uint16Array;
+ localClassIndex: Uint16Array;
}
interface LoadPointsFromZstOptions {
diff --git a/example/src/styles.css b/example/src/styles.css
index 345e428..c3e19a2 100644
--- a/example/src/styles.css
+++ b/example/src/styles.css
@@ -165,6 +165,61 @@ button:disabled {
color: var(--muted);
}
+.class-color-panel {
+ align-items: flex-start;
+}
+
+.class-color-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 8px 10px;
+ width: min(100%, 760px);
+}
+
+.class-color-row {
+ display: grid;
+ grid-template-columns: 14px minmax(0, 1fr) 42px;
+ align-items: center;
+ gap: 8px;
+ min-width: 180px;
+}
+
+.class-color-swatch {
+ width: 14px;
+ height: 14px;
+ border-radius: 999px;
+ border: 1px solid rgba(255, 255, 255, 0.25);
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.18);
+}
+
+.class-color-label {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.class-color-label strong {
+ font-size: 12px;
+ color: var(--text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.class-color-label small {
+ font-size: 11px;
+ color: var(--muted);
+}
+
+.class-color-input {
+ width: 42px;
+ height: 28px;
+ border: 1px solid #3a526d;
+ border-radius: 6px;
+ background: #0f1a28;
+ padding: 2px;
+}
+
button.active {
background: #3d6388;
border-color: #7ea5cc;
diff --git a/example/src/utils/term-resolver.ts b/example/src/utils/class-resolver.ts
similarity index 63%
rename from example/src/utils/term-resolver.ts
rename to example/src/utils/class-resolver.ts
index a6bb93d..436fa78 100644
--- a/example/src/utils/term-resolver.ts
+++ b/example/src/utils/class-resolver.ts
@@ -1,4 +1,4 @@
-import type { WsiTerm } from "../../../src";
+import type { WsiClass } from "../../../src";
function normalizeKey(value: string | null | undefined): string {
return String(value || "")
@@ -6,30 +6,30 @@ function normalizeKey(value: string | null | undefined): string {
.toLowerCase();
}
-export function createTermAliasResolver(
- terms: WsiTerm[],
- termToPaletteIndex: Map,
+export function createClassAliasResolver(
+ classes: WsiClass[],
+ classToPaletteIndex: Map,
): (rawValue: string | number | null | undefined) => number {
- const direct = termToPaletteIndex;
+ const direct = classToPaletteIndex;
const alias = new Map();
let positivePaletteIndex = 0;
let negativePaletteIndex = 0;
- for (const term of terms) {
- const termId = String(term.termId ?? "");
- const paletteIndex = direct.get(termId) ?? 0;
+ for (const item of classes) {
+ const classId = String(item.classId ?? "");
+ const paletteIndex = direct.get(classId) ?? 0;
if (!paletteIndex) continue;
- const termName = normalizeKey(term.termName);
- if (termName) {
- alias.set(termName, paletteIndex);
+ const className = normalizeKey(item.className);
+ if (className) {
+ alias.set(className, paletteIndex);
}
- if (!positivePaletteIndex && termName.includes("positive")) {
+ if (!positivePaletteIndex && className.includes("positive")) {
positivePaletteIndex = paletteIndex;
}
- if (!negativePaletteIndex && termName.includes("negative")) {
+ if (!negativePaletteIndex && className.includes("negative")) {
negativePaletteIndex = paletteIndex;
}
}
diff --git a/example/src/utils/demo-source.ts b/example/src/utils/demo-source.ts
index 5fd111b..56211b8 100644
--- a/example/src/utils/demo-source.ts
+++ b/example/src/utils/demo-source.ts
@@ -1,4 +1,4 @@
-import type { WsiImageSource } from "../../../src";
+import type { WsiClass, WsiImageSource } from "../../../src";
const _cache = new Map();
let _canvas: HTMLCanvasElement | null = null;
@@ -47,11 +47,14 @@ export function createDemoSource(): WsiImageSource {
tilePath: "",
tileBaseUrl: "",
tileUrlBuilder: buildDemoTileUrl,
- terms: [
- { termId: "0", termName: "Background", termColor: "#888888" },
- { termId: "1", termName: "Negative", termColor: "#4a90d9" },
- { termId: "2", termName: "Positive", termColor: "#e74c3c" },
- { termId: "3", termName: "Other", termColor: "#2ecc71" },
- ],
};
}
+
+export function createDemoClasses(): WsiClass[] {
+ return [
+ { classId: "0", className: "Background", classColor: "#888888" },
+ { classId: "1", className: "Negative", classColor: "#4a90d9" },
+ { classId: "2", className: "Positive", classColor: "#e74c3c" },
+ { classId: "3", className: "Other", classColor: "#2ecc71" },
+ ];
+}
diff --git a/example/src/workers/zst-point-worker.ts b/example/src/workers/zst-point-worker.ts
index f172a88..8f270d7 100644
--- a/example/src/workers/zst-point-worker.ts
+++ b/example/src/workers/zst-point-worker.ts
@@ -95,9 +95,9 @@ function getFirstLayer(tile) {
return layer;
}
-function resolveRawTermId(properties) {
+function resolveRawClassId(properties) {
if (!properties) return "0";
- const candidates = [
+ const candidates = [
properties.termId,
properties.term_id,
properties.categoryId,
@@ -122,9 +122,9 @@ function parsePointsFromLayer(layer, imageHeight) {
const len = layer.length;
const positions = new Float32Array(len * 2);
- const localTermIndex = new Uint16Array(len);
- const termTable = [""];
- const termToLocal = new Map();
+ const localClassIndex = new Uint16Array(len);
+ const classTable = [""];
+ const classToLocal = new Map();
let hasNt = false;
let hasPositivityRank = false;
@@ -157,29 +157,29 @@ function parsePointsFromLayer(layer, imageHeight) {
hasPositivityRank = true;
}
- const termId = resolveRawTermId(feature.properties);
- let localIdx = termToLocal.get(termId);
+ const classId = resolveRawClassId(feature.properties);
+ let localIdx = classToLocal.get(classId);
if (localIdx === undefined) {
- localIdx = termTable.length;
+ localIdx = classTable.length;
if (localIdx >= 65535) {
localIdx = 0;
} else {
- termToLocal.set(termId, localIdx);
- termTable.push(termId);
+ classToLocal.set(classId, localIdx);
+ classTable.push(classId);
}
}
positions[cursor * 2] = x;
positions[cursor * 2 + 1] = y;
- localTermIndex[cursor] = localIdx;
+ localClassIndex[cursor] = localIdx;
cursor += 1;
}
return {
count: cursor,
positions: positions.subarray(0, cursor * 2),
- localTermIndex: localTermIndex.subarray(0, cursor),
- termTable,
+ localClassIndex: localClassIndex.subarray(0, cursor),
+ classTable,
hasNt,
hasPositivityRank,
};
@@ -212,19 +212,19 @@ self.onmessage = async (event) => {
const parsed = parsePointsFromLayer(layer, imageHeight);
const positions = parsed.positions.slice();
- const localTermIndex = parsed.localTermIndex.slice();
+ const localClassIndex = parsed.localClassIndex.slice();
self.postMessage(
{
type: "done",
count: parsed.count,
- termTable: parsed.termTable,
+ classTable: parsed.classTable,
hasNt: parsed.hasNt,
hasPositivityRank: parsed.hasPositivityRank,
positions,
- localTermIndex,
+ localClassIndex,
},
- [positions.buffer, localTermIndex.buffer],
+ [positions.buffer, localClassIndex.buffer],
);
} catch (error) {
self.postMessage({
diff --git a/performance-optimization.md b/performance-optimization.md
index 439fae7..7ec2e97 100644
--- a/performance-optimization.md
+++ b/performance-optimization.md
@@ -21,7 +21,7 @@
### 1.4 포인트 렌더 파이프라인
- 포인트 입력을 `Float32Array(positions)` + `Uint16Array(paletteIndices)`로 고정해 CPU/GPU 전송 비용 최소화.
-- 팔레트는 1D 텍스처(`RGBA`)로 유지해 term 색 변경 시 전체 포인트 버퍼 재업로드를 피함.
+- 팔레트는 1D 텍스처(`RGBA`)로 유지해 class 색 변경 시 전체 포인트 버퍼 재업로드를 피함.
- 포인트는 단일 draw call(`gl.POINTS`)로 렌더.
- 링 형태는 fragment shader에서 계산해 geometry 확장 없이 표현.
diff --git a/src/index.ts b/src/index.ts
index 5ff843d..213f156 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -66,8 +66,8 @@ export type {
RegionHoverEvent,
} from "./react/wsi-viewer-canvas-types";
export { DEFAULT_POINT_COLOR } from "./wsi/constants";
-export type { RawImagePayload, RawImsInfo, RawWsiTerm } from "./wsi/image-info";
-export { normalizeImageInfo, toTileUrl } from "./wsi/image-info";
+export type { RawImagePayload, RawImsInfo, RawWsiClass } from "./wsi/image-info";
+export { normalizeImageInfo, normalizeImageClasses, toTileUrl } from "./wsi/image-info";
export type { RoiCoordinate, RoiPolygon } from "./wsi/point-clip";
export { filterPointDataByPolygons, filterPointIndicesByPolygons } from "./wsi/point-clip";
export type {
@@ -112,9 +112,9 @@ export type {
RoiPointGroup,
RoiPointGroupOptions,
RoiPointGroupStats,
- RoiTermCount,
-} from "./wsi/roi-term-stats";
-export { computeRoiPointGroups } from "./wsi/roi-term-stats";
+ RoiClassCount,
+} from "./wsi/roi-class-stats";
+export { computeRoiPointGroups } from "./wsi/roi-class-stats";
export type { SpatialExtent, SpatialIndex, SpatialIndexItem } from "./wsi/spatial-index";
export { createSpatialIndex } from "./wsi/spatial-index";
export type {
@@ -125,7 +125,7 @@ export type {
} from "./wsi/tile-scheduler";
export { TileScheduler } from "./wsi/tile-scheduler";
export type {
- TermPalette,
+ ClassPalette,
WsiCoordinate,
WsiImageColorSettings,
WsiImageSource,
@@ -136,11 +136,11 @@ export type {
WsiRegionCoordinates,
WsiRenderStats,
WsiRingCoordinates,
- WsiTerm,
+ WsiClass,
WsiViewState,
} from "./wsi/types";
export {
- buildTermPalette,
+ buildClassPalette,
calcScaleLength,
calcScaleResolution,
clamp,
diff --git a/src/workers/roi-clip-worker.ts b/src/workers/roi-clip-worker.ts
index 1a1313d..10f37c3 100644
--- a/src/workers/roi-clip-worker.ts
+++ b/src/workers/roi-clip-worker.ts
@@ -29,12 +29,12 @@ function handleDataRequest(msg: RoiClipWorkerDataRequest): RoiClipWorkerSuccess
const start = nowMs();
const count = Math.max(0, Math.floor(msg.count));
const positions = new Float32Array(msg.positions);
- const terms = new Uint16Array(msg.paletteIndices);
+ const classes = new Uint16Array(msg.paletteIndices);
const fillModes = msg.fillModes ? new Uint8Array(msg.fillModes) : null;
const ids = msg.ids ? new Uint32Array(msg.ids) : null;
const maxCountByPositions = Math.floor(positions.length / 2);
- const safeCount = Math.max(0, Math.min(count, maxCountByPositions, terms.length, fillModes ? fillModes.length : Number.MAX_SAFE_INTEGER));
+ const safeCount = Math.max(0, Math.min(count, maxCountByPositions, classes.length, fillModes ? fillModes.length : Number.MAX_SAFE_INTEGER));
const hasFillModes = fillModes instanceof Uint8Array && fillModes.length >= safeCount;
const hasIds = ids instanceof Uint32Array && ids.length >= safeCount;
const prepared = prepareRoiPolygons(msg.polygons ?? []);
@@ -58,7 +58,7 @@ function handleDataRequest(msg: RoiClipWorkerDataRequest): RoiClipWorkerSuccess
}
const nextPositions = new Float32Array(safeCount * 2);
- const nextTerms = new Uint16Array(safeCount);
+ const nextClasses = new Uint16Array(safeCount);
const nextFillModes = hasFillModes ? new Uint8Array(safeCount) : null;
const nextIds = hasIds ? new Uint32Array(safeCount) : null;
let cursor = 0;
@@ -69,7 +69,7 @@ function handleDataRequest(msg: RoiClipWorkerDataRequest): RoiClipWorkerSuccess
if (!pointInAnyPreparedPolygon(x, y, prepared)) continue;
nextPositions[cursor * 2] = x;
nextPositions[cursor * 2 + 1] = y;
- nextTerms[cursor] = terms[i];
+ nextClasses[cursor] = classes[i];
if (nextFillModes) {
nextFillModes[cursor] = fillModes![i];
}
@@ -80,7 +80,7 @@ function handleDataRequest(msg: RoiClipWorkerDataRequest): RoiClipWorkerSuccess
}
const outPositions = nextPositions.slice(0, cursor * 2);
- const outTerms = nextTerms.slice(0, cursor);
+ const outClasses = nextClasses.slice(0, cursor);
const outFillModes = nextFillModes ? nextFillModes.slice(0, cursor) : null;
const outIds = nextIds ? nextIds.slice(0, cursor) : null;
@@ -89,7 +89,7 @@ function handleDataRequest(msg: RoiClipWorkerDataRequest): RoiClipWorkerSuccess
id: msg.id,
count: cursor,
positions: outPositions.buffer,
- paletteIndices: outTerms.buffer,
+ paletteIndices: outClasses.buffer,
durationMs: nowMs() - start,
};
if (outFillModes) {
diff --git a/src/wsi/image-info.ts b/src/wsi/image-info.ts
index f36f2e5..4638168 100644
--- a/src/wsi/image-info.ts
+++ b/src/wsi/image-info.ts
@@ -1,4 +1,4 @@
-import type { WsiImageSource, WsiTerm } from "./types";
+import type { WsiClass, WsiImageSource } from "./types";
export interface RawImsInfo {
width?: number | null;
@@ -9,10 +9,10 @@ export interface RawImsInfo {
mpp?: number | null;
}
-export interface RawWsiTerm {
- termId?: string | null;
- termName?: string | null;
- termColor?: string | null;
+export interface RawWsiClass {
+ classId?: string | null;
+ className?: string | null;
+ classColor?: string | null;
}
export interface RawImagePayload {
@@ -26,10 +26,20 @@ export interface RawImagePayload {
path?: string | null;
mpp?: number | null;
imsInfo?: RawImsInfo | null;
- terms?: RawWsiTerm[] | null;
+ classes?: RawWsiClass[] | null;
tileUrlBuilder?: (tier: number, x: number, y: number, tilePath: string, tileBaseUrl: string) => string;
}
+export function normalizeImageClasses(raw: Pick | null | undefined): WsiClass[] {
+ return Array.isArray(raw?.classes)
+ ? raw.classes.map((item: RawWsiClass) => ({
+ classId: String(item?.classId ?? ""),
+ className: String(item?.className ?? ""),
+ classColor: String(item?.classColor ?? ""),
+ }))
+ : [];
+}
+
function trimTrailingSlash(value: string): string {
return String(value ?? "").replace(/\/+$/, "");
}
@@ -85,14 +95,6 @@ export function normalizeImageInfo(raw: RawImagePayload, tileBaseUrl: string): W
throw new Error("Incomplete image metadata: width/height/tileSize/path required");
}
- const terms: WsiTerm[] = Array.isArray(raw?.terms)
- ? raw.terms.map((term: RawWsiTerm) => ({
- termId: String(term?.termId ?? ""),
- termName: String(term?.termName ?? ""),
- termColor: String(term?.termColor ?? ""),
- }))
- : [];
-
const normalizedPath = ensureLeadingSlash(tilePath);
const imsTileRoot = joinImsTileRoot(tileBaseUrl);
const tileUrlBuilder = raw?.tileUrlBuilder
@@ -108,7 +110,6 @@ export function normalizeImageInfo(raw: RawImagePayload, tileBaseUrl: string): W
maxTierZoom: Number.isFinite(maxTierZoom) ? Math.max(0, Math.floor(maxTierZoom)) : 0,
tilePath,
tileBaseUrl,
- terms,
tileUrlBuilder,
};
}
diff --git a/src/wsi/point-clip-hybrid.ts b/src/wsi/point-clip-hybrid.ts
index 3c89a62..1581a25 100644
--- a/src/wsi/point-clip-hybrid.ts
+++ b/src/wsi/point-clip-hybrid.ts
@@ -231,7 +231,7 @@ export async function filterPointDataByPolygonsHybrid(
}
const nextPositions = new Float32Array(candidateCount * 2);
- const nextTerms = new Uint16Array(candidateCount);
+ const nextClasses = new Uint16Array(candidateCount);
const nextFillModes = pointFillModes ? new Uint8Array(candidateCount) : null;
const nextIds = pointIds ? new Uint32Array(candidateCount) : null;
let cursor = 0;
@@ -243,7 +243,7 @@ export async function filterPointDataByPolygonsHybrid(
if (!pointInAnyPreparedPolygon(x, y, prepared)) continue;
nextPositions[cursor * 2] = x;
nextPositions[cursor * 2 + 1] = y;
- nextTerms[cursor] = pointData.paletteIndices[pointIndex];
+ nextClasses[cursor] = pointData.paletteIndices[pointIndex];
if (nextFillModes) {
nextFillModes[cursor] = pointFillModes![pointIndex];
}
@@ -256,7 +256,7 @@ export async function filterPointDataByPolygonsHybrid(
const compactData: WsiPointData = {
count: cursor,
positions: nextPositions.subarray(0, cursor * 2),
- paletteIndices: nextTerms.subarray(0, cursor),
+ paletteIndices: nextClasses.subarray(0, cursor),
};
if (nextFillModes) {
compactData.fillModes = nextFillModes.subarray(0, cursor);
diff --git a/src/wsi/point-clip-worker-client.ts b/src/wsi/point-clip-worker-client.ts
index dda64bc..539e58d 100644
--- a/src/wsi/point-clip-worker-client.ts
+++ b/src/wsi/point-clip-worker-client.ts
@@ -119,7 +119,7 @@ export async function filterPointDataByPolygonsInWorker(pointData: WsiPointData
const safeCount = sanitizePointCount(pointData);
const positionsCopy = pointData.positions.slice(0, safeCount * 2);
- const termsCopy = pointData.paletteIndices.slice(0, safeCount);
+ const classesCopy = pointData.paletteIndices.slice(0, safeCount);
const fillModesCopy = pointData.fillModes instanceof Uint8Array && pointData.fillModes.length >= safeCount ? pointData.fillModes.slice(0, safeCount) : null;
const idsCopy = pointData.ids instanceof Uint32Array && pointData.ids.length >= safeCount ? pointData.ids.slice(0, safeCount) : null;
@@ -145,13 +145,13 @@ export async function filterPointDataByPolygonsInWorker(pointData: WsiPointData
id: requestTicket.id,
count: safeCount,
positions: positionsCopy.buffer,
- paletteIndices: termsCopy.buffer,
+ paletteIndices: classesCopy.buffer,
fillModes: fillModesCopy?.buffer,
ids: idsCopy?.buffer,
polygons: polygons ?? [],
};
- const transfer: Transferable[] = [positionsCopy.buffer, termsCopy.buffer];
+ const transfer: Transferable[] = [positionsCopy.buffer, classesCopy.buffer];
if (fillModesCopy) transfer.push(fillModesCopy.buffer);
if (idsCopy) transfer.push(idsCopy.buffer);
diff --git a/src/wsi/point-clip.ts b/src/wsi/point-clip.ts
index 2ac8b44..71169c3 100644
--- a/src/wsi/point-clip.ts
+++ b/src/wsi/point-clip.ts
@@ -28,12 +28,12 @@ export function filterPointDataByPolygons(pointData: WsiPointData | null | undef
const count = sanitizePointCount(pointData);
const positions = pointData.positions;
- const terms = pointData.paletteIndices;
+ const classes = pointData.paletteIndices;
const fillModes = pointData.fillModes instanceof Uint8Array && pointData.fillModes.length >= count ? pointData.fillModes : null;
const pointIds = pointData.ids instanceof Uint32Array && pointData.ids.length >= count ? pointData.ids : null;
const nextPositions = new Float32Array(count * 2);
- const nextTerms = new Uint16Array(count);
+ const nextClasses = new Uint16Array(count);
const nextFillModes = fillModes ? new Uint8Array(count) : null;
const nextIds = pointIds ? new Uint32Array(count) : null;
let cursor = 0;
@@ -44,7 +44,7 @@ export function filterPointDataByPolygons(pointData: WsiPointData | null | undef
if (!pointInAnyPreparedPolygon(x, y, prepared)) continue;
nextPositions[cursor * 2] = x;
nextPositions[cursor * 2 + 1] = y;
- nextTerms[cursor] = terms[i];
+ nextClasses[cursor] = classes[i];
if (nextFillModes) {
nextFillModes[cursor] = fillModes![i];
}
@@ -57,7 +57,7 @@ export function filterPointDataByPolygons(pointData: WsiPointData | null | undef
const output: WsiPointData = {
count: cursor,
positions: nextPositions.subarray(0, cursor * 2),
- paletteIndices: nextTerms.subarray(0, cursor),
+ paletteIndices: nextClasses.subarray(0, cursor),
};
if (nextFillModes) {
output.fillModes = nextFillModes.subarray(0, cursor);
diff --git a/src/wsi/roi-term-stats.ts b/src/wsi/roi-class-stats.ts
similarity index 87%
rename from src/wsi/roi-term-stats.ts
rename to src/wsi/roi-class-stats.ts
index e36e1ba..dd78b59 100644
--- a/src/wsi/roi-term-stats.ts
+++ b/src/wsi/roi-class-stats.ts
@@ -1,8 +1,8 @@
import { type PreparedRoiPolygon, pointInPreparedPolygon, prepareRoiPolygons, toRoiGeometry } from "./roi-geometry";
import type { WsiPointData, WsiRegion } from "./types";
-export interface RoiTermCount {
- termId: string;
+export interface RoiClassCount {
+ classId: string;
paletteIndex: number;
count: number;
}
@@ -11,11 +11,11 @@ export interface RoiPointGroup {
regionId: string | number;
regionIndex: number;
totalCount: number;
- termCounts: RoiTermCount[];
+ classCounts: RoiClassCount[];
}
export interface RoiPointGroupOptions {
- paletteIndexToTermId?: ReadonlyMap | readonly string[];
+ paletteIndexToClassId?: ReadonlyMap | readonly string[];
includeEmptyRegions?: boolean;
}
@@ -142,11 +142,7 @@ function buildPreparedRegionGridIndex(regions: readonly PreparedRegion[]): Prepa
};
}
-function getCandidateRegionIndices(
- index: PreparedRegionGridIndex | null,
- x: number,
- y: number,
-): readonly number[] {
+function getCandidateRegionIndices(index: PreparedRegionGridIndex | null, x: number, y: number): readonly number[] {
if (!index) return EMPTY_CANDIDATE_REGION_INDICES;
if (x < index.minX || x > index.maxX || y < index.minY || y > index.maxY) {
return EMPTY_CANDIDATE_REGION_INDICES;
@@ -156,13 +152,13 @@ function getCandidateRegionIndices(
return index.buckets[cellY * index.gridSize + cellX] ?? EMPTY_CANDIDATE_REGION_INDICES;
}
-function resolveTermId(paletteIndex: number, paletteIndexToTermId: RoiPointGroupOptions["paletteIndexToTermId"]): string {
- if (Array.isArray(paletteIndexToTermId)) {
- const fromArray = paletteIndexToTermId[paletteIndex];
+function resolveClassId(paletteIndex: number, paletteIndexToClassId: RoiPointGroupOptions["paletteIndexToClassId"]): string {
+ if (Array.isArray(paletteIndexToClassId)) {
+ const fromArray = paletteIndexToClassId[paletteIndex];
if (typeof fromArray === "string" && fromArray.length > 0) return fromArray;
}
- if (paletteIndexToTermId instanceof Map) {
- const fromMap = paletteIndexToTermId.get(paletteIndex);
+ if (paletteIndexToClassId instanceof Map) {
+ const fromMap = paletteIndexToClassId.get(paletteIndex);
if (typeof fromMap === "string" && fromMap.length > 0) return fromMap;
}
return String(paletteIndex);
@@ -217,7 +213,7 @@ export function computeRoiPointGroups(pointData: WsiPointData | null | undefined
};
}
- const regionTermCounters = new Map>();
+ const regionClassCounters = new Map>();
const regionTotalCounters = new Map();
const preparedRegionIndex = buildPreparedRegionGridIndex(preparedRegions);
let insideCount = 0;
@@ -249,9 +245,9 @@ export function computeRoiPointGroups(pointData: WsiPointData | null | undefined
insideCount += 1;
const paletteIndex = pointData.paletteIndices[pointIndex] ?? 0;
- const regionTermMap = regionTermCounters.get(bestRegion.regionIndex) ?? new Map();
- regionTermMap.set(paletteIndex, (regionTermMap.get(paletteIndex) ?? 0) + 1);
- regionTermCounters.set(bestRegion.regionIndex, regionTermMap);
+ const regionClassMap = regionClassCounters.get(bestRegion.regionIndex) ?? new Map();
+ regionClassMap.set(paletteIndex, (regionClassMap.get(paletteIndex) ?? 0) + 1);
+ regionClassCounters.set(bestRegion.regionIndex, regionClassMap);
regionTotalCounters.set(bestRegion.regionIndex, (regionTotalCounters.get(bestRegion.regionIndex) ?? 0) + 1);
}
@@ -260,10 +256,10 @@ export function computeRoiPointGroups(pointData: WsiPointData | null | undefined
for (const region of preparedRegions) {
const totalCount = regionTotalCounters.get(region.regionIndex) ?? 0;
if (!includeEmptyRegions && totalCount <= 0) continue;
- const termMap = regionTermCounters.get(region.regionIndex) ?? new Map();
- const termCounts: RoiTermCount[] = Array.from(termMap.entries())
+ const classMap = regionClassCounters.get(region.regionIndex) ?? new Map();
+ const classCounts: RoiClassCount[] = Array.from(classMap.entries())
.map(([paletteIndex, count]) => ({
- termId: resolveTermId(paletteIndex, options.paletteIndexToTermId),
+ classId: resolveClassId(paletteIndex, options.paletteIndexToClassId),
paletteIndex,
count,
}))
@@ -273,7 +269,7 @@ export function computeRoiPointGroups(pointData: WsiPointData | null | undefined
regionId: region.regionId,
regionIndex: region.regionIndex,
totalCount,
- termCounts,
+ classCounts,
});
}
diff --git a/src/wsi/types.ts b/src/wsi/types.ts
index 33910ac..8252916 100644
--- a/src/wsi/types.ts
+++ b/src/wsi/types.ts
@@ -1,7 +1,7 @@
-export interface WsiTerm {
- termId: string;
- termName: string;
- termColor: string;
+export interface WsiClass {
+ classId: string;
+ className: string;
+ classColor: string;
}
export interface WsiImageSource {
@@ -14,7 +14,6 @@ export interface WsiImageSource {
maxTierZoom: number;
tilePath: string;
tileBaseUrl: string;
- terms: WsiTerm[];
tileUrlBuilder?: (tier: number, x: number, y: number, tilePath: string, tileBaseUrl: string) => string;
}
@@ -70,7 +69,7 @@ export interface WsiRegion {
label?: string;
}
-export interface TermPalette {
+export interface ClassPalette {
colors: Uint8Array;
- termToPaletteIndex: Map;
+ classToPaletteIndex: Map;
}
diff --git a/src/wsi/utils.ts b/src/wsi/utils.ts
index cc4731a..b0db45b 100644
--- a/src/wsi/utils.ts
+++ b/src/wsi/utils.ts
@@ -1,5 +1,5 @@
import { DEFAULT_POINT_COLOR } from "./constants";
-import type { TermPalette, WsiPointData, WsiViewState } from "./types";
+import type { ClassPalette, WsiPointData, WsiViewState } from "./types";
export function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
@@ -94,23 +94,23 @@ export function hexToRgba(
return [(n >> 16) & 255, (n >> 8) & 255, n & 255, 255];
}
-export function buildTermPalette(
- terms:
- | Array<{ termId?: string | null; termColor?: string | null }>
+export function buildClassPalette(
+ classes:
+ | Array<{ classId?: string | null; classColor?: string | null }>
| null
| undefined,
-): TermPalette {
+): ClassPalette {
const palette: Array<[number, number, number, number]> = [
[...DEFAULT_POINT_COLOR],
];
- const termToPaletteIndex = new Map();
+ const classToPaletteIndex = new Map();
- for (const term of terms ?? []) {
- const termId = String(term?.termId ?? "");
- if (!termId || termToPaletteIndex.has(termId)) continue;
+ for (const item of classes ?? []) {
+ const classId = String(item?.classId ?? "");
+ if (!classId || classToPaletteIndex.has(classId)) continue;
- termToPaletteIndex.set(termId, palette.length);
- palette.push(hexToRgba(term?.termColor));
+ classToPaletteIndex.set(classId, palette.length);
+ palette.push(hexToRgba(item?.classColor));
}
const colors = new Uint8Array(palette.length * 4);
@@ -121,5 +121,5 @@ export function buildTermPalette(
colors[i * 4 + 3] = palette[i][3];
}
- return { colors, termToPaletteIndex };
+ return { colors, classToPaletteIndex };
}
diff --git a/src/wsi/wsi-lifecycle-ops.ts b/src/wsi/wsi-lifecycle-ops.ts
index 8c9865c..c4dcd6e 100644
--- a/src/wsi/wsi-lifecycle-ops.ts
+++ b/src/wsi/wsi-lifecycle-ops.ts
@@ -94,7 +94,7 @@ export function destroyRenderer(options: DestroyRendererOptions): DestroyRendere
options.gl.deleteProgram(options.tileProgram.program);
options.gl.deleteBuffer(options.pointProgram.posBuffer);
- options.gl.deleteBuffer(options.pointProgram.termBuffer);
+ options.gl.deleteBuffer(options.pointProgram.classBuffer);
options.gl.deleteBuffer(options.pointProgram.fillModeBuffer);
options.gl.deleteBuffer(options.pointProgram.indexBuffer);
options.gl.deleteTexture(options.pointProgram.paletteTexture);
diff --git a/src/wsi/wsi-point-data.ts b/src/wsi/wsi-point-data.ts
index ac9e425..d8fb0cb 100644
--- a/src/wsi/wsi-point-data.ts
+++ b/src/wsi/wsi-point-data.ts
@@ -131,7 +131,7 @@ export function setPointData(runtime: PointBufferRuntime, gl: WebGL2RenderingCon
gl.bindBuffer(gl.ARRAY_BUFFER, pointProgram.posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, currentPointData.positions, gl.STATIC_DRAW);
- gl.bindBuffer(gl.ARRAY_BUFFER, pointProgram.termBuffer);
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointProgram.classBuffer);
gl.bufferData(gl.ARRAY_BUFFER, currentPointData.paletteIndices, gl.STATIC_DRAW);
const zeroFillModes = getZeroFillModes(nextRuntime.zeroFillModes, safeCount);
diff --git a/src/wsi/wsi-renderer-types.ts b/src/wsi/wsi-renderer-types.ts
index 0d086f5..fb2bd1a 100644
--- a/src/wsi/wsi-renderer-types.ts
+++ b/src/wsi/wsi-renderer-types.ts
@@ -28,7 +28,7 @@ export interface PointProgram {
program: WebGLProgram;
vao: WebGLVertexArrayObject;
posBuffer: WebGLBuffer;
- termBuffer: WebGLBuffer;
+ classBuffer: WebGLBuffer;
fillModeBuffer: WebGLBuffer;
indexBuffer: WebGLBuffer;
paletteTexture: WebGLTexture;
diff --git a/src/wsi/wsi-shaders.ts b/src/wsi/wsi-shaders.ts
index 63b3f81..e72f99a 100644
--- a/src/wsi/wsi-shaders.ts
+++ b/src/wsi/wsi-shaders.ts
@@ -99,23 +99,23 @@ export function initPointProgram(gl: WebGL2RenderingContext): PointProgram {
const pointVertex = `#version 300 es
precision highp float;
in vec2 aPosition;
- in uint aTerm;
+ in uint aClass;
in uint aFillMode;
uniform mat3 uCamera;
uniform float uPointSize;
- flat out uint vTerm;
+ flat out uint vClass;
flat out uint vFillMode;
void main() {
vec3 clip = uCamera * vec3(aPosition, 1.0);
gl_Position = vec4(clip.xy, 0.0, 1.0);
gl_PointSize = uPointSize;
- vTerm = aTerm;
+ vClass = aClass;
vFillMode = aFillMode;
}`;
const pointFragment = `#version 300 es
precision highp float;
- flat in uint vTerm;
+ flat in uint vClass;
flat in uint vFillMode;
uniform sampler2D uPalette;
uniform float uPaletteSize;
@@ -128,7 +128,7 @@ export function initPointProgram(gl: WebGL2RenderingContext): PointProgram {
float r = length(pc);
if (r > 1.0) discard;
- float idx = clamp(float(vTerm), 0.0, max(0.0, uPaletteSize - 1.0));
+ float idx = clamp(float(vClass), 0.0, max(0.0, uPaletteSize - 1.0));
vec2 uv = vec2((idx + 0.5) / uPaletteSize, 0.5);
vec4 color = texture(uPalette, uv);
if (color.a <= 0.0) discard;
@@ -163,11 +163,11 @@ export function initPointProgram(gl: WebGL2RenderingContext): PointProgram {
const vao = gl.createVertexArray();
const posBuffer = gl.createBuffer();
- const termBuffer = gl.createBuffer();
+ const classBuffer = gl.createBuffer();
const fillModeBuffer = gl.createBuffer();
const indexBuffer = gl.createBuffer();
const paletteTexture = gl.createTexture();
- if (!vao || !posBuffer || !termBuffer || !fillModeBuffer || !indexBuffer || !paletteTexture) {
+ if (!vao || !posBuffer || !classBuffer || !fillModeBuffer || !indexBuffer || !paletteTexture) {
throw new Error("point buffer allocation failed");
}
@@ -182,14 +182,14 @@ export function initPointProgram(gl: WebGL2RenderingContext): PointProgram {
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
- gl.bindBuffer(gl.ARRAY_BUFFER, termBuffer);
+ gl.bindBuffer(gl.ARRAY_BUFFER, classBuffer);
gl.bufferData(gl.ARRAY_BUFFER, 0, gl.DYNAMIC_DRAW);
- const termLoc = gl.getAttribLocation(program, "aTerm");
- if (termLoc < 0) {
- throw new Error("point term attribute not found");
+ const classLoc = gl.getAttribLocation(program, "aClass");
+ if (classLoc < 0) {
+ throw new Error("point class attribute not found");
}
- gl.enableVertexAttribArray(termLoc);
- gl.vertexAttribIPointer(termLoc, 1, gl.UNSIGNED_SHORT, 0, 0);
+ gl.enableVertexAttribArray(classLoc);
+ gl.vertexAttribIPointer(classLoc, 1, gl.UNSIGNED_SHORT, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, fillModeBuffer);
gl.bufferData(gl.ARRAY_BUFFER, 0, gl.DYNAMIC_DRAW);
@@ -219,7 +219,7 @@ export function initPointProgram(gl: WebGL2RenderingContext): PointProgram {
program,
vao,
posBuffer,
- termBuffer,
+ classBuffer,
fillModeBuffer,
indexBuffer,
paletteTexture,
diff --git a/tests/perf/ws9-perf.mjs b/tests/perf/ws9-perf.mjs
index 64c636a..7e39fef 100644
--- a/tests/perf/ws9-perf.mjs
+++ b/tests/perf/ws9-perf.mjs
@@ -12,11 +12,11 @@ function createPointData(count) {
seed = (1664525 * seed + 1013904223) >>> 0;
const y = seed % 10_000;
seed = (1664525 * seed + 1013904223) >>> 0;
- const term = seed % 8;
+ const classIndex = seed % 8;
positions[i * 2] = x;
positions[i * 2 + 1] = y;
- paletteIndices[i] = term;
+ paletteIndices[i] = classIndex;
}
return { count, positions, paletteIndices };
diff --git a/tests/unit/image-info.test.mjs b/tests/unit/image-info.test.mjs
index d77f1e8..cdff2f5 100644
--- a/tests/unit/image-info.test.mjs
+++ b/tests/unit/image-info.test.mjs
@@ -11,9 +11,9 @@ function createRawImageInfo() {
tileSize: 256,
zoom: 8,
path: "/tiles/hash123",
- terms: [
- { termId: "1", termName: "Positive", termColor: "#ff0000" },
- { termId: "2", termName: "Negative", termColor: "#0000ff" },
+ classes: [
+ { classId: "1", className: "Positive", classColor: "#ff0000" },
+ { classId: "2", className: "Negative", classColor: "#0000ff" },
],
imsInfo: {
path: "/tiles/hash123",
diff --git a/tests/unit/roi-term-stats.test.mjs b/tests/unit/roi-class-stats.test.mjs
similarity index 93%
rename from tests/unit/roi-term-stats.test.mjs
rename to tests/unit/roi-class-stats.test.mjs
index 1530656..1d78db7 100644
--- a/tests/unit/roi-term-stats.test.mjs
+++ b/tests/unit/roi-class-stats.test.mjs
@@ -55,8 +55,8 @@ test("computeRoiPointGroups: respects drawIndices bridge input", () => {
assert.equal(stats.unmatchedPointCount, 1);
assert.equal(stats.groups.length, 1);
assert.equal(stats.groups[0].totalCount, 1);
- assert.equal(stats.groups[0].termCounts[0].paletteIndex, 3);
- assert.equal(stats.groups[0].termCounts[0].count, 1);
+ assert.equal(stats.groups[0].classCounts[0].paletteIndex, 3);
+ assert.equal(stats.groups[0].classCounts[0].count, 1);
});
test("computeRoiPointGroups: excludes points inside polygon holes", () => {
@@ -160,7 +160,7 @@ test("computeRoiPointGroups: prefers the smallest containing region when regions
const groupById = new Map(stats.groups.map(group => [group.regionId, group]));
assert.equal(groupById.get("large")?.totalCount, 1);
- assert.equal(groupById.get("large")?.termCounts[0]?.paletteIndex, 6);
+ assert.equal(groupById.get("large")?.classCounts[0]?.paletteIndex, 6);
assert.equal(groupById.get("small")?.totalCount, 1);
- assert.equal(groupById.get("small")?.termCounts[0]?.paletteIndex, 5);
+ assert.equal(groupById.get("small")?.classCounts[0]?.paletteIndex, 5);
});
diff --git a/tests/unit/utils.test.mjs b/tests/unit/utils.test.mjs
index e87eec9..ee7905d 100644
--- a/tests/unit/utils.test.mjs
+++ b/tests/unit/utils.test.mjs
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
-import { buildTermPalette, calcScaleLength, calcScaleResolution, isSameViewState, toBearerToken } from "../../dist/index.js";
+import { buildClassPalette, calcScaleLength, calcScaleResolution, isSameViewState, toBearerToken } from "../../dist/index.js";
test("toBearerToken: normalizes token format", () => {
assert.equal(toBearerToken("abc"), "Bearer abc");
@@ -21,14 +21,14 @@ test("isSameViewState: compares with epsilon tolerance", () => {
assert.equal(isSameViewState({ zoom: 1, offsetX: 10, offsetY: 20, rotationDeg: 1.5 }, { zoom: 1.2, offsetX: 10, offsetY: 20, rotationDeg: 1.5 }), false);
});
-test("buildTermPalette: keeps index 0 as default and deduplicates term ids", () => {
- const palette = buildTermPalette([
- { termId: "p", termColor: "#ff0000" },
- { termId: "n", termColor: "#0000ff" },
- { termId: "p", termColor: "#ffffff" },
+test("buildClassPalette: keeps index 0 as default and deduplicates class ids", () => {
+ const palette = buildClassPalette([
+ { classId: "p", classColor: "#ff0000" },
+ { classId: "n", classColor: "#0000ff" },
+ { classId: "p", classColor: "#ffffff" },
]);
assert.equal(palette.colors.length, 12);
- assert.equal(palette.termToPaletteIndex.get("p"), 1);
- assert.equal(palette.termToPaletteIndex.get("n"), 2);
+ assert.equal(palette.classToPaletteIndex.get("p"), 1);
+ assert.equal(palette.classToPaletteIndex.get("n"), 2);
});