open-plant 1.2.20 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,7 +7,25 @@ and this project follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- - No changes yet.
10
+ ### Added
11
+ - Web Worker 기반 포인트 공간 인덱스 빌드 (`point-hit-index-worker`). 메인 스레드 ~175ms 블로킹 제거.
12
+ - `FlatPointSpatialIndex` flat typed array 자료구조 + open-addressing hash table lookup.
13
+ - `buildPointSpatialIndexAsync()`, `lookupCellIndex()`, `terminatePointHitIndexWorker()` public API.
14
+ - 인접 tier (T±1) 타일 prefetch: 빠른 줌 시 blank frame 감소.
15
+ - `getVisibleTilesForTier(tier)` public method on `WsiTileRenderer`.
16
+ - 최적화 리포트: `perf-optimization-report.md`.
17
+
18
+ ### Changed
19
+ - `pointHitIndex` 빌드가 `useMemo` (동기) → `useEffect` + `useState` (비동기 워커)로 전환.
20
+ - 워커 인덱스 알고리즘: nested `Map` → 4-pass counting sort (GC-free, typed arrays only).
21
+ - 워커 프로토콜: positions/ids를 워커가 반환하지 않음 — 메인 스레드가 원본 참조 (전송량 ~120MB → ~48MB).
22
+ - `getCellByCoordinates` 내부 lookup: nested `Map.get()` → `Int32Array` hash table O(1).
23
+ - 타일 스케줄링: 현재 tier 외 인접 tier 타일을 `distance2` 페널티 기반 낮은 우선순위로 포함.
24
+ - 내부 perf logging (`logPerf`, `shouldLogPerf`, `PERF_LOG_*`) 제거.
25
+
26
+ ### Docs
27
+ - `performance-optimization.md` 업데이트: 워커 인덱스, prefetch, flat hash 내용 추가.
28
+ - `README.md` 프로젝트 구조: 워커/클라이언트 파일 반영.
11
29
 
12
30
  ## [1.2.4] - 2026-02-25
13
31
 
