open-plant 1.1.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 +26 -0
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/dist/index.cjs +94 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +2070 -0
- package/dist/index.js.map +1 -0
- package/dist/types/core/gl-utils.d.ts +4 -0
- package/dist/types/core/gl-utils.d.ts.map +1 -0
- package/dist/types/core/m1-tile-renderer.d.ts +42 -0
- package/dist/types/core/m1-tile-renderer.d.ts.map +1 -0
- package/dist/types/core/ortho-camera.d.ts +19 -0
- package/dist/types/core/ortho-camera.d.ts.map +1 -0
- package/dist/types/core/types.d.ts +7 -0
- package/dist/types/core/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/react/draw-layer.d.ts +79 -0
- package/dist/types/react/draw-layer.d.ts.map +1 -0
- package/dist/types/react/overview-map.d.ts +34 -0
- package/dist/types/react/overview-map.d.ts.map +1 -0
- package/dist/types/react/tile-viewer-canvas.d.ts +13 -0
- package/dist/types/react/tile-viewer-canvas.d.ts.map +1 -0
- package/dist/types/react/wsi-viewer-canvas.d.ts +46 -0
- package/dist/types/react/wsi-viewer-canvas.d.ts.map +1 -0
- package/dist/types/wsi/constants.d.ts +2 -0
- package/dist/types/wsi/constants.d.ts.map +1 -0
- package/dist/types/wsi/image-info.d.ts +4 -0
- package/dist/types/wsi/image-info.d.ts.map +1 -0
- package/dist/types/wsi/point-clip.d.ts +5 -0
- package/dist/types/wsi/point-clip.d.ts.map +1 -0
- package/dist/types/wsi/types.d.ts +47 -0
- package/dist/types/wsi/types.d.ts.map +1 -0
- package/dist/types/wsi/utils.d.ts +13 -0
- package/dist/types/wsi/utils.d.ts.map +1 -0
- package/dist/types/wsi/wsi-tile-renderer.d.ts +42 -0
- package/dist/types/wsi/wsi-tile-renderer.d.ts.map +1 -0
- package/package.json +67 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
6
|
+
and this project follows [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-02-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- WebGL2-based React WSI viewer core with tile rendering, drag pan, and zoom interactions.
|
|
14
|
+
- High-volume cell point rendering pipeline with Zstd MVT parsing support.
|
|
15
|
+
- Draw/region tools: freehand, rectangle, and circular region creation.
|
|
16
|
+
- Stamp tools for circle and rectangle with externally configurable size options.
|
|
17
|
+
- Region label rendering with top placement and customizable style options.
|
|
18
|
+
- Example app and bilingual (`docs/en`, `docs/ko`) documentation structure.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Region styling API expanded with `hover` and `active` stroke states.
|
|
22
|
+
- Region interaction API expanded with `onHover` and `onClick` listeners.
|
|
23
|
+
- npm package build/publish setup finalized for ESM/CJS/types outputs.
|
|
24
|
+
|
|
25
|
+
### Docs
|
|
26
|
+
- Added engine roadmap documentation for moving from viewer library to render engine.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Open Plant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/banner.png" width="100%" alt="Open Plant banner" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
WebGL2 기반 고성능 WSI(Whole Slide Image) 뷰어 라이브러리
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://frorong.github.io/open-plant/">📖 Documentation</a> ·
|
|
11
|
+
<a href="https://github.com/frorong/open-plant">GitHub</a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
> Engine roadmap: [`engine-roadmap.md`](./engine-roadmap.md)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why Open Plant
|
|
19
|
+
|
|
20
|
+
범용 시각화 프레임워크 위에 병리 뷰어를 올리면 추상화 비용을 그대로 떠안게 됩니다.
|
|
21
|
+
Open Plant는 WSI 렌더링 **한 가지만** 하도록 설계되었고, 그래서 아래가 가능합니다.
|
|
22
|
+
|
|
23
|
+
### 포인트 1개당 10바이트
|
|
24
|
+
|
|
25
|
+
범용 라이브러리는 포인트마다 인스턴스 버퍼에 position + RGBA를 넣어 **20바이트 이상** 씁니다.
|
|
26
|
+
Open Plant는 `Float32Array`(x, y) 8바이트 + `Uint16Array`(palette index) 2바이트 = **10바이트**입니다.
|
|
27
|
+
색상은 1×N 팔레트 텍스처 1장에 들어가므로, term 색상을 바꿀 때 수백 바이트짜리 텍스처만 재업로드하면 됩니다.
|
|
28
|
+
50만 셀 기준 GPU 메모리가 **절반 이하**로 줄어듭니다.
|
|
29
|
+
|
|
30
|
+
### 프래그먼트 셰이더 안에서 끝나는 링 렌더링
|
|
31
|
+
|
|
32
|
+
`gl.POINTS` + `gl_PointCoord`로 원형 마스킹하고, ring width를 `clamp(3.0 / pointSize, 0.12, 0.62)`로 줌에 따라 적응시킵니다.
|
|
33
|
+
안티앨리어싱도 `smoothstep(1.5 / pointSize)` 기반으로 프래그먼트 셰이더 안에서 처리하기 때문에 하드웨어 MSAA를 끌 수 있습니다.
|
|
34
|
+
고배율에서는 얇은 링으로 개별 세포를 구분하고, 저배율에서는 두꺼운 링으로 밀집 영역이 묻히지 않습니다.
|
|
35
|
+
별도 geometry 없이 draw call **1회**로 전체 포인트를 그립니다.
|
|
36
|
+
|
|
37
|
+
### 2-pass fallback 타일 렌더링
|
|
38
|
+
|
|
39
|
+
일반적인 타일 뷰어는 줌 전환 시 현재 tier 타일만 그리고, 로딩 중인 칸은 부모 타일을 확대하거나 회색 placeholder를 보여줍니다.
|
|
40
|
+
Open Plant는 매 프레임 **캐시 전체**(최대 320장)를 viewport와 교차 검사해서, tier 오름차순(가장 거친 것부터)으로 먼저 깔고 그 위에 현재 tier를 덮어씁니다.
|
|
41
|
+
줌/팬 중 **빈 타일이 보이지 않습니다.**
|
|
42
|
+
|
|
43
|
+
### 타일과 포인트가 같은 렌더 루프
|
|
44
|
+
|
|
45
|
+
별도 레이어 시스템 없이 하나의 WebGL2 컨텍스트에서 `fallback tiles → current tiles → points` 순서로 draw call이 나갑니다.
|
|
46
|
+
카메라가 바뀌면 한 프레임 안에 타일과 포인트가 **동시에** 갱신되므로 레이어 간 1-frame 지연이 없습니다.
|
|
47
|
+
|
|
48
|
+
### 드로잉 오버레이는 Canvas 2D로 분리
|
|
49
|
+
|
|
50
|
+
WebGL 캔버스(z-index: 1) 위에 Canvas 2D(z-index: 2)를 올려 어노테이션을 처리합니다.
|
|
51
|
+
draw mode가 아닐 때는 `pointerEvents: "none"`으로 이벤트가 WebGL에 바로 통과하고,
|
|
52
|
+
draw mode에 진입하면 `setPointerCapture`로 입력을 독점한 뒤 `interactionLock`이 WebGL 쪽 팬/줌 핸들러를 즉시 차단합니다.
|
|
53
|
+
드로잉과 네비게이션이 동시에 발동하는 일이 구조적으로 불가능합니다.
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
| | |
|
|
58
|
+
|---|---|
|
|
59
|
+
| **WebGL2 타일 렌더링** | 멀티 티어 타일 피라미드, LRU 캐시(320장), 저해상도 fallback 렌더링 |
|
|
60
|
+
| **포인트 오버레이** | WebGL2 `gl.POINTS`로 수십, 수백만 개 포인트를 팔레트 텍스처 기반 컬러링. 파싱된 TypedArray만 입력 |
|
|
61
|
+
| **드로잉 / ROI 도구** | Freehand · Rectangle · Circular + Stamp(사각형/원, mm² 지정) |
|
|
62
|
+
| **ROI 포인트 클리핑** | Ray-casting 기반 point-in-polygon으로 ROI 내부 포인트만 필터링 |
|
|
63
|
+
| **오버뷰 미니맵** | 썸네일 + 현재 뷰포트 인디케이터, 클릭/드래그 네비게이션 |
|
|
64
|
+
| **React 바인딩** | `<WsiViewerCanvas>`, `<DrawLayer>`, `<OverviewMap>` 컴포넌트 제공 |
|
|
65
|
+
| **좌표 변환** | `screenToWorld()` / `worldToScreen()` 양방향 좌표 변환 |
|
|
66
|
+
| **인증 지원** | Bearer 토큰 패스스루로 프라이빗 타일/포인트 엔드포인트 접근 |
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install
|
|
72
|
+
npm run dev:example
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
브라우저에서 `http://localhost:5174` 접속.
|
|
76
|
+
|
|
77
|
+
## Project Structure
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
src/
|
|
81
|
+
├── core/ # WebGL2 저수준 렌더링 엔진
|
|
82
|
+
│ ├── gl-utils.ts # 셰이더 컴파일, 프로그램 링킹
|
|
83
|
+
│ ├── ortho-camera.ts # 2D 직교 카메라 (translate + zoom)
|
|
84
|
+
│ └── m1-tile-renderer.ts # 기본 타일 렌더러
|
|
85
|
+
├── wsi/ # WSI 전용 로직
|
|
86
|
+
│ ├── wsi-tile-renderer.ts # 멀티 티어 타일 + 포인트 렌더러
|
|
87
|
+
│ ├── point-clip.ts # ROI 포인트 클리핑
|
|
88
|
+
│ ├── image-info.ts # 이미지 메타데이터 정규화
|
|
89
|
+
│ └── utils.ts # 팔레트, 색상, 토큰 유틸리티
|
|
90
|
+
└── react/ # React 컴포넌트
|
|
91
|
+
├── wsi-viewer-canvas.tsx # 전체 기능 WSI 뷰어
|
|
92
|
+
├── draw-layer.tsx # 드로잉 오버레이
|
|
93
|
+
└── overview-map.tsx # 미니맵
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## React Components
|
|
97
|
+
|
|
98
|
+
### `<WsiViewerCanvas>`
|
|
99
|
+
|
|
100
|
+
전체 기능을 갖춘 WSI 뷰어 컴포넌트.
|
|
101
|
+
|
|
102
|
+
```jsx
|
|
103
|
+
import { WsiViewerCanvas } from "open-plant";
|
|
104
|
+
|
|
105
|
+
<WsiViewerCanvas
|
|
106
|
+
source={imageSource}
|
|
107
|
+
viewState={viewState}
|
|
108
|
+
authToken={bearerToken}
|
|
109
|
+
pointData={pointPayload}
|
|
110
|
+
pointPalette={termPalette.colors}
|
|
111
|
+
drawTool="stamp-circle"
|
|
112
|
+
stampOptions={{
|
|
113
|
+
rectangleAreaMm2: 2,
|
|
114
|
+
circleAreaMm2: 0.2, // HPF 예시
|
|
115
|
+
}}
|
|
116
|
+
onDrawComplete={handleDraw}
|
|
117
|
+
onViewStateChange={handleViewChange}
|
|
118
|
+
onStats={setStats}
|
|
119
|
+
/>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`mpp`(microns per pixel, 픽셀당 마이크론)는 `WsiImageSource`에 포함되는 물리 스케일 값이며, 스탬프의 mm² 크기를 실제 픽셀 단위로 환산할 때 사용됩니다.
|
|
123
|
+
|
|
124
|
+
### `<DrawLayer>`
|
|
125
|
+
|
|
126
|
+
Freehand, Rectangle, Circular + Stamp(사각형/원) 드로잉 오버레이.
|
|
127
|
+
|
|
128
|
+
### `<OverviewMap>`
|
|
129
|
+
|
|
130
|
+
현재 뷰포트를 표시하는 인터랙티브 미니맵.
|
|
131
|
+
|
|
132
|
+
## API
|
|
133
|
+
|
|
134
|
+
| Export | 설명 |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `WsiTileRenderer` | WebGL2 WSI 타일 + 포인트 렌더러 클래스 |
|
|
137
|
+
| `M1TileRenderer` | 기본 타일 렌더러 클래스 |
|
|
138
|
+
| `normalizeImageInfo(raw, tileBaseUrl)` | API 응답 + 타일 베이스 URL을 `WsiImageSource`로 변환 |
|
|
139
|
+
| `filterPointDataByPolygons()` | ROI 폴리곤으로 포인트 필터링 |
|
|
140
|
+
| `buildTermPalette()` | Term 기반 컬러 팔레트 생성 |
|
|
141
|
+
| `toTileUrl()` | 타일 URL 생성 |
|
|
142
|
+
| `calcScaleResolution()` | 현재 줌 기준 μm/px 계산 |
|
|
143
|
+
| `calcScaleLength()` | 100px 스케일 라벨 문자열 생성 |
|
|
144
|
+
|
|
145
|
+
## Scripts
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm run dev # 개발 서버 (기본 타일 그리드)
|
|
149
|
+
npm run dev:example # 예제 앱 (전체 WSI 뷰어, port 5174)
|
|
150
|
+
npm run build # 프로덕션 빌드
|
|
151
|
+
npm run build:example # 예제 앱 빌드
|
|
152
|
+
npm run typecheck # 타입 체크
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";var jt=Object.defineProperty;var Jt=(e,t,i)=>t in e?jt(e,t,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[t]=i;var y=(e,t,i)=>Jt(e,typeof t!="symbol"?t+"":t,i);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const nt=require("react/jsx-runtime"),c=require("react");function Mt(e,t,i){const r=e.createShader(t);if(!r)throw new Error("Failed to create shader.");if(e.shaderSource(r,i),e.compileShader(r),!e.getShaderParameter(r,e.COMPILE_STATUS)){const o=e.getShaderInfoLog(r)??"unknown shader error";throw e.deleteShader(r),new Error(o)}return r}function Kt(e,t,i){const r=Mt(e,e.VERTEX_SHADER,t),n=Mt(e,e.FRAGMENT_SHADER,i),o=e.createProgram();if(!o)throw e.deleteShader(r),e.deleteShader(n),new Error("Failed to create program.");if(e.attachShader(o,r),e.attachShader(o,n),e.linkProgram(o),e.deleteShader(r),e.deleteShader(n),!e.getProgramParameter(o,e.LINK_STATUS)){const u=e.getProgramInfoLog(o)??"unknown link error";throw e.deleteProgram(o),new Error(u)}return o}function bt(e,t,i){const r=e.getUniformLocation(t,i);if(!r)throw new Error(`Failed to get uniform location: ${i}`);return r}function Qt(e){const t=e.getContext("webgl2",{alpha:!1,antialias:!1,depth:!1,stencil:!1,preserveDrawingBuffer:!1,powerPreference:"high-performance"});if(!t)throw new Error("WebGL2 is not available.");return t}let te=class{constructor(){y(this,"viewportWidth",1);y(this,"viewportHeight",1);y(this,"viewState",{offsetX:0,offsetY:0,zoom:1})}setViewport(t,i){this.viewportWidth=Math.max(1,t),this.viewportHeight=Math.max(1,i)}getViewportSize(){return{width:this.viewportWidth,height:this.viewportHeight}}setViewState(t){t.offsetX!==void 0&&(this.viewState.offsetX=t.offsetX),t.offsetY!==void 0&&(this.viewState.offsetY=t.offsetY),t.zoom!==void 0&&(this.viewState.zoom=Math.max(1e-4,t.zoom))}getViewState(){return{...this.viewState}}getMatrix(){const t=this.viewportWidth/this.viewState.zoom,i=this.viewportHeight/this.viewState.zoom,r=2/t,n=-2/i,o=-1-this.viewState.offsetX*r,a=1-this.viewState.offsetY*n;return new Float32Array([r,0,0,0,n,0,o,a,1])}};const ee=`#version 300 es
|
|
2
|
+
precision highp float;
|
|
3
|
+
|
|
4
|
+
in vec2 aUnit;
|
|
5
|
+
in vec2 aUv;
|
|
6
|
+
|
|
7
|
+
uniform mat3 uCamera;
|
|
8
|
+
uniform vec4 uBounds;
|
|
9
|
+
|
|
10
|
+
out vec2 vUv;
|
|
11
|
+
|
|
12
|
+
void main() {
|
|
13
|
+
vec2 world = vec2(
|
|
14
|
+
mix(uBounds.x, uBounds.z, aUnit.x),
|
|
15
|
+
mix(uBounds.y, uBounds.w, aUnit.y)
|
|
16
|
+
);
|
|
17
|
+
vec3 clip = uCamera * vec3(world, 1.0);
|
|
18
|
+
gl_Position = vec4(clip.xy, 0.0, 1.0);
|
|
19
|
+
vUv = aUv;
|
|
20
|
+
}
|
|
21
|
+
`,ie=`#version 300 es
|
|
22
|
+
precision highp float;
|
|
23
|
+
|
|
24
|
+
in vec2 vUv;
|
|
25
|
+
uniform sampler2D uTexture;
|
|
26
|
+
|
|
27
|
+
out vec4 outColor;
|
|
28
|
+
|
|
29
|
+
void main() {
|
|
30
|
+
outColor = texture(uTexture, vUv);
|
|
31
|
+
}
|
|
32
|
+
`;class Vt{constructor(t){y(this,"canvas");y(this,"gl");y(this,"camera",new te);y(this,"imageWidth");y(this,"imageHeight");y(this,"clearColor");y(this,"program");y(this,"vao");y(this,"quadBuffer");y(this,"uCameraLocation");y(this,"uBoundsLocation");y(this,"uTextureLocation");y(this,"resizeObserver");y(this,"tiles",[]);y(this,"frameId",null);y(this,"loadVersion",0);y(this,"destroyed",!1);y(this,"fitted",!1);y(this,"controlledViewState",!1);this.canvas=t.canvas,this.imageWidth=Math.max(1,t.imageWidth),this.imageHeight=Math.max(1,t.imageHeight),this.clearColor=t.clearColor??[.03,.05,.08,1],this.gl=Qt(this.canvas),this.program=Kt(this.gl,ee,ie);const i=this.gl.createVertexArray(),r=this.gl.createBuffer();if(!i||!r)throw new Error("Failed to create WebGL buffers.");this.vao=i,this.quadBuffer=r,this.gl.bindVertexArray(this.vao),this.gl.bindBuffer(this.gl.ARRAY_BUFFER,this.quadBuffer);const n=new Float32Array([0,0,0,0,1,0,1,0,0,1,0,1,1,1,1,1]);this.gl.bufferData(this.gl.ARRAY_BUFFER,n,this.gl.STATIC_DRAW);const o=this.gl.getAttribLocation(this.program,"aUnit"),a=this.gl.getAttribLocation(this.program,"aUv");if(o<0||a<0)throw new Error("Failed to get attribute locations.");const u=4*Float32Array.BYTES_PER_ELEMENT;this.gl.enableVertexAttribArray(o),this.gl.vertexAttribPointer(o,2,this.gl.FLOAT,!1,u,0),this.gl.enableVertexAttribArray(a),this.gl.vertexAttribPointer(a,2,this.gl.FLOAT,!1,u,2*Float32Array.BYTES_PER_ELEMENT),this.gl.bindVertexArray(null),this.gl.bindBuffer(this.gl.ARRAY_BUFFER,null),this.uCameraLocation=bt(this.gl,this.program,"uCamera"),this.uBoundsLocation=bt(this.gl,this.program,"uBounds"),this.uTextureLocation=bt(this.gl,this.program,"uTexture"),t.initialViewState&&(this.controlledViewState=!0,this.camera.setViewState(t.initialViewState)),this.resizeObserver=new ResizeObserver(()=>{this.resize()}),this.resizeObserver.observe(this.canvas),this.resize()}async setTiles(t){if(this.destroyed)return;const i=++this.loadVersion,r=await Promise.all(t.map(async n=>await this.loadTile(n,i)));if(this.destroyed||i!==this.loadVersion){for(const n of r)n&&this.gl.deleteTexture(n.texture);return}this.disposeTiles(this.tiles),this.tiles=r.filter(n=>n!==null),this.requestRender()}setViewState(t){this.controlledViewState=!0,this.camera.setViewState(t),this.requestRender()}getViewState(){return this.camera.getViewState()}destroy(){this.destroyed||(this.destroyed=!0,this.loadVersion+=1,this.frameId!==null&&(cancelAnimationFrame(this.frameId),this.frameId=null),this.resizeObserver.disconnect(),this.disposeTiles(this.tiles),this.tiles=[],this.gl.deleteBuffer(this.quadBuffer),this.gl.deleteVertexArray(this.vao),this.gl.deleteProgram(this.program))}async loadTile(t,i){try{const r=await fetch(t.url);if(!r.ok)throw new Error(`Tile fetch failed: ${r.status} ${r.statusText}`);const n=await r.blob(),o=await createImageBitmap(n);if(this.destroyed||i!==this.loadVersion)return o.close(),null;const a=this.gl.createTexture();if(!a)throw o.close(),new Error("Failed to create tile texture.");return this.gl.bindTexture(this.gl.TEXTURE_2D,a),this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL,1),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_S,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_T,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MIN_FILTER,this.gl.LINEAR),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MAG_FILTER,this.gl.LINEAR),this.gl.texImage2D(this.gl.TEXTURE_2D,0,this.gl.RGBA,this.gl.RGBA,this.gl.UNSIGNED_BYTE,o),this.gl.bindTexture(this.gl.TEXTURE_2D,null),o.close(),{id:t.id,bounds:t.bounds,texture:a}}catch(r){return console.error(`[M1TileRenderer] tile load failed: ${t.id}`,r),null}}resize(){if(this.destroyed)return;const t=this.canvas.getBoundingClientRect(),i=Math.max(1,t.width||this.canvas.clientWidth||1),r=Math.max(1,t.height||this.canvas.clientHeight||1),n=Math.max(1,window.devicePixelRatio||1),o=Math.max(1,Math.round(i*n)),a=Math.max(1,Math.round(r*n));(this.canvas.width!==o||this.canvas.height!==a)&&(this.canvas.width=o,this.canvas.height=a),this.camera.setViewport(i,r),this.gl.viewport(0,0,this.canvas.width,this.canvas.height),!this.fitted&&!this.controlledViewState&&(this.fitToImage(),this.fitted=!0),this.requestRender()}fitToImage(){const t=this.camera.getViewportSize(),i=Math.min(t.width/this.imageWidth,t.height/this.imageHeight),r=Number.isFinite(i)&&i>0?i:1,n=t.width/r,o=t.height/r,a=(this.imageWidth-n)*.5,u=(this.imageHeight-o)*.5;this.camera.setViewState({zoom:r,offsetX:a,offsetY:u})}requestRender(){this.frameId!==null||this.destroyed||(this.frameId=requestAnimationFrame(()=>{this.frameId=null,this.render()}))}render(){if(!this.destroyed){this.gl.clearColor(this.clearColor[0],this.clearColor[1],this.clearColor[2],this.clearColor[3]),this.gl.clear(this.gl.COLOR_BUFFER_BIT),this.gl.useProgram(this.program),this.gl.bindVertexArray(this.vao),this.gl.uniformMatrix3fv(this.uCameraLocation,!1,this.camera.getMatrix()),this.gl.uniform1i(this.uTextureLocation,0);for(const t of this.tiles)this.gl.activeTexture(this.gl.TEXTURE0),this.gl.bindTexture(this.gl.TEXTURE_2D,t.texture),this.gl.uniform4f(this.uBoundsLocation,t.bounds[0],t.bounds[1],t.bounds[2],t.bounds[3]),this.gl.drawArrays(this.gl.TRIANGLE_STRIP,0,4);this.gl.bindTexture(this.gl.TEXTURE_2D,null),this.gl.bindVertexArray(null)}}disposeTiles(t){for(const i of t)this.gl.deleteTexture(i.texture)}}const Rt=[160,160,160,255];function Y(e,t,i){return Math.max(t,Math.min(i,e))}function At(e,t,i){const r=Number(e),n=Number(t),o=Number(i);return!Number.isFinite(r)||r<=0?1:!Number.isFinite(n)||!Number.isFinite(o)?r:Math.pow(2,n-o)*r}function re(e,t,i){let n=100*At(e,t,i);if(Number(e)){let o="μm";return n>1e3&&(n/=1e3,o="mm"),`${n.toPrecision(3)} ${o}`}return`${Math.round(n*1e3)/1e3} pixels`}function ne(e,t){return!e&&!t?!0:!e||!t?!1:Math.abs((e.zoom??0)-(t.zoom??0))<1e-6&&Math.abs((e.offsetX??0)-(t.offsetX??0))<1e-6&&Math.abs((e.offsetY??0)-(t.offsetY??0))<1e-6}function oe(e){const t=String(e??"").trim();if(!t)return"";if(/^bearer\s+/i.test(t)){const i=t.replace(/^bearer\s+/i,"").trim();return i?`Bearer ${i}`:""}return`Bearer ${t}`}function Yt(e){const i=String(e??"").trim().match(/^#?([0-9a-fA-F]{6})$/);if(!i)return[...Rt];const r=Number.parseInt(i[1],16);return[r>>16&255,r>>8&255,r&255,255]}function se(e){const t=[[...Rt]],i=new Map;for(const n of e??[]){const o=String(n?.termId??"");!o||i.has(o)||(i.set(o,t.length),t.push(Yt(n?.termColor)))}const r=new Uint8Array(t.length*4);for(let n=0;n<t.length;n+=1)r[n*4]=t[n][0],r[n*4+1]=t[n][1],r[n*4+2]=t[n][2],r[n*4+3]=t[n][3];return{colors:r,termToPaletteIndex:i}}function It(e,t,i){const r=e.createShader(e.VERTEX_SHADER),n=e.createShader(e.FRAGMENT_SHADER);if(!r||!n)throw new Error("Shader allocation failed");if(e.shaderSource(r,t),e.compileShader(r),!e.getShaderParameter(r,e.COMPILE_STATUS))throw new Error(e.getShaderInfoLog(r)||"vertex compile failed");if(e.shaderSource(n,i),e.compileShader(n),!e.getShaderParameter(n,e.COMPILE_STATUS))throw new Error(e.getShaderInfoLog(n)||"fragment compile failed");const o=e.createProgram();if(!o)throw new Error("Program allocation failed");if(e.attachShader(o,r),e.attachShader(o,n),e.linkProgram(o),e.deleteShader(r),e.deleteShader(n),!e.getProgramParameter(o,e.LINK_STATUS))throw new Error(e.getProgramInfoLog(o)||"program link failed");return o}const ae="rgba(255, 77, 79, 0.16)",ce=3,le=2,Nt=96,ue=1,he=[],vt=[],_t=1e3,kt=2,Wt=2,fe=.2,ut={color:"#ff4d4f",width:2,lineJoin:"round",lineCap:"round",shadowColor:"rgba(0, 0, 0, 0)",shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0},rt={fontFamily:"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",fontSize:12,fontWeight:500,textColor:"#ffffff",backgroundColor:"rgba(8, 14, 22, 0.88)",borderColor:"rgba(255, 77, 79, 0.85)",borderWidth:1,paddingX:6,paddingY:4,offsetY:10,borderRadius:3};function wt(e,t,i){return Math.max(t,Math.min(i,e))}function mt(e){return e==="stamp-rectangle"||e==="stamp-circle"||e==="stamp-rectangle-2mm2"||e==="stamp-circle-2mm2"||e==="stamp-circle-hpf-0.2mm2"}function yt(e,t){return typeof e!="number"||!Number.isFinite(e)||e<=0?t:e}function de(e){return{rectangleAreaMm2:yt(e?.rectangleAreaMm2,kt),circleAreaMm2:yt(e?.circleAreaMm2,Wt)}}function me(e){return e*_t*_t}function ge(e,t){return!e||!Number.isFinite(t)||t<=0?[]:ct([[e[0]-t,e[1]-t],[e[0]+t,e[1]-t],[e[0]+t,e[1]+t],[e[0]-t,e[1]+t]])}function pe(e,t,i=Nt){if(!e||!Number.isFinite(t)||t<=0)return[];const r=[];for(let n=0;n<=i;n+=1){const o=n/i*Math.PI*2;r.push([e[0]+Math.cos(o)*t,e[1]+Math.sin(o)*t])}return ct(r)}function ct(e){if(!Array.isArray(e)||e.length<3)return[];const t=e.map(([n,o])=>[n,o]),i=t[0],r=t[t.length-1];return!i||!r?[]:((i[0]!==r[0]||i[1]!==r[1])&&t.push([i[0],i[1]]),t)}function Et(e,t){return!e||!t?[]:ct([[e[0],e[1]],[t[0],e[1]],[t[0],t[1]],[e[0],t[1]]])}function xt(e,t,i=Nt){if(!e||!t)return[];const r=(e[0]+t[0])*.5,n=(e[1]+t[1])*.5,o=Math.hypot(t[0]-e[0],t[1]-e[1])*.5;if(o<1)return[];const a=[];for(let u=0;u<=i;u+=1){const f=u/i*Math.PI*2;a.push([r+Math.cos(f)*o,n+Math.sin(f)*o])}return ct(a)}function St(e){if(!Array.isArray(e)||e.length<4)return 0;let t=0;for(let i=0;i<e.length-1;i+=1){const r=e[i],n=e[i+1];t+=r[0]*n[1]-n[0]*r[1]}return Math.abs(t*.5)}function zt(e){if(!Array.isArray(e)||e.length===0)return[0,0,0,0];let t=1/0,i=1/0,r=-1/0,n=-1/0;for(const[o,a]of e)o<t&&(t=o),o>r&&(r=o),a<i&&(i=a),a>n&&(n=a);return[t,i,r,n]}function Ft(e){return Array.isArray(e)&&e.length>=4&&St(e)>ue}function gt(e,t,i,r=!1,n=!1){if(t.length!==0){e.beginPath(),e.moveTo(t[0][0],t[0][1]);for(let o=1;o<t.length;o+=1)e.lineTo(t[o][0],t[o][1]);r&&e.closePath(),n&&r&&(e.fillStyle=ae,e.fill()),e.strokeStyle=i.color,e.lineWidth=i.width,e.lineJoin=i.lineJoin,e.lineCap=i.lineCap,e.shadowColor=i.shadowColor,e.shadowBlur=i.shadowBlur,e.shadowOffsetX=i.shadowOffsetX,e.shadowOffsetY=i.shadowOffsetY,e.setLineDash(i.lineDash),e.stroke(),e.setLineDash(vt),e.shadowColor="rgba(0, 0, 0, 0)",e.shadowBlur=0,e.shadowOffsetX=0,e.shadowOffsetY=0}}function Ot(e){const t=Array.isArray(e?.lineDash)?e.lineDash.filter(a=>Number.isFinite(a)&&a>=0):vt,i=typeof e?.width=="number"&&Number.isFinite(e.width)?Math.max(0,e.width):ut.width,r=typeof e?.shadowBlur=="number"&&Number.isFinite(e.shadowBlur)?Math.max(0,e.shadowBlur):ut.shadowBlur,n=typeof e?.shadowOffsetX=="number"&&Number.isFinite(e.shadowOffsetX)?e.shadowOffsetX:ut.shadowOffsetX,o=typeof e?.shadowOffsetY=="number"&&Number.isFinite(e.shadowOffsetY)?e.shadowOffsetY:ut.shadowOffsetY;return{color:e?.color||ut.color,width:i,lineDash:t.length?t:vt,lineJoin:e?.lineJoin||ut.lineJoin,lineCap:e?.lineCap||ut.lineCap,shadowColor:e?.shadowColor||ut.shadowColor,shadowBlur:r,shadowOffsetX:n,shadowOffsetY:o}}function Ut(e,t){return t?Ot({color:t.color??e.color,width:t.width??e.width,lineDash:t.lineDash??e.lineDash,lineJoin:t.lineJoin??e.lineJoin,lineCap:t.lineCap??e.lineCap,shadowColor:t.shadowColor??e.shadowColor,shadowBlur:t.shadowBlur??e.shadowBlur,shadowOffsetX:t.shadowOffsetX??e.shadowOffsetX,shadowOffsetY:t.shadowOffsetY??e.shadowOffsetY}):e}function Bt(e,t){return e==null||t===null||t===void 0?!1:String(e)===String(t)}function we(e){const t=typeof e?.paddingX=="number"&&Number.isFinite(e.paddingX)?Math.max(0,e.paddingX):rt.paddingX,i=typeof e?.paddingY=="number"&&Number.isFinite(e.paddingY)?Math.max(0,e.paddingY):rt.paddingY,r=typeof e?.fontSize=="number"&&Number.isFinite(e.fontSize)?Math.max(8,e.fontSize):rt.fontSize,n=typeof e?.borderWidth=="number"&&Number.isFinite(e.borderWidth)?Math.max(0,e.borderWidth):rt.borderWidth,o=typeof e?.offsetY=="number"&&Number.isFinite(e.offsetY)?e.offsetY:rt.offsetY,a=typeof e?.borderRadius=="number"&&Number.isFinite(e.borderRadius)?Math.max(0,e.borderRadius):rt.borderRadius;return{fontFamily:e?.fontFamily||rt.fontFamily,fontSize:r,fontWeight:e?.fontWeight||rt.fontWeight,textColor:e?.textColor||rt.textColor,backgroundColor:e?.backgroundColor||rt.backgroundColor,borderColor:e?.borderColor||rt.borderColor,borderWidth:n,paddingX:t,paddingY:i,offsetY:o,borderRadius:a}}function be(e,t,i,r,n,o){const a=Math.max(0,Math.min(o,r*.5,n*.5));e.beginPath(),e.moveTo(t+a,i),e.lineTo(t+r-a,i),e.quadraticCurveTo(t+r,i,t+r,i+a),e.lineTo(t+r,i+n-a),e.quadraticCurveTo(t+r,i+n,t+r-a,i+n),e.lineTo(t+a,i+n),e.quadraticCurveTo(t,i+n,t,i+n-a),e.lineTo(t,i+a),e.quadraticCurveTo(t,i,t+a,i),e.closePath()}function Te(e){if(!e.length)return null;let t=1/0;for(const n of e)n[1]<t&&(t=n[1]);if(!Number.isFinite(t))return null;let i=1/0,r=-1/0;for(const n of e)Math.abs(n[1]-t)>.5||(n[0]<i&&(i=n[0]),n[0]>r&&(r=n[0]));return!Number.isFinite(i)||!Number.isFinite(r)?null:[(i+r)*.5,t]}function ve(e,t,i,r,n,o){const a=t.trim();if(!a)return;e.save(),e.font=`${o.fontWeight} ${o.fontSize}px ${o.fontFamily}`,e.textAlign="center",e.textBaseline="middle";const f=e.measureText(a).width+o.paddingX*2,g=o.fontSize+o.paddingY*2,v=wt(i[0],f*.5+1,r-f*.5-1),S=wt(i[1]-o.offsetY,g*.5+1,n-g*.5-1),z=v-f*.5,H=S-g*.5;e.fillStyle=o.backgroundColor,e.strokeStyle=o.borderColor,e.lineWidth=o.borderWidth,be(e,z,H,f,g,o.borderRadius),e.fill(),o.borderWidth>0&&e.stroke(),e.fillStyle=o.textColor,e.fillText(a,v,S+.5),e.restore()}function Xt(e,t,i){return[wt(e[0],0,t),wt(e[1],0,i)]}function Tt(e){if(!Array.isArray(e)||e.length<2)return null;const t=Number(e[0]),i=Number(e[1]);return!Number.isFinite(t)||!Number.isFinite(i)?null:[t,i]}function Ht({tool:e,imageWidth:t,imageHeight:i,imageMpp:r,imageZoom:n,stampOptions:o,projectorRef:a,onDrawComplete:u,enabled:f,viewStateSignal:g,persistedRegions:v,persistedPolygons:S,regionStrokeStyle:z,regionStrokeHoverStyle:H,regionStrokeActiveStyle:Z,hoveredRegionId:j=null,activeRegionId:ot=null,regionLabelStyle:J,invalidateRef:L,className:K,style:D}){const _=c.useRef(null),et=c.useRef(!1),N=c.useRef(e),G=c.useRef({isDrawing:!1,pointerId:null,start:null,current:null,points:[],stampCenter:null}),P=f??e!=="cursor",W=c.useMemo(()=>v&&v.length>0?v:!S||S.length===0?he:S.map((s,l)=>({id:l,coordinates:s})),[v,S]),R=c.useMemo(()=>Ot(z),[z]),k=c.useMemo(()=>Ut(R,H),[R,H]),st=c.useMemo(()=>Ut(R,Z),[R,Z]),at=c.useMemo(()=>we(J),[J]),ht=c.useMemo(()=>de(o),[o]),lt=c.useMemo(()=>({position:"absolute",inset:0,zIndex:2,width:"100%",height:"100%",display:"block",touchAction:"none",pointerEvents:P?"auto":"none",cursor:P?"crosshair":"default",...D}),[P,D]),it=c.useCallback(()=>{const s=_.current;if(!s)return;const l=s.getBoundingClientRect(),m=Math.max(1,window.devicePixelRatio||1),E=Math.max(1,Math.round(l.width*m)),A=Math.max(1,Math.round(l.height*m));(s.width!==E||s.height!==A)&&(s.width=E,s.height=A)},[]),d=c.useCallback(s=>{const l=a.current;if(!l||s.length===0)return[];const m=new Array(s.length);for(let E=0;E<s.length;E+=1){const A=Tt(l.worldToScreen(s[E][0],s[E][1]));if(!A)return[];m[E]=A}return m},[a]),w=c.useCallback(s=>{if(!Number.isFinite(s)||s<=0)return 0;const l=typeof r=="number"&&Number.isFinite(r)&&r>0?r:1,m=typeof n=="number"&&Number.isFinite(n)?n:0,E=a.current?.getViewState?.().zoom,A=typeof E=="number"&&Number.isFinite(E)&&E>0?E:1,h=m+Math.log2(A),p=Math.max(1e-9,At(l,m,h));return s/p/A},[r,n,a]),b=c.useCallback((s,l)=>{if(!l)return[];let m=0;if(s==="stamp-rectangle"||s==="stamp-rectangle-2mm2"?m=s==="stamp-rectangle-2mm2"?kt:ht.rectangleAreaMm2:(s==="stamp-circle"||s==="stamp-circle-2mm2"||s==="stamp-circle-hpf-0.2mm2")&&(m=s==="stamp-circle-hpf-0.2mm2"?fe:s==="stamp-circle-2mm2"?Wt:ht.circleAreaMm2),!Number.isFinite(m)||m<=0)return[];const E=me(m);let A=[];if(s==="stamp-rectangle"||s==="stamp-rectangle-2mm2"){const h=w(Math.sqrt(E)*.5);A=ge(l,h)}else if(s==="stamp-circle"||s==="stamp-circle-2mm2"||s==="stamp-circle-hpf-0.2mm2"){const h=w(Math.sqrt(E/Math.PI));A=pe(l,h)}return A.length?A.map(h=>Xt(h,t,i)):[]},[w,t,i,ht]),x=c.useCallback(()=>{const s=G.current;return mt(e)?b(e,s.stampCenter):s.isDrawing?e==="freehand"?s.points:e==="rectangle"?Et(s.start,s.current):e==="circular"?xt(s.start,s.current):[]:[]},[e,b]),F=c.useCallback(()=>{it();const s=_.current;if(!s)return;const l=s.getContext("2d");if(!l)return;const m=Math.max(1,window.devicePixelRatio||1),E=s.width/m,A=s.height/m;if(l.setTransform(1,0,0,1,0,0),l.clearRect(0,0,s.width,s.height),l.setTransform(m,0,0,m,0,0),W.length>0)for(let h=0;h<W.length;h+=1){const p=W[h],M=p?.coordinates;if(!M||M.length<3)continue;const I=ct(M),O=d(I);if(O.length>=4){const dt=p.id??h,$t=Bt(ot,dt)?st:Bt(j,dt)?k:R;gt(l,O,$t,!0,!1)}}if(P){const h=x();if(h.length>0)if(e==="freehand"){const p=d(h);p.length>=2&>(l,p,R,!1,!1),p.length>=3&>(l,d(ct(h)),R,!0,!0)}else{const p=d(h);p.length>=4&>(l,p,R,!0,!0)}}if(W.length>0)for(const h of W){if(!h.label)continue;const p=h?.coordinates;if(!p||p.length<3)continue;const M=ct(p),I=Te(M);if(!I)continue;const O=Tt(a.current?.worldToScreen(I[0],I[1])??[]);O&&ve(l,h.label,O,E,A,at)}},[P,e,x,it,d,a,W,j,ot,R,k,st,at]),T=c.useCallback(()=>{et.current||(et.current=!0,requestAnimationFrame(()=>{et.current=!1,F()}))},[F]),V=c.useCallback(()=>{const s=G.current,l=_.current;if(l&&s.pointerId!==null&&l.hasPointerCapture(s.pointerId))try{l.releasePointerCapture(s.pointerId)}catch{}s.isDrawing=!1,s.pointerId=null,s.start=null,s.current=null,s.points=[],s.stampCenter=null},[]),U=c.useCallback(s=>{const l=a.current;if(!l||t<=0||i<=0)return null;const m=Tt(l.screenToWorld(s.clientX,s.clientY));return m?Xt(m,t,i):null},[a,t,i]),q=c.useCallback(()=>{const s=G.current;if(!s.isDrawing){V(),T();return}let l=[];e==="freehand"?s.points.length>=ce&&(l=ct(s.points)):e==="rectangle"?l=Et(s.start,s.current):e==="circular"&&(l=xt(s.start,s.current)),(e==="freehand"||e==="rectangle"||e==="circular")&&Ft(l)&&u&&u({tool:e,coordinates:l,bbox:zt(l),areaPx:St(l)}),V(),T()},[e,u,V,T]),Q=c.useCallback((s,l)=>{const m=b(s,l);!Ft(m)||!u||u({tool:s,coordinates:m,bbox:zt(m),areaPx:St(m)})},[b,u]),$=c.useCallback(s=>{if(!P||e==="cursor"||s.button!==0)return;const l=U(s);if(!l)return;if(s.preventDefault(),s.stopPropagation(),mt(e)){const A=G.current;A.stampCenter=l,Q(e,l),T();return}const m=_.current;m&&m.setPointerCapture(s.pointerId);const E=G.current;E.isDrawing=!0,E.pointerId=s.pointerId,E.start=l,E.current=l,E.points=e==="freehand"?[l]:[],T()},[P,e,U,Q,T]),C=c.useCallback(s=>{if(!P||e==="cursor")return;const l=U(s);if(!l)return;if(mt(e)){const E=G.current;E.stampCenter=l,s.preventDefault(),s.stopPropagation(),T();return}const m=G.current;if(!(!m.isDrawing||m.pointerId!==s.pointerId)){if(s.preventDefault(),s.stopPropagation(),e==="freehand"){const E=a.current,A=Math.max(1e-6,E?.getViewState?.().zoom??1),h=le/A,p=h*h,M=m.points[m.points.length-1];if(!M)m.points.push(l);else{const I=l[0]-M[0],O=l[1]-M[1];I*I+O*O>=p&&m.points.push(l)}}else m.current=l;T()}},[P,e,U,T,a]),B=c.useCallback(s=>{const l=G.current;if(!l.isDrawing||l.pointerId!==s.pointerId)return;s.preventDefault(),s.stopPropagation();const m=_.current;if(m&&m.hasPointerCapture(s.pointerId))try{m.releasePointerCapture(s.pointerId)}catch{}q()},[q]),X=c.useCallback(()=>{if(!mt(e))return;const s=G.current;s.stampCenter&&(s.stampCenter=null,T())},[e,T]);return c.useEffect(()=>{it(),T();const s=_.current;if(!s)return;const l=new ResizeObserver(()=>{it(),T()});return l.observe(s),()=>{l.disconnect()}},[it,T]),c.useEffect(()=>{P||V(),T()},[P,T,V]),c.useEffect(()=>{N.current!==e&&(N.current=e,V(),T())},[e,V,T]),c.useEffect(()=>{T()},[g,W,T]),c.useEffect(()=>{if(L)return L.current=T,()=>{L.current===T&&(L.current=null)}},[L,T]),c.useEffect(()=>{if(!P)return;const s=l=>{l.key==="Escape"&&(V(),T())};return window.addEventListener("keydown",s),()=>{window.removeEventListener("keydown",s)}},[P,V,T]),nt.jsx("canvas",{ref:_,className:K,style:lt,onPointerDown:$,onPointerMove:C,onPointerUp:B,onPointerCancel:B,onPointerLeave:X,onContextMenu:s=>{P&&s.preventDefault()},onWheel:s=>{P&&s.preventDefault()}})}function Ee(e,t){const i=e?.imsInfo||{},r=Number(i.width??e?.width??0),n=Number(i.height??e?.height??0),o=Number(i.tileSize??e?.tileSize??0),a=Number(i.zoom??e?.zoom??0),u=String(i.path??e?.path??""),f=Number(i.mpp??e?.mpp??0);if(!r||!n||!o||!u)throw new Error("이미지 메타데이터가 불완전합니다. width/height/tileSize/path 확인 필요");const g=Array.isArray(e?.terms)?e.terms.map(v=>({termId:String(v?.termId??""),termName:String(v?.termName??""),termColor:String(v?.termColor??"")})):[];return{id:e?._id||"unknown",name:e?.name||"unknown",width:r,height:n,mpp:Number.isFinite(f)&&f>0?f:void 0,tileSize:o,maxTierZoom:Number.isFinite(a)?Math.max(0,Math.floor(a)):0,tilePath:u,tileBaseUrl:t,terms:g}}function Ct(e,t,i,r){const n=e.tilePath.startsWith("/")?e.tilePath:`/${e.tilePath}`;return`${e.tileBaseUrl}${n}/${t}/${r}_${i}.webp`}const tt={width:220,height:140,margin:16,position:"bottom-right",borderRadius:10,borderWidth:1.5,backgroundColor:"rgba(4, 10, 18, 0.88)",borderColor:"rgba(230, 244, 255, 0.35)",viewportStrokeColor:"rgba(255, 106, 61, 0.95)",viewportFillColor:"rgba(255, 106, 61, 0.2)",interactive:!0,showThumbnail:!0,maxThumbnailTiles:16};function ft(e,t,i=1){return typeof e!="number"||!Number.isFinite(e)?t:Math.max(i,e)}function pt(e){return Array.isArray(e)&&e.length===4&&Number.isFinite(e[0])&&Number.isFinite(e[1])&&Number.isFinite(e[2])&&Number.isFinite(e[3])}function Gt({source:e,projectorRef:t,authToken:i="",options:r,invalidateRef:n,className:o,style:a}){const u=c.useRef(null),f=c.useRef(null),g=c.useRef(null),v=c.useRef({active:!1,pointerId:null}),S=c.useRef(null),z=c.useRef(!1),H=ft(r?.width,tt.width,64),Z=ft(r?.height,tt.height,48),j=ft(r?.margin,tt.margin,0),ot=ft(r?.borderRadius,tt.borderRadius,0),J=ft(r?.borderWidth,tt.borderWidth,0),L=Math.max(1,Math.round(ft(r?.maxThumbnailTiles,tt.maxThumbnailTiles,1))),K=r?.backgroundColor||tt.backgroundColor,D=r?.borderColor||tt.borderColor,_=r?.viewportStrokeColor||tt.viewportStrokeColor,et=r?.viewportFillColor||tt.viewportFillColor,N=r?.interactive??tt.interactive,G=r?.showThumbnail??tt.showThumbnail,P=r?.position||tt.position,W=c.useMemo(()=>{const d={};return P==="top-left"||P==="bottom-left"?d.left=j:d.right=j,P==="top-left"||P==="top-right"?d.top=j:d.bottom=j,{position:"absolute",...d,width:H,height:Z,borderRadius:ot,overflow:"hidden",zIndex:4,pointerEvents:N?"auto":"none",touchAction:"none",boxShadow:"0 10px 22px rgba(0, 0, 0, 0.3)",...a}},[j,P,H,Z,ot,N,a]),R=c.useCallback(()=>{const d=u.current;if(!d)return;const w=d.getContext("2d");if(!w)return;const b=H,x=Z,F=Math.max(1,window.devicePixelRatio||1),T=Math.max(1,Math.round(b*F)),V=Math.max(1,Math.round(x*F));(d.width!==T||d.height!==V)&&(d.width=T,d.height=V),w.setTransform(1,0,0,1,0,0),w.clearRect(0,0,d.width,d.height),w.setTransform(F,0,0,F,0,0),w.fillStyle=K,w.fillRect(0,0,b,x);const U=f.current;U&&w.drawImage(U,0,0,b,x),w.strokeStyle=D,w.lineWidth=J,w.strokeRect(J*.5,J*.5,b-J,x-J);const Q=t.current?.getViewBounds?.(),$=pt(Q)?Q:pt(g.current)?g.current:null;if(!$)return;g.current=$;const C=b/Math.max(1,e.width),B=x/Math.max(1,e.height),X=Y($[0]*C,0,b),s=Y($[1]*B,0,x),l=Y($[2]*C,0,b),m=Y($[3]*B,0,x),E=Math.max(1,l-X),A=Math.max(1,m-s);w.fillStyle=et,w.fillRect(X,s,E,A),w.strokeStyle=_,w.lineWidth=1.5,w.strokeRect(X+.5,s+.5,Math.max(1,E-1),Math.max(1,A-1))},[H,Z,K,D,J,t,e.width,e.height,et,_]),k=c.useCallback(()=>{z.current||(z.current=!0,S.current=requestAnimationFrame(()=>{z.current=!1,S.current=null,R()}))},[R]),st=c.useCallback((d,w)=>{const b=u.current;if(!b)return null;const x=b.getBoundingClientRect();if(!x.width||!x.height)return null;const F=Y((d-x.left)/x.width,0,1),T=Y((w-x.top)/x.height,0,1);return[F*e.width,T*e.height]},[e.width,e.height]),at=c.useCallback((d,w)=>{const b=t.current;if(!b)return;const x=b.getViewBounds?.(),F=pt(x)?x:pt(g.current)?g.current:null;if(!F)return;const T=Math.max(1e-6,F[2]-F[0]),V=Math.max(1e-6,F[3]-F[1]);b.setViewState({offsetX:d-T*.5,offsetY:w-V*.5}),k()},[t,k]),ht=c.useCallback(d=>{if(!N||d.button!==0)return;const w=u.current;if(!w)return;const b=st(d.clientX,d.clientY);b&&(d.preventDefault(),d.stopPropagation(),w.setPointerCapture(d.pointerId),v.current={active:!0,pointerId:d.pointerId},at(b[0],b[1]))},[N,st,at]),lt=c.useCallback(d=>{const w=v.current;if(!w.active||w.pointerId!==d.pointerId)return;const b=st(d.clientX,d.clientY);b&&(d.preventDefault(),d.stopPropagation(),at(b[0],b[1]))},[st,at]),it=c.useCallback(d=>{const w=v.current;if(!w.active||w.pointerId!==d.pointerId)return;const b=u.current;if(b&&b.hasPointerCapture(d.pointerId))try{b.releasePointerCapture(d.pointerId)}catch{}v.current={active:!1,pointerId:null},k()},[k]);return c.useEffect(()=>{let d=!1;f.current=null,k();const w=0,b=2**(e.maxTierZoom-w),x=Math.ceil(e.width/b),F=Math.ceil(e.height/b),T=Math.max(1,Math.ceil(x/e.tileSize)),V=Math.max(1,Math.ceil(F/e.tileSize)),U=T*V;if(!G||U>L)return;const q=document.createElement("canvas");q.width=Math.max(1,Math.round(H)),q.height=Math.max(1,Math.round(Z));const Q=q.getContext("2d");if(!Q)return;Q.fillStyle=K,Q.fillRect(0,0,q.width,q.height);const $=[];for(let C=0;C<V;C+=1)for(let B=0;B<T;B+=1){const X=B*e.tileSize*b,s=C*e.tileSize*b,l=Math.min((B+1)*e.tileSize,x)*b,m=Math.min((C+1)*e.tileSize,F)*b;$.push({url:Ct(e,w,B,C),bounds:[X,s,l,m]})}return Promise.allSettled($.map(async C=>{const B=!!i,X=await fetch(C.url,{headers:B?{Authorization:i}:void 0});if(!X.ok)throw new Error(`HTTP ${X.status}`);const s=await createImageBitmap(await X.blob());return{tile:C,bitmap:s}})).then(C=>{if(d){for(const s of C)s.status==="fulfilled"&&s.value.bitmap.close();return}const B=q.width/Math.max(1,e.width),X=q.height/Math.max(1,e.height);for(const s of C){if(s.status!=="fulfilled")continue;const{tile:{bounds:l},bitmap:m}=s.value,E=l[0]*B,A=l[1]*X,h=Math.max(1,(l[2]-l[0])*B),p=Math.max(1,(l[3]-l[1])*X);Q.drawImage(m,E,A,h,p),m.close()}f.current=q,k()}),()=>{d=!0}},[e,i,H,Z,K,G,L,k]),c.useEffect(()=>{k()},[k]),c.useEffect(()=>{if(n)return n.current=k,()=>{n.current===k&&(n.current=null)}},[n,k]),c.useEffect(()=>()=>{v.current={active:!1,pointerId:null},S.current!==null&&(cancelAnimationFrame(S.current),S.current=null),z.current=!1},[]),nt.jsx("canvas",{ref:u,className:o,style:W,onPointerDown:ht,onPointerMove:lt,onPointerUp:it,onPointerCancel:it,onContextMenu:d=>{d.preventDefault()},onWheel:d=>{d.preventDefault(),d.stopPropagation()}})}function xe({imageWidth:e,imageHeight:t,tiles:i,viewState:r,className:n,style:o}){const a=c.useRef(null),u=c.useRef(null),f=c.useMemo(()=>({width:"100%",height:"100%",display:"block",...o}),[o]);return c.useEffect(()=>{const g=a.current;if(!g)return;const v=new Vt({canvas:g,imageWidth:e,imageHeight:t,initialViewState:r});return u.current=v,v.setTiles(i),()=>{v.destroy(),u.current=null}},[e,t]),c.useEffect(()=>{const g=u.current;g&&g.setTiles(i)},[i]),c.useEffect(()=>{const g=u.current;!g||!r||g.setViewState(r)},[r]),nt.jsx("canvas",{ref:a,className:n,style:f})}function Se(e){if(!Array.isArray(e)||e.length<3)return[];const t=e.map(([n,o])=>[n,o]),i=t[0],r=t[t.length-1];return!i||!r?[]:((i[0]!==r[0]||i[1]!==r[1])&&t.push([i[0],i[1]]),t)}function Pe(e){const t=[];for(const i of e??[]){const r=Se(i);if(r.length<4)continue;let n=1/0,o=1/0,a=-1/0,u=-1/0;for(const[f,g]of r)f<n&&(n=f),f>a&&(a=f),g<o&&(o=g),g>u&&(u=g);!Number.isFinite(n)||!Number.isFinite(o)||t.push({ring:r,minX:n,minY:o,maxX:a,maxY:u})}return t}function Re(e,t,i){let r=!1;for(let n=0,o=i.length-1;n<i.length;o=n,n+=1){const a=i[n][0],u=i[n][1],f=i[o][0],g=i[o][1];u>t!=g>t&&e<(f-a)*(t-u)/(g-u||Number.EPSILON)+a&&(r=!r)}return r}function Ae(e,t,i){for(const r of i)if(!(e<r.minX||e>r.maxX||t<r.minY||t>r.maxY)&&Re(e,t,r.ring))return!0;return!1}function qt(e,t){if(!e||!e.count||!e.positions||!e.paletteIndices)return null;const i=Pe(t??[]);if(i.length===0)return{count:0,positions:new Float32Array(0),paletteIndices:new Uint16Array(0)};const r=e.count,n=e.positions,o=e.paletteIndices,a=new Float32Array(r*2),u=new Uint16Array(r);let f=0;for(let g=0;g<r;g+=1){const v=n[g*2],S=n[g*2+1];Ae(v,S,i)&&(a[f*2]=v,a[f*2+1]=S,u[f]=o[g],f+=1)}return{count:f,positions:a.subarray(0,f*2),paletteIndices:u.subarray(0,f)}}class Ce{constructor(){this.viewportWidth=1,this.viewportHeight=1,this.viewState={zoom:1,offsetX:0,offsetY:0}}setViewport(t,i){this.viewportWidth=Math.max(1,t),this.viewportHeight=Math.max(1,i)}getViewport(){return{width:this.viewportWidth,height:this.viewportHeight}}setViewState(t){typeof t.zoom=="number"&&(this.viewState.zoom=Math.max(1e-4,t.zoom)),typeof t.offsetX=="number"&&(this.viewState.offsetX=t.offsetX),typeof t.offsetY=="number"&&(this.viewState.offsetY=t.offsetY)}getViewState(){return{...this.viewState}}getMatrix(){const t=this.viewportWidth/this.viewState.zoom,i=this.viewportHeight/this.viewState.zoom,r=2/t,n=-2/i,o=-1-this.viewState.offsetX*r,a=1-this.viewState.offsetY*n;return new Float32Array([r,0,0,0,n,0,o,a,1])}}class Zt{constructor(t,i,r={}){this.canvas=t,this.source=i,this.onViewStateChange=r.onViewStateChange,this.onStats=r.onStats,this.authToken=r.authToken||"",this.destroyed=!1,this.frame=null,this.frameSerial=0,this.dragging=!1,this.pointerId=null,this.lastPointerX=0,this.lastPointerY=0,this.interactionLocked=!1,this.cache=new Map,this.inflight=new Map,this.maxCacheTiles=320,this.fitZoom=1,this.minZoom=1e-6,this.maxZoom=1,this.currentTier=0,this.pointCount=0,this.pointPaletteSize=1;const n=t.getContext("webgl2",{alpha:!1,antialias:!1,depth:!1,stencil:!1,powerPreference:"high-performance"});if(!n)throw new Error("WebGL2 not supported");this.gl=n,this.camera=new Ce,this.initTileProgram(),this.initPointProgram(),this.resizeObserver=new ResizeObserver(()=>this.resize()),this.resizeObserver.observe(t),this.boundPointerDown=o=>this.onPointerDown(o),this.boundPointerMove=o=>this.onPointerMove(o),this.boundPointerUp=o=>this.onPointerUp(o),this.boundWheel=o=>this.onWheel(o),this.boundDoubleClick=o=>this.onDoubleClick(o),t.addEventListener("pointerdown",this.boundPointerDown),t.addEventListener("pointermove",this.boundPointerMove),t.addEventListener("pointerup",this.boundPointerUp),t.addEventListener("pointercancel",this.boundPointerUp),t.addEventListener("wheel",this.boundWheel,{passive:!1}),t.addEventListener("dblclick",this.boundDoubleClick),this.fitToImage(),this.resize()}initTileProgram(){const t=this.gl,i=`#version 300 es
|
|
33
|
+
precision highp float;
|
|
34
|
+
in vec2 aUnit;
|
|
35
|
+
in vec2 aUv;
|
|
36
|
+
uniform mat3 uCamera;
|
|
37
|
+
uniform vec4 uBounds;
|
|
38
|
+
out vec2 vUv;
|
|
39
|
+
void main() {
|
|
40
|
+
vec2 world = vec2(
|
|
41
|
+
mix(uBounds.x, uBounds.z, aUnit.x),
|
|
42
|
+
mix(uBounds.y, uBounds.w, aUnit.y)
|
|
43
|
+
);
|
|
44
|
+
vec3 clip = uCamera * vec3(world, 1.0);
|
|
45
|
+
gl_Position = vec4(clip.xy, 0.0, 1.0);
|
|
46
|
+
vUv = aUv;
|
|
47
|
+
}`,r=`#version 300 es
|
|
48
|
+
precision highp float;
|
|
49
|
+
in vec2 vUv;
|
|
50
|
+
uniform sampler2D uTexture;
|
|
51
|
+
out vec4 outColor;
|
|
52
|
+
void main() {
|
|
53
|
+
outColor = texture(uTexture, vUv);
|
|
54
|
+
}`;if(this.program=It(t,i,r),this.uCamera=t.getUniformLocation(this.program,"uCamera"),this.uBounds=t.getUniformLocation(this.program,"uBounds"),this.uTexture=t.getUniformLocation(this.program,"uTexture"),!this.uCamera||!this.uBounds||!this.uTexture)throw new Error("uniform location lookup failed");if(this.vao=t.createVertexArray(),this.vbo=t.createBuffer(),!this.vao||!this.vbo)throw new Error("buffer allocation failed");t.bindVertexArray(this.vao),t.bindBuffer(t.ARRAY_BUFFER,this.vbo),t.bufferData(t.ARRAY_BUFFER,new Float32Array([0,0,0,0,1,0,1,0,0,1,0,1,1,1,1,1]),t.STATIC_DRAW);const n=t.getAttribLocation(this.program,"aUnit"),o=t.getAttribLocation(this.program,"aUv");t.enableVertexAttribArray(n),t.enableVertexAttribArray(o),t.vertexAttribPointer(n,2,t.FLOAT,!1,16,0),t.vertexAttribPointer(o,2,t.FLOAT,!1,16,8),t.bindVertexArray(null),t.bindBuffer(t.ARRAY_BUFFER,null)}initPointProgram(){const t=this.gl,i=`#version 300 es
|
|
55
|
+
precision highp float;
|
|
56
|
+
in vec2 aPosition;
|
|
57
|
+
in uint aTerm;
|
|
58
|
+
uniform mat3 uCamera;
|
|
59
|
+
uniform float uPointSize;
|
|
60
|
+
flat out uint vTerm;
|
|
61
|
+
void main() {
|
|
62
|
+
vec3 clip = uCamera * vec3(aPosition, 1.0);
|
|
63
|
+
gl_Position = vec4(clip.xy, 0.0, 1.0);
|
|
64
|
+
gl_PointSize = uPointSize;
|
|
65
|
+
vTerm = aTerm;
|
|
66
|
+
}`,r=`#version 300 es
|
|
67
|
+
precision highp float;
|
|
68
|
+
flat in uint vTerm;
|
|
69
|
+
uniform sampler2D uPalette;
|
|
70
|
+
uniform float uPaletteSize;
|
|
71
|
+
uniform float uPointSize;
|
|
72
|
+
out vec4 outColor;
|
|
73
|
+
void main() {
|
|
74
|
+
vec2 pc = gl_PointCoord * 2.0 - 1.0;
|
|
75
|
+
float r = length(pc);
|
|
76
|
+
if (r > 1.0) discard;
|
|
77
|
+
|
|
78
|
+
float idx = clamp(float(vTerm), 0.0, max(0.0, uPaletteSize - 1.0));
|
|
79
|
+
vec2 uv = vec2((idx + 0.5) / uPaletteSize, 0.5);
|
|
80
|
+
vec4 color = texture(uPalette, uv);
|
|
81
|
+
if (color.a <= 0.0) discard;
|
|
82
|
+
|
|
83
|
+
float ringWidth = clamp(3.0 / max(1.0, uPointSize), 0.12, 0.62);
|
|
84
|
+
float innerRadius = 1.0 - ringWidth;
|
|
85
|
+
float aa = 1.5 / max(1.0, uPointSize);
|
|
86
|
+
|
|
87
|
+
float outerMask = 1.0 - smoothstep(1.0 - aa, 1.0 + aa, r);
|
|
88
|
+
float innerMask = smoothstep(innerRadius - aa, innerRadius + aa, r);
|
|
89
|
+
float alpha = outerMask * innerMask * color.a;
|
|
90
|
+
if (alpha <= 0.001) discard;
|
|
91
|
+
|
|
92
|
+
outColor = vec4(color.rgb * alpha, alpha);
|
|
93
|
+
}`;if(this.pointProgram=It(t,i,r),this.uPointCamera=t.getUniformLocation(this.pointProgram,"uCamera"),this.uPointSize=t.getUniformLocation(this.pointProgram,"uPointSize"),this.uPointPalette=t.getUniformLocation(this.pointProgram,"uPalette"),this.uPointPaletteSize=t.getUniformLocation(this.pointProgram,"uPaletteSize"),!this.uPointCamera||!this.uPointSize||!this.uPointPalette||!this.uPointPaletteSize)throw new Error("point uniform location lookup failed");if(this.pointVao=t.createVertexArray(),this.pointPosBuffer=t.createBuffer(),this.pointTermBuffer=t.createBuffer(),this.pointPaletteTexture=t.createTexture(),!this.pointVao||!this.pointPosBuffer||!this.pointTermBuffer||!this.pointPaletteTexture)throw new Error("point buffer allocation failed");t.bindVertexArray(this.pointVao),t.bindBuffer(t.ARRAY_BUFFER,this.pointPosBuffer),t.bufferData(t.ARRAY_BUFFER,0,t.DYNAMIC_DRAW);const n=t.getAttribLocation(this.pointProgram,"aPosition");if(n<0)throw new Error("point position attribute not found");t.enableVertexAttribArray(n),t.vertexAttribPointer(n,2,t.FLOAT,!1,0,0),t.bindBuffer(t.ARRAY_BUFFER,this.pointTermBuffer),t.bufferData(t.ARRAY_BUFFER,0,t.DYNAMIC_DRAW);const o=t.getAttribLocation(this.pointProgram,"aTerm");if(o<0)throw new Error("point term attribute not found");t.enableVertexAttribArray(o),t.vertexAttribIPointer(o,1,t.UNSIGNED_SHORT,0,0),t.bindVertexArray(null),t.bindBuffer(t.ARRAY_BUFFER,null),t.bindTexture(t.TEXTURE_2D,this.pointPaletteTexture),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.NEAREST),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MAG_FILTER,t.NEAREST),t.texImage2D(t.TEXTURE_2D,0,t.RGBA,1,1,0,t.RGBA,t.UNSIGNED_BYTE,new Uint8Array([160,160,160,255])),t.bindTexture(t.TEXTURE_2D,null)}setViewState(t){const i={...t};typeof i.zoom=="number"&&(i.zoom=Y(i.zoom,this.minZoom,this.maxZoom)),this.camera.setViewState(i),this.clampViewState(),this.emitViewState(),this.requestRender()}getViewState(){return this.camera.getViewState()}setPointPalette(t){if(!t||!t.length)return;const i=this.gl,r=Math.max(1,Math.floor(t.length/4));this.pointPaletteSize=r,i.bindTexture(i.TEXTURE_2D,this.pointPaletteTexture),i.texImage2D(i.TEXTURE_2D,0,i.RGBA,r,1,0,i.RGBA,i.UNSIGNED_BYTE,t),i.bindTexture(i.TEXTURE_2D,null),this.requestRender()}setPointData(t){const i=this.gl;if(!t||!t.count||!t.positions||!t.paletteIndices){this.pointCount=0,this.requestRender();return}i.bindBuffer(i.ARRAY_BUFFER,this.pointPosBuffer),i.bufferData(i.ARRAY_BUFFER,t.positions,i.STATIC_DRAW),i.bindBuffer(i.ARRAY_BUFFER,this.pointTermBuffer),i.bufferData(i.ARRAY_BUFFER,t.paletteIndices,i.STATIC_DRAW),i.bindBuffer(i.ARRAY_BUFFER,null),this.pointCount=t.count,this.requestRender()}setInteractionLock(t){const i=!!t;this.interactionLocked!==i&&(this.interactionLocked=i,i&&this.cancelDrag())}cancelDrag(){if(this.pointerId!==null&&this.canvas.hasPointerCapture(this.pointerId))try{this.canvas.releasePointerCapture(this.pointerId)}catch{}this.dragging=!1,this.pointerId=null,this.canvas.classList.remove("dragging")}screenToWorld(t,i){const r=this.canvas.getBoundingClientRect(),n=t-r.left,o=i-r.top,a=this.camera.getViewState();return[a.offsetX+n/a.zoom,a.offsetY+o/a.zoom]}worldToScreen(t,i){const r=this.camera.getViewState();return[(t-r.offsetX)*r.zoom,(i-r.offsetY)*r.zoom]}getPointSizeByZoom(){const t=Math.max(1e-6,this.camera.getViewState().zoom),i=this.source.maxTierZoom+Math.log2(t),r=[[1,2.6],[2,3.1],[3,3.8],[4,4.8],[5,6.1],[6,7.4],[7,8.4],[8,9],[9,11.5],[10,14.5],[11,18],[12,22]];let n=r[0][1];for(let a=1;a<r.length;a+=1){const[u,f]=r[a-1],[g,v]=r[a];if(i<=u)break;const S=Y((i-u)/Math.max(1e-6,g-u),0,1);n=f+(v-f)*S}const o=r[r.length-1];return i>o[0]&&(n+=(i-o[0])*4),Y(n,2.2,36)}fitToImage(){const t=this.canvas.getBoundingClientRect(),i=Math.max(1,t.width||1),r=Math.max(1,t.height||1),n=Math.min(i/this.source.width,r/this.source.height),o=Number.isFinite(n)&&n>0?n:1;this.fitZoom=o,this.minZoom=Math.max(this.fitZoom*.5,1e-6),this.maxZoom=Math.max(1,this.fitZoom*8),this.minZoom>this.maxZoom&&(this.minZoom=this.maxZoom);const a=i/o,u=r/o;this.camera.setViewState({zoom:Y(o,this.minZoom,this.maxZoom),offsetX:(this.source.width-a)*.5,offsetY:(this.source.height-u)*.5}),this.clampViewState(),this.emitViewState(),this.requestRender()}zoomBy(t,i,r){const n=this.camera.getViewState(),o=Y(n.zoom*t,this.minZoom,this.maxZoom);if(o===n.zoom)return;const a=n.offsetX+i/n.zoom,u=n.offsetY+r/n.zoom;this.camera.setViewState({zoom:o,offsetX:a-i/o,offsetY:u-r/o}),this.clampViewState(),this.emitViewState(),this.requestRender()}clampViewState(){const t=this.camera.getViewState(),i=this.camera.getViewport(),r=i.width/t.zoom,n=i.height/t.zoom,o=r*.2,a=n*.2,u=-o,f=this.source.width-r+o,g=-a,v=this.source.height-n+a;this.camera.setViewState({offsetX:Y(t.offsetX,u,f),offsetY:Y(t.offsetY,g,v)})}emitViewState(){typeof this.onViewStateChange=="function"&&this.onViewStateChange(this.camera.getViewState())}selectTier(){const t=Math.max(1e-6,this.camera.getViewState().zoom),i=this.source.maxTierZoom+Math.log2(t);return Y(Math.floor(i),0,this.source.maxTierZoom)}getViewBounds(){const t=this.camera.getViewState(),i=this.camera.getViewport();return[t.offsetX,t.offsetY,t.offsetX+i.width/t.zoom,t.offsetY+i.height/t.zoom]}intersectsBounds(t,i){return!(t[2]<=i[0]||t[0]>=i[2]||t[3]<=i[1]||t[1]>=i[3])}getVisibleTiles(){const t=this.selectTier();this.currentTier=t;const i=this.camera.getViewState(),r=this.camera.getViewport(),n=Math.pow(2,this.source.maxTierZoom-t),o=Math.ceil(this.source.width/n),a=Math.ceil(this.source.height/n),u=Math.max(1,Math.ceil(o/this.source.tileSize)),f=Math.max(1,Math.ceil(a/this.source.tileSize)),g=i.offsetX,v=i.offsetY,S=i.offsetX+r.width/i.zoom,z=i.offsetY+r.height/i.zoom,H=Y(Math.floor(g/n/this.source.tileSize),0,u-1),Z=Y(Math.floor((S-1)/n/this.source.tileSize),0,u-1),j=Y(Math.floor(v/n/this.source.tileSize),0,f-1),ot=Y(Math.floor((z-1)/n/this.source.tileSize),0,f-1);if(H>Z||j>ot)return[];const J=(g+S)*.5/n/this.source.tileSize,L=(v+z)*.5/n/this.source.tileSize,K=[];for(let D=j;D<=ot;D+=1)for(let _=H;_<=Z;_+=1){const et=_*this.source.tileSize*n,N=D*this.source.tileSize*n,G=Math.min((_+1)*this.source.tileSize,o)*n,P=Math.min((D+1)*this.source.tileSize,a)*n,W=_-J,R=D-L;K.push({key:`${t}/${_}/${D}`,tier:t,x:_,y:D,bounds:[et,N,G,P],distance2:W*W+R*R,url:Ct(this.source,t,_,D)})}return K.sort((D,_)=>D.distance2-_.distance2),K}requestTile(t){if(this.cache.has(t.key)||this.inflight.has(t.key)||this.destroyed)return;const i=new AbortController;this.inflight.set(t.key,i);const r=!!this.authToken;fetch(t.url,{signal:i.signal,headers:r?{Authorization:this.authToken}:void 0}).then(n=>{if(!n.ok)throw new Error(`HTTP ${n.status}`);return n.blob()}).then(n=>createImageBitmap(n)).then(n=>{if(this.inflight.delete(t.key),this.destroyed||i.signal.aborted){n.close();return}const o=this.gl.createTexture();if(!o){n.close();return}this.gl.bindTexture(this.gl.TEXTURE_2D,o),this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL,1),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_S,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_T,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MIN_FILTER,this.gl.LINEAR),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MAG_FILTER,this.gl.LINEAR),this.gl.texImage2D(this.gl.TEXTURE_2D,0,this.gl.RGBA,this.gl.RGBA,this.gl.UNSIGNED_BYTE,n),this.gl.bindTexture(this.gl.TEXTURE_2D,null),n.close(),this.cache.set(t.key,{texture:o,bounds:t.bounds,tier:t.tier,lastUsed:this.frameSerial}),this.trimCache(),this.requestRender()}).catch(n=>{this.inflight.delete(t.key),!i.signal.aborted&&console.warn("tile load failed",t.url,n)})}trimCache(){if(this.cache.size<=this.maxCacheTiles)return;const t=Array.from(this.cache.entries());t.sort((r,n)=>r[1].lastUsed-n[1].lastUsed);const i=this.cache.size-this.maxCacheTiles;for(let r=0;r<i;r+=1){const[n,o]=t[r];this.gl.deleteTexture(o.texture),this.cache.delete(n)}}render(){if(this.destroyed)return;this.frameSerial+=1;const t=this.gl;t.clearColor(.03,.06,.1,1),t.clear(t.COLOR_BUFFER_BIT);const i=this.getVisibleTiles();t.useProgram(this.program),t.bindVertexArray(this.vao),t.uniformMatrix3fv(this.uCamera,!1,this.camera.getMatrix()),t.uniform1i(this.uTexture,0);const r=this.getViewBounds(),n=[];for(const[,u]of this.cache)this.intersectsBounds(u.bounds,r)&&n.push(u);n.sort((u,f)=>u.tier-f.tier);for(const u of n)u.lastUsed=this.frameSerial,t.activeTexture(t.TEXTURE0),t.bindTexture(t.TEXTURE_2D,u.texture),t.uniform4f(this.uBounds,u.bounds[0],u.bounds[1],u.bounds[2],u.bounds[3]),t.drawArrays(t.TRIANGLE_STRIP,0,4);let o=0;for(const u of i){const f=this.cache.get(u.key);if(!f){this.requestTile(u);continue}f.lastUsed=this.frameSerial,t.activeTexture(t.TEXTURE0),t.bindTexture(t.TEXTURE_2D,f.texture),t.uniform4f(this.uBounds,f.bounds[0],f.bounds[1],f.bounds[2],f.bounds[3]),t.drawArrays(t.TRIANGLE_STRIP,0,4),o+=1}t.bindTexture(t.TEXTURE_2D,null),t.bindVertexArray(null);let a=0;this.pointCount>0&&(t.enable(t.BLEND),t.blendFunc(t.ONE,t.ONE_MINUS_SRC_ALPHA),t.useProgram(this.pointProgram),t.bindVertexArray(this.pointVao),t.uniformMatrix3fv(this.uPointCamera,!1,this.camera.getMatrix()),t.uniform1f(this.uPointSize,this.getPointSizeByZoom()),t.uniform1f(this.uPointPaletteSize,this.pointPaletteSize),t.uniform1i(this.uPointPalette,1),t.activeTexture(t.TEXTURE1),t.bindTexture(t.TEXTURE_2D,this.pointPaletteTexture),t.drawArrays(t.POINTS,0,this.pointCount),t.bindTexture(t.TEXTURE_2D,null),t.bindVertexArray(null),a=this.pointCount),typeof this.onStats=="function"&&this.onStats({tier:this.currentTier,visible:i.length,rendered:o,points:a,fallback:n.length,cache:this.cache.size,inflight:this.inflight.size})}requestRender(){this.frame!==null||this.destroyed||(this.frame=requestAnimationFrame(()=>{this.frame=null,this.render()}))}resize(){const t=this.canvas.getBoundingClientRect(),i=Math.max(1,t.width||this.canvas.clientWidth||1),r=Math.max(1,t.height||this.canvas.clientHeight||1),n=Math.max(1,window.devicePixelRatio||1),o=Math.max(1,Math.round(i*n)),a=Math.max(1,Math.round(r*n));(this.canvas.width!==o||this.canvas.height!==a)&&(this.canvas.width=o,this.canvas.height=a),this.camera.setViewport(i,r),this.gl.viewport(0,0,o,a),this.requestRender()}onPointerDown(t){this.interactionLocked||(this.dragging=!0,this.pointerId=t.pointerId,this.lastPointerX=t.clientX,this.lastPointerY=t.clientY,this.canvas.classList.add("dragging"),this.canvas.setPointerCapture(t.pointerId))}onPointerMove(t){if(this.interactionLocked||!this.dragging||t.pointerId!==this.pointerId)return;const i=t.clientX-this.lastPointerX,r=t.clientY-this.lastPointerY;this.lastPointerX=t.clientX,this.lastPointerY=t.clientY;const n=this.camera.getViewState();this.camera.setViewState({offsetX:n.offsetX-i/n.zoom,offsetY:n.offsetY-r/n.zoom}),this.clampViewState(),this.emitViewState(),this.requestRender()}onPointerUp(t){this.interactionLocked||t.pointerId===this.pointerId&&(this.dragging=!1,this.pointerId=null,this.canvas.classList.remove("dragging"))}onWheel(t){if(this.interactionLocked){t.preventDefault();return}t.preventDefault();const i=this.canvas.getBoundingClientRect(),r=t.clientX-i.left,n=t.clientY-i.top,o=t.deltaY<0?1.12:.89;this.zoomBy(o,r,n)}onDoubleClick(t){if(this.interactionLocked)return;const i=this.canvas.getBoundingClientRect(),r=t.clientX-i.left,n=t.clientY-i.top;this.zoomBy(t.shiftKey?.8:1.25,r,n)}destroy(){if(!this.destroyed){this.destroyed=!0,this.frame!==null&&(cancelAnimationFrame(this.frame),this.frame=null),this.resizeObserver.disconnect(),this.canvas.removeEventListener("pointerdown",this.boundPointerDown),this.canvas.removeEventListener("pointermove",this.boundPointerMove),this.canvas.removeEventListener("pointerup",this.boundPointerUp),this.canvas.removeEventListener("pointercancel",this.boundPointerUp),this.canvas.removeEventListener("wheel",this.boundWheel),this.canvas.removeEventListener("dblclick",this.boundDoubleClick),this.cancelDrag();for(const[,t]of this.inflight)t.abort();this.inflight.clear();for(const[,t]of this.cache)this.gl.deleteTexture(t.texture);this.cache.clear(),this.gl.deleteBuffer(this.vbo),this.gl.deleteVertexArray(this.vao),this.gl.deleteProgram(this.program),this.gl.deleteBuffer(this.pointPosBuffer),this.gl.deleteBuffer(this.pointTermBuffer),this.gl.deleteTexture(this.pointPaletteTexture),this.gl.deleteVertexArray(this.pointVao),this.gl.deleteProgram(this.pointProgram)}}}const Lt=[],Me=[];function Pt(e,t){return e.id??t}function Ie(e,t){if(!Array.isArray(t)||t.length<3)return!1;const[i,r]=e;let n=!1;for(let o=0,a=t.length-1;o<t.length;a=o++){const[u,f]=t[o],[g,v]=t[a];f>r!=v>r&&i<(g-u)*(r-f)/Math.max(1e-12,v-f)+u&&(n=!n)}return n}function Dt(e,t){for(let i=t.length-1;i>=0;i-=1){const r=t[i];if(r?.coordinates?.length&&Ie(e,r.coordinates))return{region:r,regionIndex:i,regionId:Pt(r,i)}}return null}function _e({source:e,viewState:t,onViewStateChange:i,onStats:r,fitNonce:n=0,authToken:o="",pointData:a=null,pointPalette:u=null,roiRegions:f,roiPolygons:g,clipPointsToRois:v=!1,interactionLock:S=!1,drawTool:z="cursor",stampOptions:H,regionStrokeStyle:Z,regionStrokeHoverStyle:j,regionStrokeActiveStyle:ot,regionLabelStyle:J,onRegionHover:L,onRegionClick:K,onActiveRegionChange:D,onDrawComplete:_,showOverviewMap:et=!1,overviewMapOptions:N,className:G,style:P}){const W=c.useRef(null),R=c.useRef(null),k=c.useRef(null),st=c.useRef(null),at=c.useRef(i),[ht,lt]=c.useState(!0),[it,d]=c.useState(null),[w,b]=c.useState(null),x=c.useRef(null),F=f??Lt,T=g??Me,V=c.useMemo(()=>({position:"relative",width:"100%",height:"100%",...P}),[P]),U=c.useMemo(()=>F.length>0?F:T.length===0?Lt:T.map((h,p)=>({id:p,coordinates:h})),[F,T]),q=c.useMemo(()=>U.map(h=>h.coordinates),[U]),Q=c.useMemo(()=>v?qt(a,q):a,[v,a,q]);c.useMemo(()=>{const h=Number(N?.width??220);return Number.isFinite(h)?Math.max(64,h):220},[N?.width]);const $=c.useMemo(()=>{const h=Number(N?.height??140);return Number.isFinite(h)?Math.max(48,h):140},[N?.height]),C=c.useMemo(()=>{const h=Number(N?.margin??16);return Number.isFinite(h)?Math.max(0,h):16},[N?.margin]),B=N?.position||"bottom-right",X=c.useCallback(h=>{b(p=>String(p)===String(h)?p:(D?.(h),h))},[D]);c.useEffect(()=>{at.current=i},[i]),c.useEffect(()=>{!(w===null?!0:U.some((I,O)=>String(Pt(I,O))===String(w)))&&w!==null&&X(null);const p=x.current;!(p===null?!0:U.some((I,O)=>String(Pt(I,O))===String(p)))&&p!==null&&(x.current=null,d(null),L?.({region:null,regionId:null,regionIndex:-1,coordinate:null}))},[U,w,L,X]);const s=c.useCallback(h=>{const p=at.current;p&&p(h),k.current?.(),st.current?.()},[]);c.useEffect(()=>{if(!et){lt(!1);return}lt(!0)},[et,e?.id]),c.useEffect(()=>{z!=="cursor"&&x.current!==null&&(x.current=null,d(null),L?.({region:null,regionId:null,regionIndex:-1,coordinate:null}))},[z,L]);const l=c.useCallback((h,p)=>{const M=R.current;if(!M)return null;const I=M.screenToWorld(h,p);if(!Array.isArray(I)||I.length<2)return null;const O=Number(I[0]),dt=Number(I[1]);return!Number.isFinite(O)||!Number.isFinite(dt)?null:[O,dt]},[]),m=c.useCallback(h=>{if(z!=="cursor")return;if(h.target!==W.current){x.current!==null&&(x.current=null,d(null),L?.({region:null,regionId:null,regionIndex:-1,coordinate:null}));return}if(!U.length)return;const p=l(h.clientX,h.clientY);if(!p)return;const M=Dt(p,U),I=M?.regionId??null,O=x.current;String(O)!==String(I)&&(x.current=I,d(I),L?.({region:M?.region??null,regionId:I,regionIndex:M?.regionIndex??-1,coordinate:p}))},[z,U,l,L]),E=c.useCallback(()=>{x.current!==null&&(x.current=null,d(null),L?.({region:null,regionId:null,regionIndex:-1,coordinate:null}))},[L]),A=c.useCallback(h=>{if(z!=="cursor"||h.target!==W.current)return;if(!U.length){X(null);return}const p=l(h.clientX,h.clientY);if(!p)return;const M=Dt(p,U);if(!M){X(null);return}let I=M.regionId;w!==null&&(I=String(w)===String(M.regionId)?w:null),X(I),K?.({region:M.region,regionId:M.regionId,regionIndex:M.regionIndex,coordinate:p})},[z,U,l,K,w,X]);return c.useEffect(()=>{const h=W.current;if(!h||!e)return;const p=new Zt(h,e,{onViewStateChange:s,onStats:r,authToken:o});return R.current=p,t&&p.setViewState(t),p.setInteractionLock(S),()=>{p.destroy(),R.current=null}},[e,r,o,s]),c.useEffect(()=>{const h=R.current;!h||!t||h.setViewState(t)},[t]),c.useEffect(()=>{const h=R.current;h&&h.fitToImage()},[n]),c.useEffect(()=>{const h=R.current;!h||!u||h.setPointPalette(u)},[u]),c.useEffect(()=>{const h=R.current;h&&h.setPointData(Q)},[Q]),c.useEffect(()=>{const h=R.current;h&&h.setInteractionLock(S)},[S]),nt.jsxs("div",{className:G,style:V,onPointerMove:m,onPointerLeave:E,onClick:A,children:[nt.jsx("canvas",{ref:W,className:"wsi-render-canvas",style:{position:"absolute",inset:0,zIndex:1,width:"100%",height:"100%",display:"block",touchAction:"none",cursor:z==="cursor"&&it!==null?"pointer":S?"crosshair":"grab"}}),e?nt.jsx(Ht,{tool:z,enabled:z!=="cursor",imageWidth:e.width,imageHeight:e.height,imageMpp:e.mpp,imageZoom:e.maxTierZoom,stampOptions:H,projectorRef:R,viewStateSignal:t,persistedRegions:U,regionStrokeStyle:Z,regionStrokeHoverStyle:j,regionStrokeActiveStyle:ot,hoveredRegionId:it,activeRegionId:w,regionLabelStyle:J,invalidateRef:k,onDrawComplete:_}):null,e&&et?ht?nt.jsxs(nt.Fragment,{children:[nt.jsx(Gt,{source:e,projectorRef:R,authToken:o,options:N,invalidateRef:st}),nt.jsx("button",{type:"button","aria-label":"Hide overview map",onClick:()=>lt(!1),style:{position:"absolute",zIndex:6,...B.includes("left")?{left:C}:{right:C},...B.includes("top")?{top:C+$+8}:{bottom:C+$+8},width:20,height:20,borderRadius:999,border:"1px solid rgba(255,255,255,0.4)",background:"rgba(8, 14, 22, 0.9)",color:"#fff",fontSize:13,lineHeight:1,cursor:"pointer",padding:0},children:"×"})]}):nt.jsx("button",{type:"button","aria-label":"Show overview map",onClick:()=>lt(!0),style:{position:"absolute",zIndex:6,...B.includes("left")?{left:C}:{right:C},...B.includes("top")?{top:C}:{bottom:C},height:24,minWidth:40,borderRadius:999,border:"1px solid rgba(255,255,255,0.45)",background:"rgba(8, 14, 22, 0.9)",color:"#dff8ff",fontSize:11,fontWeight:700,cursor:"pointer",padding:"0 8px"},children:"Map"}):null]})}exports.DEFAULT_POINT_COLOR=Rt;exports.DrawLayer=Ht;exports.M1TileRenderer=Vt;exports.OverviewMap=Gt;exports.TileViewerCanvas=xe;exports.WsiTileRenderer=Zt;exports.WsiViewerCanvas=_e;exports.buildTermPalette=se;exports.calcScaleLength=re;exports.calcScaleResolution=At;exports.clamp=Y;exports.closeRing=ct;exports.createCircle=xt;exports.createRectangle=Et;exports.filterPointDataByPolygons=qt;exports.hexToRgba=Yt;exports.isSameViewState=ne;exports.normalizeImageInfo=Ee;exports.toBearerToken=oe;exports.toTileUrl=Ct;
|
|
94
|
+
//# sourceMappingURL=index.cjs.map
|