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)

    • customLayersuseViewerContext + host React overlay
    • -
    • onRoiPointGroups / roiPaletteIndexToTermIdcomputeRoiPointGroups()
    • +
    • onRoiPointGroups / roiPaletteIndexToClassIdcomputeRoiPointGroups()
    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[] }]

    pointDataregions가 바뀔 때 computeRoiPointGroups를 호출하세요. 레거시 WsiViewerCanvasonRoiPointGroups를 대체합니다.

    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 통계

    onRoiPointGroupsWsiViewerCanvas와 함께 제거되었습니다. 데이터가 바뀔 때 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 타입

    레이어에 없는 기능(앱에서 처리)

    • customLayersuseViewerContext + 호스트 React 오버레이
    • -
    • onRoiPointGroups / roiPaletteIndexToTermIdcomputeRoiPointGroups()
    • +
    • onRoiPointGroups / roiPaletteIndexToClassIdcomputeRoiPointGroups()
    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 => ( + + ))} +
    +
    + ); +} 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); });