package/README.md CHANGED
@@ -73,12 +73,19 @@ draw mode에 진입하면 `setPointerCapture`로 입력을 독점해 팬(드래
73
73
  | | |
74
74
  |---|---|
75
75
  | **WebGL2 타일 렌더링** | 멀티 티어 타일 피라미드, LRU 캐시(320장), 저해상도 fallback 렌더링 |
76
+ | **타일 전용 색상 보정** | `imageColorSettings`로 brightness/contrast/saturation 실시간 반영 (cell/ROI/draw overlay는 영향 없음) |
76
77
  | **회전 인터랙션** | `WsiViewState.rotationDeg`, `Ctrl/Cmd + drag` 회전, `resetRotation` 경로 |
78
+ | **줌 범위 제어 + 전환 애니메이션** | `minZoom`/`maxZoom` clamp + `viewTransition`(duration/easing) |
77
79
  | **포인트 오버레이** | WebGL2 `gl.POINTS`로 수십, 수백만 개 포인트를 팔레트 텍스처 기반 컬러링. 파싱된 TypedArray만 입력 |
78
80
  | **포인트 크기 커스터마이즈** | `pointSizeByZoom` 객체로 zoom별 셀(px) 크기 지정 + 내부 선형 보간 |
81
+ | **포인트 렌더 모드 제어** | `pointData.fillModes`로 ring/solid 렌더링 제어 |
79
82
  | **모바일 타겟 성능** | iPhone 15급 환경에서 수백만 cell 워크로드를 전제로 pan/zoom 응답성을 유지하도록 설계 |
80
- | **드로잉 / ROI 도구** | Freehand · Rectangle · Circular + Stamp(사각형/원, mm² 지정) |
83
+ | **드로잉 / ROI 도구** | Freehand · Rectangle · Circular · Brush + Stamp(사각형/원, mm² 지정) |
81
84
  | **고정 픽셀 스탬프** | `stamp-rectangle-4096px` + `stampOptions.rectanglePixelSize` |
85
+ | **브러시 UX 제어** | `brushOptions` (`radius`, `edgeDetail`, `edgeSmoothing`, `clickSelectRoi` 등) |
86
+ | **ROI 인터랙션 제어** | `activeRegionId` controlled/uncontrolled + contour/label 기반 hit-test |
87
+ | **ROI 라벨 동적 제어** | `resolveRegionLabelStyle` + `autoLiftRegionLabelAtMaxZoom` |
88
+ | **실시간 면적 툴팁** | `drawAreaTooltip`으로 draw 중 mm² 표시 |
82
89
  | **ROI 포인트 클리핑** | `clipMode`: `sync` / `worker` / `hybrid-webgpu` (실험) |
83
90
  | **ROI 통계 API** | `computeRoiPointGroups()` + `onRoiPointGroups` 콜백 |
84
91
  | **ROI 커스텀 오버레이** | `resolveRegionStrokeStyle`, `overlayShapes` |
@@ -111,11 +118,14 @@ src/
111
118
  │ ├── point-clip.ts # ROI 포인트 클리핑
112
119
  │ ├── point-clip-worker-client.ts # ROI 워커 클리핑 클라이언트
113
120
  │ ├── point-clip-hybrid.ts # WebGPU + polygon 하이브리드 클리핑(실험)
121
+ │ ├── point-hit-index-worker-client.ts # 포인트 공간 인덱스 워커 클라이언트
122
+ │ ├── point-hit-index-worker-protocol.ts # 인덱스 워커 메시지 프로토콜
114
123
  │ ├── webgpu.ts # WebGPU capability/compute 유틸
115
124
  │ ├── image-info.ts # 이미지 메타데이터 정규화
116
125
  │ └── utils.ts # 팔레트, 색상, 토큰 유틸리티
117
126
  ├── workers/
118
- └── roi-clip-worker.ts # ROI point-in-polygon worker
127
+ ├── roi-clip-worker.ts # ROI point-in-polygon worker
128
+ │ └── point-hit-index-worker.ts # 포인트 공간 인덱스 빌드 worker
119
129
  └── react/ # React 컴포넌트
120
130
  ├── wsi-viewer-canvas.tsx # 전체 기능 WSI 뷰어
121
131
  ├── draw-layer.tsx # 드로잉 오버레이
@@ -126,7 +136,7 @@ src/
126
136
 
127
137
  ### `<WsiViewerCanvas>`
128
138
 
129
- 전체 기능을 갖춘 WSI 뷰어 컴포넌트.
139
+ 전체 기능을 갖춘 WSI 뷰어 컴포넌트. 실사용 시 대부분의 기능은 이 컴포넌트 하나로 제어합니다.
130
140
 
131
141
  ```jsx
132
142
  import { WsiViewerCanvas } from "open-plant";
@@ -137,8 +147,8 @@ import { WsiViewerCanvas } from "open-plant";
137
147
  imageColorSettings={{ brightness: 0, contrast: 0, saturation: 0 }}
138
148
  ctrlDragRotate
139
149
  rotationResetNonce={rotationResetNonce}
140
- minZoom={0.25}
141
- maxZoom={4}
150
+ minZoom={0.25} // 미지정 시 fitZoom * 0.5
151
+ maxZoom={1} // 미지정 시 fitZoom * 8
142
152
  viewTransition={{ duration: 300 }}
143
153
  authToken={bearerToken}
144
154
  pointData={pointPayload}
@@ -152,7 +162,7 @@ import { WsiViewerCanvas } from "open-plant";
152
162
  clipPointsToRois
153
163
  clipMode="worker"
154
164
  onClipStats={(s) => console.log(s.mode, s.durationMs)}
155
- drawTool="stamp-rectangle-4096px"
165
+ drawTool={drawTool}
156
166
  drawFillColor="transparent"
157
167
  activeRegionId={selectedRoiId} // controlled: 외부에서 active ROI 제어
158
168
  onActiveRegionChange={setSelectedRoiId} // 내부 클릭/탭 선택 변경 알림
@@ -190,6 +200,7 @@ import { WsiViewerCanvas } from "open-plant";
190
200
  onRoiPointGroups={(stats) => console.log(stats.groups)}
191
201
  onDrawComplete={(result) => {
192
202
  if (result.intent === "roi") handleRoi(result);
203
+ if (result.intent === "brush") handleBrush(result);
193
204
  }}
194
205
  onPatchComplete={(patch) => {
195
206
  // stamp-rectangle-4096px 전용
@@ -200,23 +211,118 @@ import { WsiViewerCanvas } from "open-plant";
200
211
  />
201
212
  ```
202
213
 
203
- `mpp`(microns per pixel, 픽셀당 마이크론)는 `WsiImageSource`에 포함되는 물리 스케일 값이며, 스탬프의 mm² 크기를 실제 픽셀 단위로 환산할 때 사용됩니다.
204
-
205
- `rotationDeg`는 뷰포트 회전 각도(도 단위)이며, `Ctrl/Cmd + drag`로 조작하거나 `viewState`로 직접 제어할 있습니다.
206
-
207
- `brushOptions.clickSelectRoi`를 `true`로 두면 브러시 모드에서 클릭(드래그 없음) 시 ROI hit-test 선택을 시도하고, ROI 클릭은 기존 브러시 점찍기 동작을 유지합니다.
208
- `brushOptions.edgeDetail`/`edgeSmoothing`으로 브러시 경계의 해상도와 스무딩 정도를 조절할 수 있습니다.
209
- `brushOptions.radius`는 world 좌표가 아니라 HTML/CSS px 단위이며, 줌 변화와 무관하게 on-screen 크기가 고정됩니다.
210
- `drawFillColor`는 freehand/rectangle/circular draw preview 내부 채움 색을 제어합니다. 미지정 시 기본값은 `transparent`입니다.
211
- `activeRegionId`를 전달하면 controlled mode로 동작하며 내부 state 대신 prop 값을 active ROI로 사용합니다. `activeRegionId`를 생략하면 기존처럼 uncontrolled mode(내부 state)로 동작합니다.
212
- `minZoom`/`maxZoom`으로 뷰어 zoom 범위를 지정할 있으며, 줌/더블클릭 줌/`setViewState`/`fitToImage` 모두 동일하게 clamp됩니다.
213
- `viewTransition`을 지정하면 `viewState` 반영, `fitToImage`, `zoomBy` 전환에 애니메이션이 적용됩니다.
214
- `resolveRegionLabelStyle`로 zoom/region context 기반 nametag 스타일(예: `offsetY`)을 동적으로 계산할 있습니다.
215
- `autoLiftRegionLabelAtMaxZoom`을 `true`로 주면 `maxZoom`에 도달하는 순간 nametag가 위로 `20px` 부드럽게 올라가고, `maxZoom`에서 벗어나면 다시 부드럽게 내려옵니다.
216
- `drawAreaTooltip`을 켜면 freehand/rectangle/circular 드로잉 커서 근처에 실시간 면적(mm²) tooltip을 표시합니다.
217
- `imageColorSettings`는 이미지 타일에만 적용되며, cell marker/ROI/draw overlay에는 영향을 주지 않습니다.
218
- `pointData.fillModes`(선택, `Uint8Array`)를 주면 포인트별 렌더 모드를 제어할 수 있습니다. `0`은 ring(stroked), `1`은 solid(fill)이며 `0`이 아닌 값은 solid로 처리됩니다.
219
- `roiRegions[].coordinates`는 단일 링뿐 아니라 hole이 포함된 Polygon(`[[outer],[hole1], ...]`)과 MultiPolygon(`[[[...]], [[...]], ...]`)도 지원합니다.
214
+ #### 동작 규약 (중요)
215
+
216
+ - `mpp`(microns per pixel) 스탬프 mm² 환산에 사용됩니다. 미지정 물리 크기는 근사치입니다.
217
+ - `imageColorSettings`는 타일 레이어에만 적용됩니다. 포인트/ROI/드로잉은 영향받지 않습니다.
218
+ - ROI hit-test **contour + nametag 영역** 기준입니다. ROI 내부 fill은 클릭/hover 영역에서 제외됩니다.
219
+ - `activeRegionId`를 주면 controlled mode, 생략하면 uncontrolled mode로 동작합니다.
220
+ - `minZoom`/`maxZoom`은 휠/더블클릭/`setViewState`/`fitToImage` 경로에 동일 clamp가 적용됩니다.
221
+ - `viewTransition`은 `setViewState`/`fitToImage`/`zoomBy` 전환에 적용되며 `duration` 최대값은 `2000ms`입니다.
222
+ - `drawFillColor` 기본값은 `transparent`입니다.
223
+ - `brushOptions.radius`는 HTML/CSS px 기준이며, 줌이 바뀌어도 on-screen 크기는 고정됩니다.
224
+ - `brushOptions.clickSelectRoi=true`이면 브러시 탭(드래그 없음) ROI를 먼저 선택하고, ROI 외부 탭은 일반 브러시 결과를 반환합니다.
225
+ - `autoLiftRegionLabelAtMaxZoom=true`이면 `maxZoom` 도달 라벨이 위로 `20px` 애니메이션 이동하고, 이탈 시 원위치로 내려옵니다.
226
+ - `drawAreaTooltip.enabled=true`이면 freehand/rectangle/circular 그리기 커서 근처에 실시간 면적(mm²)을 표시합니다.
227
+ - `roiRegions[].coordinates`는 ring / polygon(with holes) / multipolygon을 모두 지원합니다.
228
+
229
+ #### WsiViewerCanvas Props By Concern
230
+
231
+ **View / Camera**
232
+
233
+ | Prop | Type | Notes |
234
+ |---|---|---|
235
+ | `source` | `WsiImageSource \| null` | 필수 입력 메타데이터 |
236
+ | `viewState` | `Partial<WsiViewState> \| null` | 외부 제어 시점 |
237
+ | `onViewStateChange` | `(next) => void` | 내부 변경 통지 |
238
+ | `fitNonce` | `number` | 변경 시 fit 재실행 |
239
+ | `rotationResetNonce` | `number` | 변경 시 회전 0도 |
240
+ | `ctrlDragRotate` | `boolean` | 기본 `true` |
241
+ | `minZoom` / `maxZoom` | `number` | 미지정 시 `fitZoom*0.5` / `fitZoom*8` |
242
+ | `viewTransition` | `{ duration?: number; easing?: (t)=>number }` | 기본 즉시 반영(duration 0) |
243
+ | `authToken` | `string` | 타일/포인트 요청 인증 |
244
+ | `overviewMapConfig` | `OverviewMapConfig` | 미니맵 표시/옵션 |
245
+
246
+ **Tile / Point / Clip**
247
+
248
+ | Prop | Type | Notes |
249
+ |---|---|---|
250
+ | `imageColorSettings` | `WsiImageColorSettings \| null` | brightness/contrast/saturation 입력 범위 `[-100, 100]` |
251
+ | `pointData` | `WsiPointData \| null` | `positions`, `paletteIndices` 필수 |
252
+ | `pointPalette` | `Uint8Array \| null` | RGBA 팔레트 텍스처 |
253
+ | `pointSizeByZoom` | `Record<number, number>` | continuous zoom stop |
254
+ | `pointStrokeScale` | `number` | point ring 두께 스케일 |
255
+ | `clipPointsToRois` | `boolean` | ROI 외부 포인트 필터 |
256
+ | `clipMode` | `"sync" \| "worker" \| "hybrid-webgpu"` | 기본 `"worker"` |
257
+ | `onClipStats` | `(event) => void` | clip 실행 통계 |
258
+ | `onRoiPointGroups` | `(stats) => void` | ROI term 통계 |
259
+ | `roiPaletteIndexToTermId` | `ReadonlyMap<number,string> \| readonly string[]` | ROI term 매핑 |
260
+
261
+ **ROI / Draw / Overlay**
262
+
263
+ | Prop | Type | Notes |
264
+ |---|---|---|
265
+ | `roiRegions` / `roiPolygons` | `WsiRegion[]` / `DrawRegionCoordinates[]` | 영속 ROI 입력 |
266
+ | `patchRegions` | `WsiRegion[]` | patch 전용 표시 채널 |
267
+ | `interactionLock` | `boolean` | pan/zoom 잠금 |
268
+ | `drawTool` | `DrawTool` | 기본 `"cursor"` |
269
+ | `stampOptions` | `StampOptions` | mm² / 고정 px stamp 크기 |
270
+ | `brushOptions` | `BrushOptions` | 브러시 궤적/커서/탭 선택 |
271
+ | `drawFillColor` | `string` | draw preview fill, 기본 `transparent` |
272
+ | `regionStrokeStyle` / `regionStrokeHoverStyle` / `regionStrokeActiveStyle` | `Partial<RegionStrokeStyle>` | ROI 외곽선 스타일 |
273
+ | `patchStrokeStyle` | `Partial<RegionStrokeStyle>` | patch 선 스타일 |
274
+ | `resolveRegionStrokeStyle` | `RegionStrokeStyleResolver` | 상태별 동적 stroke |
275
+ | `regionLabelStyle` | `Partial<RegionLabelStyle>` | 기본 배지 스타일 override |
276
+ | `resolveRegionLabelStyle` | `RegionLabelStyleResolver` | 줌/region별 동적 라벨 스타일 |
277
+ | `autoLiftRegionLabelAtMaxZoom` | `boolean` | max zoom 도달 시 라벨 auto-lift |
278
+ | `drawAreaTooltip` | `DrawAreaTooltipOptions` | draw 중 실시간 mm² tooltip |
279
+ | `overlayShapes` | `DrawOverlayShape[]` | 커스텀 도형/반전 마스크 |
280
+ | `customLayers` | `WsiCustomLayer[]` | host React 오버레이 슬롯 |
281
+ | `activeRegionId` | `string \| number \| null` | controlled active ROI |
282
+
283
+ **Events / Refs**
284
+
285
+ | Prop | Type | Notes |
286
+ |---|---|---|
287
+ | `onStats` | `(stats: WsiRenderStats) => void` | 프레임 통계 |
288
+ | `onTileError` | `(event: WsiTileErrorEvent) => void` | 타일 로드 실패 |
289
+ | `onContextLost` / `onContextRestored` | `() => void` | WebGL 컨텍스트 이벤트 |
290
+ | `onPointerWorldMove` | `(event) => void` | world 좌표 포인터 스트림 |
291
+ | `onPointHover` / `onPointClick` | `(event) => void` | 포인트 hit 이벤트 |
292
+ | `getCellByCoordinatesRef` | `MutableRefObject<(coord)=>PointHitEvent \| null>` | imperative 좌표 hit-test |
293
+ | `onRegionHover` / `onRegionClick` | `(event) => void` | region hit 이벤트 |
294
+ | `onActiveRegionChange` | `(regionId) => void` | active 변경 통지 |
295
+ | `onDrawComplete` | `(result: DrawResult) => void` | `intent: "roi" \| "patch" \| "brush"` |
296
+ | `onPatchComplete` | `(result: PatchDrawResult) => void` | `stamp-rectangle-4096px` 전용 |
297
+ | `className` / `style` | `string` / `CSSProperties` | 컨테이너 스타일 |
298
+
299
+ ### `<DrawLayer>`
300
+
301
+ 독립 오버레이 드로잉 컴포넌트입니다. `WsiViewerCanvas` 내부에서 자동 사용되지만, 필요하면 별도로 직접 사용할 수 있습니다.
302
+
303
+ - 지원 툴: `freehand`, `rectangle`, `circular`, `brush`, `stamp-*`
304
+ - 브러시는 화면 픽셀 기준 반경 + `edgeDetail`/`edgeSmoothing` 옵션을 사용합니다.
305
+ - `Esc`로 현재 드로잉 세션을 취소할 수 있습니다.
306
+
307
+ ### `<OverviewMap>`
308
+
309
+ 현재 뷰포트를 표시하는 인터랙티브 미니맵입니다. `overviewMapConfig.show`를 `true`로 설정하면 `WsiViewerCanvas`에 함께 렌더링됩니다.
310
+
311
+ ## API
312
+
313
+ | Export | 설명 |
314
+ |---|---|
315
+ | `WsiViewerCanvas`, `DrawLayer`, `OverviewMap`, `TileViewerCanvas` | React 컴포넌트 |
316
+ | `WsiTileRenderer`, `M1TileRenderer`, `TileScheduler` | 렌더러/스케줄러 클래스 |
317
+ | `normalizeImageInfo`, `toTileUrl` | 이미지 메타데이터/타일 URL 유틸 |
318
+ | `buildTermPalette`, `calcScaleResolution`, `calcScaleLength`, `toBearerToken` | 공통 유틸 |
319
+ | `filterPointDataByPolygons`, `filterPointDataByPolygonsInWorker`, `filterPointDataByPolygonsHybrid` | ROI 포인트 클리핑 |
320
+ | `filterPointIndicesByPolygons`, `filterPointIndicesByPolygonsInWorker`, `terminateRoiClipWorker` | 인덱스 기반 클리핑/워커 관리 |
321
+ | `buildPointSpatialIndexAsync`, `lookupCellIndex`, `terminatePointHitIndexWorker` | 포인트 공간 인덱스 (워커) |
322
+ | `computeRoiPointGroups` | ROI term 통계 |
323
+ | `getWebGpuCapabilities`, `prefilterPointsByBoundsWebGpu` | WebGPU capability/연산(실험) |
324
+ | `closeRing`, `createRectangle`, `createCircle` | 도형 유틸 |
325
+ | 타입 export (`WsiViewerCanvasProps`, `WsiImageSource`, `WsiPointData`, `WsiViewTransitionOptions` 등) | TypeScript 통합용 공개 타입 |
220
326
 
221
327
  ### `<DrawLayer>`
222
328
 
@@ -0,0 +1,2 @@
1
+ (function(){"use strict";function U(){return typeof performance<"u"&&typeof performance.now=="function"?performance.now():Date.now()}function q(r){if(r instanceof Error)return r.message;try{return String(r)}catch{return"unknown worker error"}}function X(r,f,o){if(r<=0||f<=0||o<=0)return 256;const i=Math.max(1,r*f),h=Math.sqrt(i/Math.max(1,o))*4;return Math.max(24,Math.min(1024,h))}function S(r,f,o){return(r*73856093^f*19349663)>>>0&o}function B(r){const f=U(),o=Math.max(0,Math.floor(r.count)),i=new Float32Array(r.positions),L=Math.floor(i.length/2),h=Math.max(0,Math.min(o,L));if(h<=0)return null;let I=null;if(r.drawIndices){const t=new Uint32Array(r.drawIndices);if(t.length>0){let n=!0;for(let s=0;s<t.length;s+=1)if(t[s]>=h){n=!1;break}if(n)I=t;else{const s=new Uint32Array(t.length);let e=0;for(let u=0;u<t.length;u+=1)t[u]<h&&(s[e++]=t[u]);I=e>0?s.subarray(0,e):null}}}const A=I?I.length:h;if(A===0)return null;const F=X(r.sourceWidth,r.sourceHeight,A),H=1/F,O=new Int32Array(A),P=new Int32Array(A);let c=0;if(I)for(let t=0;t<A;t+=1){const n=I[t],s=i[n*2],e=i[n*2+1];!Number.isFinite(s)||!Number.isFinite(e)||(O[c]=Math.floor(s*H),P[c]=Math.floor(e*H),c+=1)}else for(let t=0;t<h;t+=1){const n=i[t*2],s=i[t*2+1];!Number.isFinite(n)||!Number.isFinite(s)||(O[c]=Math.floor(n*H),P[c]=Math.floor(s*H),c+=1)}if(c===0)return null;const V=Math.min(c,Math.max(64,c>>>3));let a=1;for(;a<V*2;)a<<=1;const G=a-1;let l=new Int32Array(a*2),p=new Int32Array(a);l.fill(2147483647);let y=0;const _=new Int32Array(c);for(let t=0;t<c;t+=1){const n=O[t],s=P[t];let e=S(n,s,G);for(;;){const u=l[e*2];if(u===2147483647){if(l[e*2]=n,l[e*2+1]=s,p[e]=1,_[t]=e,y+=1,y*4>a*3){const j=a;a<<=1;const E=a-1,x=new Int32Array(a*2),v=new Int32Array(a);x.fill(2147483647);for(let d=0;d<j;d+=1){if(l[d*2]===2147483647)continue;const z=l[d*2],Z=l[d*2+1];let M=S(z,Z,E);for(;x[M*2]!==2147483647;)M=M+1&E;x[M*2]=z,x[M*2+1]=Z,v[M]=p[d]}for(l=x,p=v,e=S(n,s,E);l[e*2]!==n||l[e*2+1]!==s;)e=e+1&E;_[t]=e}break}if(u===n&&l[e*2+1]===s){p[e]+=1,_[t]=e;break}e=e+1&G}}const b=new Int32Array(y*2),k=new Uint32Array(y),K=new Uint32Array(y),g=new Int32Array(a);g.fill(-1);let w=0,R=0;for(let t=0;t<a;t+=1)l[t*2]!==2147483647&&(b[w*2]=l[t*2],b[w*2+1]=l[t*2+1],k[w]=R,K[w]=p[t],g[t]=w,R+=p[t],w+=1);const D=new Uint32Array(c),T=new Uint32Array(y);if(T.set(k),I)for(let t=0;t<c;t+=1){const n=g[_[t]];D[T[n]]=I[t],T[n]+=1}else{let t=0;for(let n=0;n<h;n+=1){const s=i[n*2],e=i[n*2+1];if(!Number.isFinite(s)||!Number.isFinite(e))continue;const u=g[_[t]];D[T[u]]=n,T[u]+=1,t+=1}}let C=1;for(;C<y*2;)C<<=1;const Y=C-1,N=new Int32Array(C);N.fill(-1);for(let t=0;t<y;t+=1){const n=b[t*2],s=b[t*2+1];let e=S(n,s,Y);for(;N[e]!==-1;)e=e+1&Y;N[e]=t}return{type:"point-hit-index-success",id:r.id,cellSize:F,safeCount:h,cellCount:y,hashCapacity:C,hashTable:N.buffer,cellKeys:b.buffer,cellOffsets:k.buffer,cellLengths:K.buffer,pointIndices:D.buffer,durationMs:U()-f}}const m=self;m.addEventListener("message",r=>{const f=r.data;if(!(!f||f.type!=="point-hit-index-request"))try{const o=B(f);if(!o){const i={type:"point-hit-index-success",id:f.id,cellSize:0,safeCount:0,cellCount:0,hashCapacity:0,hashTable:new Int32Array(0).buffer,cellKeys:new Int32Array(0).buffer,cellOffsets:new Uint32Array(0).buffer,cellLengths:new Uint32Array(0).buffer,pointIndices:new Uint32Array(0).buffer,durationMs:0};m.postMessage(i,[i.hashTable,i.cellKeys,i.cellOffsets,i.cellLengths,i.pointIndices]);return}m.postMessage(o,[o.hashTable,o.cellKeys,o.cellOffsets,o.cellLengths,o.pointIndices])}catch(o){const i={type:"point-hit-index-failure",id:f.id,error:q(o)};m.postMessage(i)}})})();
2
+ //# sourceMappingURL=point-hit-index-worker-Bp1uxxyQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"point-hit-index-worker-Bp1uxxyQ.js","sources":["../src/workers/point-hit-index-worker.ts"],"sourcesContent":["import type {\n\tPointHitIndexWorkerRequest,\n\tPointHitIndexWorkerResponse,\n\tPointHitIndexWorkerSuccess,\n} from \"../wsi/point-hit-index-worker-protocol\";\n\nconst MIN_POINT_HIT_GRID_SIZE = 24;\nconst MAX_POINT_HIT_GRID_SIZE = 1024;\nconst POINT_HIT_GRID_DENSITY_SCALE = 4;\nconst HASH_EMPTY = -1;\n\nfunction nowMs(): number {\n\tif (typeof performance !== \"undefined\" && typeof performance.now === \"function\") {\n\t\treturn performance.now();\n\t}\n\treturn Date.now();\n}\n\nfunction toErrorMessage(error: unknown): string {\n\tif (error instanceof Error) return error.message;\n\ttry {\n\t\treturn String(error);\n\t} catch {\n\t\treturn \"unknown worker error\";\n\t}\n}\n\nfunction resolveGridSize(sourceWidth: number, sourceHeight: number, visibleCount: number): number {\n\tif (sourceWidth <= 0 || sourceHeight <= 0 || visibleCount <= 0) return 256;\n\tconst area = Math.max(1, sourceWidth * sourceHeight);\n\tconst avgSpacing = Math.sqrt(area / Math.max(1, visibleCount));\n\tconst raw = avgSpacing * POINT_HIT_GRID_DENSITY_SCALE;\n\treturn Math.max(MIN_POINT_HIT_GRID_SIZE, Math.min(MAX_POINT_HIT_GRID_SIZE, raw));\n}\n\nfunction cellHash(cellX: number, cellY: number, mask: number): number {\n\treturn (((cellX * 73856093) ^ (cellY * 19349663)) >>> 0) & mask;\n}\n\nfunction handleRequest(msg: PointHitIndexWorkerRequest): PointHitIndexWorkerSuccess | null {\n\tconst start = nowMs();\n\tconst count = Math.max(0, Math.floor(msg.count));\n\tconst positions = new Float32Array(msg.positions);\n\tconst maxCountByPositions = Math.floor(positions.length / 2);\n\tconst safeCount = Math.max(0, Math.min(count, maxCountByPositions));\n\n\tif (safeCount <= 0) return null;\n\n\tlet drawIndices: Uint32Array | null = null;\n\tif (msg.drawIndices) {\n\t\tconst raw = new Uint32Array(msg.drawIndices);\n\t\tif (raw.length > 0) {\n\t\t\tlet allValid = true;\n\t\t\tfor (let i = 0; i < raw.length; i += 1) {\n\t\t\t\tif (raw[i] >= safeCount) { allValid = false; break; }\n\t\t\t}\n\t\t\tif (allValid) {\n\t\t\t\tdrawIndices = raw;\n\t\t\t} else {\n\t\t\t\tconst filtered = new Uint32Array(raw.length);\n\t\t\t\tlet cursor = 0;\n\t\t\t\tfor (let i = 0; i < raw.length; i += 1) {\n\t\t\t\t\tif (raw[i] < safeCount) { filtered[cursor++] = raw[i]; }\n\t\t\t\t}\n\t\t\t\tdrawIndices = cursor > 0 ? filtered.subarray(0, cursor) : null;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst visibleCount = drawIndices ? drawIndices.length : safeCount;\n\tif (visibleCount === 0) return null;\n\n\tconst cellSize = resolveGridSize(msg.sourceWidth, msg.sourceHeight, visibleCount);\n\tconst invCellSize = 1.0 / cellSize;\n\n\t// --- Pass 1: assign cell keys, count unique cells ---\n\tconst pointCellX = new Int32Array(visibleCount);\n\tconst pointCellY = new Int32Array(visibleCount);\n\tlet validCount = 0;\n\n\tif (drawIndices) {\n\t\tfor (let i = 0; i < visibleCount; i += 1) {\n\t\t\tconst pi = drawIndices[i];\n\t\t\tconst px = positions[pi * 2];\n\t\t\tconst py = positions[pi * 2 + 1];\n\t\t\tif (!Number.isFinite(px) || !Number.isFinite(py)) continue;\n\t\t\tpointCellX[validCount] = Math.floor(px * invCellSize);\n\t\t\tpointCellY[validCount] = Math.floor(py * invCellSize);\n\t\t\tvalidCount += 1;\n\t\t}\n\t} else {\n\t\tfor (let i = 0; i < safeCount; i += 1) {\n\t\t\tconst px = positions[i * 2];\n\t\t\tconst py = positions[i * 2 + 1];\n\t\t\tif (!Number.isFinite(px) || !Number.isFinite(py)) continue;\n\t\t\tpointCellX[validCount] = Math.floor(px * invCellSize);\n\t\t\tpointCellY[validCount] = Math.floor(py * invCellSize);\n\t\t\tvalidCount += 1;\n\t\t}\n\t}\n\n\tif (validCount === 0) return null;\n\n\t// --- Pass 2: build temporary hash to discover unique cells and count per cell ---\n\tconst estimatedCells = Math.min(validCount, Math.max(64, validCount >>> 3));\n\tlet hashCapacity = 1;\n\twhile (hashCapacity < estimatedCells * 2) hashCapacity <<= 1;\n\tconst hashMask = hashCapacity - 1;\n\n\tlet tempHashKeys = new Int32Array(hashCapacity * 2);\n\tlet tempHashCounts = new Int32Array(hashCapacity);\n\ttempHashKeys.fill(0x7FFFFFFF);\n\tlet cellCount = 0;\n\n\tconst pointCellSlot = new Int32Array(validCount);\n\n\tfor (let i = 0; i < validCount; i += 1) {\n\t\tconst cx = pointCellX[i];\n\t\tconst cy = pointCellY[i];\n\t\tlet slot = cellHash(cx, cy, hashMask);\n\n\t\twhile (true) {\n\t\t\tconst kx = tempHashKeys[slot * 2];\n\t\t\tif (kx === 0x7FFFFFFF) {\n\t\t\t\ttempHashKeys[slot * 2] = cx;\n\t\t\t\ttempHashKeys[slot * 2 + 1] = cy;\n\t\t\t\ttempHashCounts[slot] = 1;\n\t\t\t\tpointCellSlot[i] = slot;\n\t\t\t\tcellCount += 1;\n\t\t\t\tif (cellCount * 4 > hashCapacity * 3) {\n\t\t\t\t\tconst oldCap = hashCapacity;\n\t\t\t\t\thashCapacity <<= 1;\n\t\t\t\t\tconst newMask = hashCapacity - 1;\n\t\t\t\t\tconst newKeys = new Int32Array(hashCapacity * 2);\n\t\t\t\t\tconst newCounts = new Int32Array(hashCapacity);\n\t\t\t\t\tnewKeys.fill(0x7FFFFFFF);\n\t\t\t\t\tfor (let s = 0; s < oldCap; s += 1) {\n\t\t\t\t\t\tif (tempHashKeys[s * 2] === 0x7FFFFFFF) continue;\n\t\t\t\t\t\tconst ocx = tempHashKeys[s * 2];\n\t\t\t\t\t\tconst ocy = tempHashKeys[s * 2 + 1];\n\t\t\t\t\t\tlet ns = cellHash(ocx, ocy, newMask);\n\t\t\t\t\t\twhile (newKeys[ns * 2] !== 0x7FFFFFFF) ns = (ns + 1) & newMask;\n\t\t\t\t\t\tnewKeys[ns * 2] = ocx;\n\t\t\t\t\t\tnewKeys[ns * 2 + 1] = ocy;\n\t\t\t\t\t\tnewCounts[ns] = tempHashCounts[s];\n\t\t\t\t\t}\n\t\t\t\t\ttempHashKeys = newKeys;\n\t\t\t\t\ttempHashCounts = newCounts;\n\n\t\t\t\t\tslot = cellHash(cx, cy, newMask);\n\t\t\t\t\twhile (tempHashKeys[slot * 2] !== cx || tempHashKeys[slot * 2 + 1] !== cy) {\n\t\t\t\t\t\tslot = (slot + 1) & newMask;\n\t\t\t\t\t}\n\t\t\t\t\tpointCellSlot[i] = slot;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (kx === cx && tempHashKeys[slot * 2 + 1] === cy) {\n\t\t\t\ttempHashCounts[slot] += 1;\n\t\t\t\tpointCellSlot[i] = slot;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tslot = (slot + 1) & hashMask;\n\t\t}\n\t}\n\n\t// --- Pass 3: compute offsets via prefix sum ---\n\tconst finalHashMask = (hashCapacity - 1);\n\tconst cellKeys = new Int32Array(cellCount * 2);\n\tconst cellOffsets = new Uint32Array(cellCount);\n\tconst cellLengths = new Uint32Array(cellCount);\n\tconst slotToCellIndex = new Int32Array(hashCapacity);\n\tslotToCellIndex.fill(HASH_EMPTY);\n\n\tlet cellIdx = 0;\n\tlet offset = 0;\n\tfor (let s = 0; s < hashCapacity; s += 1) {\n\t\tif (tempHashKeys[s * 2] === 0x7FFFFFFF) continue;\n\t\tcellKeys[cellIdx * 2] = tempHashKeys[s * 2];\n\t\tcellKeys[cellIdx * 2 + 1] = tempHashKeys[s * 2 + 1];\n\t\tcellOffsets[cellIdx] = offset;\n\t\tcellLengths[cellIdx] = tempHashCounts[s];\n\t\tslotToCellIndex[s] = cellIdx;\n\t\toffset += tempHashCounts[s];\n\t\tcellIdx += 1;\n\t}\n\n\t// --- Pass 4: scatter point indices into flat array ---\n\tconst pointIndices = new Uint32Array(validCount);\n\tconst fillCursor = new Uint32Array(cellCount);\n\tfillCursor.set(cellOffsets);\n\n\tif (drawIndices) {\n\t\tfor (let i = 0; i < validCount; i += 1) {\n\t\t\tconst ci = slotToCellIndex[pointCellSlot[i]];\n\t\t\tpointIndices[fillCursor[ci]] = drawIndices[i];\n\t\t\tfillCursor[ci] += 1;\n\t\t}\n\t} else {\n\t\tlet srcIdx = 0;\n\t\tfor (let i = 0; i < safeCount; i += 1) {\n\t\t\tconst px = positions[i * 2];\n\t\t\tconst py = positions[i * 2 + 1];\n\t\t\tif (!Number.isFinite(px) || !Number.isFinite(py)) continue;\n\t\t\tconst ci = slotToCellIndex[pointCellSlot[srcIdx]];\n\t\t\tpointIndices[fillCursor[ci]] = i;\n\t\t\tfillCursor[ci] += 1;\n\t\t\tsrcIdx += 1;\n\t\t}\n\t}\n\n\t// --- Build final hash table for main-thread lookup ---\n\tlet finalCap = 1;\n\twhile (finalCap < cellCount * 2) finalCap <<= 1;\n\tconst finalMask = finalCap - 1;\n\tconst hashTable = new Int32Array(finalCap);\n\thashTable.fill(HASH_EMPTY);\n\n\tfor (let i = 0; i < cellCount; i += 1) {\n\t\tconst cx = cellKeys[i * 2];\n\t\tconst cy = cellKeys[i * 2 + 1];\n\t\tlet slot = cellHash(cx, cy, finalMask);\n\t\twhile (hashTable[slot] !== HASH_EMPTY) slot = (slot + 1) & finalMask;\n\t\thashTable[slot] = i;\n\t}\n\n\treturn {\n\t\ttype: \"point-hit-index-success\",\n\t\tid: msg.id,\n\t\tcellSize,\n\t\tsafeCount,\n\t\tcellCount,\n\t\thashCapacity: finalCap,\n\t\thashTable: hashTable.buffer,\n\t\tcellKeys: cellKeys.buffer,\n\t\tcellOffsets: cellOffsets.buffer,\n\t\tcellLengths: cellLengths.buffer,\n\t\tpointIndices: pointIndices.buffer,\n\t\tdurationMs: nowMs() - start,\n\t};\n}\n\ninterface WorkerScope {\n\tpostMessage(message: unknown, transfer?: Transferable[]): void;\n\taddEventListener(type: \"message\", listener: (event: MessageEvent<PointHitIndexWorkerRequest>) => void): void;\n}\n\nconst workerScope = self as unknown as WorkerScope;\n\nworkerScope.addEventListener(\"message\", (event: MessageEvent<PointHitIndexWorkerRequest>) => {\n\tconst data = event.data;\n\tif (!data || data.type !== \"point-hit-index-request\") return;\n\n\ttry {\n\t\tconst result = handleRequest(data);\n\t\tif (!result) {\n\t\t\tconst empty: PointHitIndexWorkerSuccess = {\n\t\t\t\ttype: \"point-hit-index-success\",\n\t\t\t\tid: data.id,\n\t\t\t\tcellSize: 0,\n\t\t\t\tsafeCount: 0,\n\t\t\t\tcellCount: 0,\n\t\t\t\thashCapacity: 0,\n\t\t\t\thashTable: new Int32Array(0).buffer,\n\t\t\t\tcellKeys: new Int32Array(0).buffer,\n\t\t\t\tcellOffsets: new Uint32Array(0).buffer,\n\t\t\t\tcellLengths: new Uint32Array(0).buffer,\n\t\t\t\tpointIndices: new Uint32Array(0).buffer,\n\t\t\t\tdurationMs: 0,\n\t\t\t};\n\t\t\tworkerScope.postMessage(empty, [\n\t\t\t\tempty.hashTable, empty.cellKeys,\n\t\t\t\tempty.cellOffsets, empty.cellLengths, empty.pointIndices,\n\t\t\t]);\n\t\t\treturn;\n\t\t}\n\t\tworkerScope.postMessage(result, [\n\t\t\tresult.hashTable, result.cellKeys,\n\t\t\tresult.cellOffsets, result.cellLengths, result.pointIndices,\n\t\t]);\n\t} catch (error) {\n\t\tconst fail: PointHitIndexWorkerResponse = {\n\t\t\ttype: \"point-hit-index-failure\",\n\t\t\tid: data.id,\n\t\t\terror: toErrorMessage(error),\n\t\t};\n\t\tworkerScope.postMessage(fail);\n\t}\n});\n"],"names":["nowMs","toErrorMessage","error","resolveGridSize","sourceWidth","sourceHeight","visibleCount","area","raw","cellHash","cellX","cellY","mask","handleRequest","msg","start","count","positions","maxCountByPositions","safeCount","drawIndices","allValid","i","filtered","cursor","cellSize","invCellSize","pointCellX","pointCellY","validCount","pi","px","py","estimatedCells","hashCapacity","hashMask","tempHashKeys","tempHashCounts","cellCount","pointCellSlot","cx","cy","slot","kx","oldCap","newMask","newKeys","newCounts","s","ocx","ocy","ns","cellKeys","cellOffsets","cellLengths","slotToCellIndex","cellIdx","offset","pointIndices","fillCursor","ci","srcIdx","finalCap","finalMask","hashTable","workerScope","event","data","result","empty","fail"],"mappings":"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"}