open-plant 1.2.1 → 1.2.3
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 +9 -1
- package/README.md +33 -4
- package/dist/assets/roi-clip-worker-DdVYCepx.js +2 -0
- package/dist/assets/roi-clip-worker-DdVYCepx.js.map +1 -0
- package/dist/index.cjs +7 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1258 -1141
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +9 -9
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/react/draw-layer.d.ts +17 -2
- package/dist/types/react/draw-layer.d.ts.map +1 -1
- package/dist/types/react/wsi-viewer-canvas.d.ts +24 -3
- package/dist/types/react/wsi-viewer-canvas.d.ts.map +1 -1
- package/dist/types/wsi/point-clip-worker-client.d.ts +5 -0
- package/dist/types/wsi/point-clip-worker-client.d.ts.map +1 -1
- package/dist/types/wsi/point-clip-worker-protocol.d.ts +17 -2
- package/dist/types/wsi/point-clip-worker-protocol.d.ts.map +1 -1
- package/dist/types/wsi/point-clip.d.ts +1 -0
- package/dist/types/wsi/point-clip.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/assets/roi-clip-worker-i1SE1Dpa.js +0 -2
- package/dist/assets/roi-clip-worker-i1SE1Dpa.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,10 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
- No changes yet.
|
|
11
|
+
|
|
12
|
+
## [1.2.2] - 2026-02-25
|
|
13
|
+
|
|
10
14
|
### Added
|
|
11
15
|
- Camera rotation support in `WsiViewState` via `rotationDeg`.
|
|
12
16
|
- `Ctrl/Cmd + drag` rotation input path with configurable renderer option.
|
|
@@ -22,6 +26,10 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
|
|
22
26
|
- Hybrid WebGPU draw bridge payload support via `WsiPointData.drawIndices`.
|
|
23
27
|
- Hybrid clip option `bridgeToDraw` and clip stat flag `bridgedToDraw`.
|
|
24
28
|
- Unit test coverage for ROI term stats with draw-index bridge input.
|
|
29
|
+
- Patch-intent draw path for `stamp-rectangle-4096px` with dedicated `onPatchComplete` callback.
|
|
30
|
+
- Patch overlay channel on viewer (`patchRegions`, `patchStrokeStyle`) separated from ROI hover/active interaction.
|
|
31
|
+
- Custom React overlay layer slots via `customLayers` for host-owned rendering pipelines.
|
|
32
|
+
- Point-index clipping primitives for export workflows: `filterPointIndicesByPolygons` and worker variant.
|
|
25
33
|
|
|
26
34
|
### Changed
|
|
27
35
|
- `WsiTileRenderer` projection, bounds, and zoom anchoring now account for rotation.
|
|
@@ -33,7 +41,7 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
|
|
33
41
|
- Publish gate now enforces `npm run release:gate` via `prepublishOnly`.
|
|
34
42
|
|
|
35
43
|
### Docs
|
|
36
|
-
- Updated EN/KO API and guides for rotation, pointer world callbacks, overlay shapes, 4096px
|
|
44
|
+
- Updated EN/KO API and guides for rotation, pointer world callbacks, overlay shapes, 4096px patch intent flow, custom layers, and ROI term stats.
|
|
37
45
|
- Updated `todo.md` gap table with current support status and code-path references.
|
|
38
46
|
- Added EN/KO migration guides with API stability/deprecation policy and release-gate contract.
|
|
39
47
|
- Added EN/KO contributing pages and linked them across docs navigation.
|
package/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
WebGL2 기반 고성능 WSI(Whole Slide Image) 뷰어
|
|
6
|
+
WebGL2 기반 고성능 WSI(Whole Slide Image) 뷰어 라이브러리<br/>
|
|
7
|
+
고사양 PC가 아니어도, 고작, 고~오작 iPhone 15에서 수백만 cell을 부드럽게 렌더링
|
|
7
8
|
</p>
|
|
8
9
|
|
|
9
10
|
<p align="center">
|
|
@@ -17,15 +18,24 @@
|
|
|
17
18
|
|
|
18
19
|
---
|
|
19
20
|
|
|
20
|
-
<h3 align="center">10,000,000 cells · ~300 MB RAM · 60 fps</h3>
|
|
21
|
+
<h3 align="center">10,000,000 cells · ~300 MB RAM · 60 fps · iPhone 15 ready</h3>
|
|
21
22
|
|
|
22
23
|
https://github.com/user-attachments/assets/5a6b5deb-7442-4389-908f-bf2c69348824
|
|
23
24
|
|
|
25
|
+
> 핵심 포지셔닝: Open Plant는 데스크톱 전용 엔진이 아닙니다. iPhone 15급 모바일 환경에서도 수백만 cell pan/zoom 워크로드를 체감 렉 없이 다루는 것을 목표로 설계했습니다.
|
|
26
|
+
|
|
24
27
|
## Why Open Plant
|
|
25
28
|
|
|
26
29
|
범용 시각화 프레임워크 위에 병리 뷰어를 올리면 추상화 비용을 그대로 떠안게 됩니다.
|
|
27
30
|
Open Plant는 WSI 렌더링 **한 가지만** 하도록 설계되었고, 그래서 아래가 가능합니다.
|
|
28
31
|
|
|
32
|
+
### 모바일 실전 성능 (iPhone 15)
|
|
33
|
+
|
|
34
|
+
Open Plant는 “고사양 PC에서만 빠른 뷰어”가 아니라, iPhone 15 같은 일반 플래그십 모바일에서도
|
|
35
|
+
수백만 cell을 pan/zoom하면서 작업 가능한 성능을 목표로 최적화되어 있습니다.
|
|
36
|
+
타일 스케줄러 + fallback 렌더링 + TypedArray 포인트 파이프라인 덕분에, 실제 사용 시에도 뷰 전환 안정성을 유지합니다.
|
|
37
|
+
(`실효 성능은 데이터 밀도/타일 서버 응답/네트워크 상태에 따라 달라질 수 있습니다.`)
|
|
38
|
+
|
|
29
39
|
### 포인트 1개당 10바이트
|
|
30
40
|
|
|
31
41
|
범용 라이브러리는 포인트마다 인스턴스 버퍼에 position + RGBA를 넣어 **20바이트 이상** 씁니다.
|
|
@@ -65,6 +75,7 @@ draw mode에 진입하면 `setPointerCapture`로 입력을 독점한 뒤 `intera
|
|
|
65
75
|
| **WebGL2 타일 렌더링** | 멀티 티어 타일 피라미드, LRU 캐시(320장), 저해상도 fallback 렌더링 |
|
|
66
76
|
| **회전 인터랙션** | `WsiViewState.rotationDeg`, `Ctrl/Cmd + drag` 회전, `resetRotation` 경로 |
|
|
67
77
|
| **포인트 오버레이** | WebGL2 `gl.POINTS`로 수십, 수백만 개 포인트를 팔레트 텍스처 기반 컬러링. 파싱된 TypedArray만 입력 |
|
|
78
|
+
| **모바일 타겟 성능** | iPhone 15급 환경에서 수백만 cell 워크로드를 전제로 pan/zoom 응답성을 유지하도록 설계 |
|
|
68
79
|
| **드로잉 / ROI 도구** | Freehand · Rectangle · Circular + Stamp(사각형/원, mm² 지정) |
|
|
69
80
|
| **고정 픽셀 스탬프** | `stamp-rectangle-4096px` + `stampOptions.rectanglePixelSize` |
|
|
70
81
|
| **ROI 포인트 클리핑** | `clipMode`: `sync` / `worker` / `hybrid-webgpu` (실험) |
|
|
@@ -129,15 +140,31 @@ import { WsiViewerCanvas } from "open-plant";
|
|
|
129
140
|
clipPointsToRois
|
|
130
141
|
clipMode="worker"
|
|
131
142
|
onClipStats={(s) => console.log(s.mode, s.durationMs)}
|
|
132
|
-
drawTool="stamp-
|
|
143
|
+
drawTool="stamp-rectangle-4096px"
|
|
133
144
|
stampOptions={{
|
|
134
145
|
rectangleAreaMm2: 2,
|
|
135
146
|
circleAreaMm2: 0.2, // HPF 예시
|
|
136
147
|
rectanglePixelSize: 4096,
|
|
137
148
|
}}
|
|
149
|
+
patchRegions={patchRegions}
|
|
150
|
+
patchStrokeStyle={{ color: "#8ad8ff", lineDash: [10, 8], width: 2 }}
|
|
151
|
+
customLayers={[
|
|
152
|
+
{
|
|
153
|
+
id: "patch-labels",
|
|
154
|
+
render: ({ worldToScreen }) => {
|
|
155
|
+
/* host overlay */
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
]}
|
|
138
159
|
onPointerWorldMove={(e) => console.log(e.coordinate)}
|
|
139
160
|
onRoiPointGroups={(stats) => console.log(stats.groups)}
|
|
140
|
-
onDrawComplete={
|
|
161
|
+
onDrawComplete={(result) => {
|
|
162
|
+
if (result.intent === "roi") handleRoi(result);
|
|
163
|
+
}}
|
|
164
|
+
onPatchComplete={(patch) => {
|
|
165
|
+
// stamp-rectangle-4096px 전용
|
|
166
|
+
handlePatch(patch);
|
|
167
|
+
}}
|
|
141
168
|
onViewStateChange={handleViewChange}
|
|
142
169
|
onStats={setStats}
|
|
143
170
|
/>
|
|
@@ -164,6 +191,8 @@ Freehand, Rectangle, Circular + Stamp(사각형/원) 드로잉 오버레이.
|
|
|
164
191
|
| `normalizeImageInfo(raw, tileBaseUrl)` | API 응답 + 타일 베이스 URL을 `WsiImageSource`로 변환 |
|
|
165
192
|
| `filterPointDataByPolygons()` | ROI 폴리곤으로 포인트 필터링 |
|
|
166
193
|
| `filterPointDataByPolygonsInWorker()` | 워커 스레드 ROI 필터링 |
|
|
194
|
+
| `filterPointIndicesByPolygons()` | 폴리곤 내부 원본 포인트 인덱스 추출(패치 JSON export용) |
|
|
195
|
+
| `filterPointIndicesByPolygonsInWorker()` | 포인트 인덱스 추출 워커 버전 |
|
|
167
196
|
| `filterPointDataByPolygonsHybrid()` | WebGPU bbox prefilter + polygon 정밀 판정(실험) |
|
|
168
197
|
| `getWebGpuCapabilities()` | WebGPU 지원/어댑터 정보 조회 |
|
|
169
198
|
| `buildTermPalette()` | Term 기반 컬러 팔레트 생성 |
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
(function(){"use strict";function l(){return typeof performance<"u"&&typeof performance.now=="function"?performance.now():Date.now()}function I(t){if(!Array.isArray(t)||t.length<3)return[];const n=t.map(([i,s])=>[i,s]),o=n[0],e=n[n.length-1];return!o||!e?[]:((o[0]!==e[0]||o[1]!==e[1])&&n.push([o[0],o[1]]),n)}function x(t){const n=[];for(const o of t??[]){const e=I(o);if(e.length<4)continue;let i=1/0,s=1/0,c=-1/0,a=-1/0;for(const[r,f]of e)r<i&&(i=r),r>c&&(c=r),f<s&&(s=f),f>a&&(a=f);!Number.isFinite(i)||!Number.isFinite(s)||n.push({ring:e,minX:i,minY:s,maxX:c,maxY:a})}return n}function A(t,n,o){let e=!1;for(let i=0,s=o.length-1;i<o.length;s=i,i+=1){const c=o[i][0],a=o[i][1],r=o[s][0],f=o[s][1];a>n!=f>n&&t<(r-c)*(n-a)/(f-a||Number.EPSILON)+c&&(e=!e)}return e}function m(t,n,o){for(const e of o)if(!(t<e.minX||t>e.maxX||n<e.minY||n>e.maxY)&&A(t,n,e.ring))return!0;return!1}function g(t){if(t instanceof Error)return t.message;try{return String(t)}catch{return"unknown worker error"}}const d=self;function b(t){const n=l(),o=Math.max(0,Math.floor(t.count)),e=new Float32Array(t.positions),i=new Uint16Array(t.paletteIndices),s=Math.floor(e.length/2),c=Math.max(0,Math.min(o,s,i.length)),a=x(t.polygons??[]);if(c===0||a.length===0)return{type:"roi-clip-success",id:t.id,count:0,positions:new Float32Array(0).buffer,paletteIndices:new Uint16Array(0).buffer,durationMs:l()-n};const r=new Float32Array(c*2),f=new Uint16Array(c);let u=0;for(let p=0;p<c;p+=1){const M=e[p*2],w=e[p*2+1];m(M,w,a)&&(r[u*2]=M,r[u*2+1]=w,f[u]=i[p],u+=1)}const y=r.slice(0,u*2),h=f.slice(0,u);return{type:"roi-clip-success",id:t.id,count:u,positions:y.buffer,paletteIndices:h.buffer,durationMs:l()-n}}function P(t){const n=l(),o=Math.max(0,Math.floor(t.count)),e=new Float32Array(t.positions),i=Math.floor(e.length/2),s=Math.max(0,Math.min(o,i)),c=x(t.polygons??[]);if(s===0||c.length===0)return{type:"roi-clip-index-success",id:t.id,count:0,indices:new Uint32Array(0).buffer,durationMs:l()-n};const a=new Uint32Array(s);let r=0;for(let u=0;u<s;u+=1){const y=e[u*2],h=e[u*2+1];m(y,h,c)&&(a[r]=u,r+=1)}const f=a.slice(0,r);return{type:"roi-clip-index-success",id:t.id,count:r,indices:f.buffer,durationMs:l()-n}}d.addEventListener("message",t=>{const n=t.data;if(!(!n||n.type!=="roi-clip-request"&&n.type!=="roi-clip-index-request"))try{if(n.type==="roi-clip-index-request"){const e=P(n);d.postMessage(e,[e.indices]);return}const o=b(n);d.postMessage(o,[o.positions,o.paletteIndices])}catch(o){const e={type:"roi-clip-failure",id:n.id,error:g(o)};d.postMessage(e)}})})();
|
|
2
|
+
//# sourceMappingURL=roi-clip-worker-DdVYCepx.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roi-clip-worker-DdVYCepx.js","sources":["../src/workers/roi-clip-worker.ts"],"sourcesContent":["import type { RoiCoordinate, RoiPolygon } from \"../wsi/point-clip\";\nimport type {\n RoiClipWorkerDataRequest,\n RoiClipWorkerIndexRequest,\n RoiClipWorkerIndexSuccess,\n RoiClipWorkerRequest,\n RoiClipWorkerResponse,\n RoiClipWorkerSuccess,\n} from \"../wsi/point-clip-worker-protocol\";\n\ninterface PreparedPolygon {\n ring: RoiPolygon;\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n}\n\nfunction nowMs(): number {\n if (typeof performance !== \"undefined\" && typeof performance.now === \"function\") {\n return performance.now();\n }\n return Date.now();\n}\n\nfunction closeRing(coords: RoiPolygon): RoiPolygon {\n if (!Array.isArray(coords) || coords.length < 3) return [];\n const out = coords.map(([x, y]) => [x, y] as RoiCoordinate);\n const first = out[0];\n const last = out[out.length - 1];\n if (!first || !last) return [];\n if (first[0] !== last[0] || first[1] !== last[1]) {\n out.push([first[0], first[1]]);\n }\n return out;\n}\n\nfunction preparePolygons(polygons: RoiPolygon[]): PreparedPolygon[] {\n const prepared: PreparedPolygon[] = [];\n for (const poly of polygons ?? []) {\n const ring = closeRing(poly);\n if (ring.length < 4) continue;\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n for (const [x, y] of ring) {\n if (x < minX) minX = x;\n if (x > maxX) maxX = x;\n if (y < minY) minY = y;\n if (y > maxY) maxY = y;\n }\n if (!Number.isFinite(minX) || !Number.isFinite(minY)) continue;\n prepared.push({ ring, minX, minY, maxX, maxY });\n }\n return prepared;\n}\n\nfunction isInsideRing(x: number, y: number, ring: RoiPolygon): boolean {\n let inside = false;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {\n const xi = ring[i][0];\n const yi = ring[i][1];\n const xj = ring[j][0];\n const yj = ring[j][1];\n const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi || Number.EPSILON) + xi;\n if (intersect) inside = !inside;\n }\n return inside;\n}\n\nfunction isInsideAnyPolygon(x: number, y: number, polygons: PreparedPolygon[]): boolean {\n for (const poly of polygons) {\n if (x < poly.minX || x > poly.maxX || y < poly.minY || y > poly.maxY) {\n continue;\n }\n if (isInsideRing(x, y, poly.ring)) {\n return true;\n }\n }\n return false;\n}\n\nfunction toErrorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n try {\n return String(error);\n } catch {\n return \"unknown worker error\";\n }\n}\n\ninterface WorkerScope {\n postMessage(message: unknown, transfer?: Transferable[]): void;\n addEventListener(type: \"message\", listener: (event: MessageEvent<RoiClipWorkerRequest>) => void): void;\n}\n\nconst workerScope = self as unknown as WorkerScope;\n\nfunction handleDataRequest(msg: RoiClipWorkerDataRequest): RoiClipWorkerSuccess {\n const start = nowMs();\n const count = Math.max(0, Math.floor(msg.count));\n const positions = new Float32Array(msg.positions);\n const terms = new Uint16Array(msg.paletteIndices);\n\n const maxCountByPositions = Math.floor(positions.length / 2);\n const safeCount = Math.max(0, Math.min(count, maxCountByPositions, terms.length));\n const prepared = preparePolygons(msg.polygons ?? []);\n\n if (safeCount === 0 || prepared.length === 0) {\n return {\n type: \"roi-clip-success\",\n id: msg.id,\n count: 0,\n positions: new Float32Array(0).buffer,\n paletteIndices: new Uint16Array(0).buffer,\n durationMs: nowMs() - start,\n };\n }\n\n const nextPositions = new Float32Array(safeCount * 2);\n const nextTerms = new Uint16Array(safeCount);\n let cursor = 0;\n\n for (let i = 0; i < safeCount; i += 1) {\n const x = positions[i * 2];\n const y = positions[i * 2 + 1];\n if (!isInsideAnyPolygon(x, y, prepared)) continue;\n nextPositions[cursor * 2] = x;\n nextPositions[cursor * 2 + 1] = y;\n nextTerms[cursor] = terms[i];\n cursor += 1;\n }\n\n const outPositions = nextPositions.slice(0, cursor * 2);\n const outTerms = nextTerms.slice(0, cursor);\n\n return {\n type: \"roi-clip-success\",\n id: msg.id,\n count: cursor,\n positions: outPositions.buffer,\n paletteIndices: outTerms.buffer,\n durationMs: nowMs() - start,\n };\n}\n\nfunction handleIndexRequest(msg: RoiClipWorkerIndexRequest): RoiClipWorkerIndexSuccess {\n const start = nowMs();\n const count = Math.max(0, Math.floor(msg.count));\n const positions = new Float32Array(msg.positions);\n const maxCountByPositions = Math.floor(positions.length / 2);\n const safeCount = Math.max(0, Math.min(count, maxCountByPositions));\n const prepared = preparePolygons(msg.polygons ?? []);\n\n if (safeCount === 0 || prepared.length === 0) {\n return {\n type: \"roi-clip-index-success\",\n id: msg.id,\n count: 0,\n indices: new Uint32Array(0).buffer,\n durationMs: nowMs() - start,\n };\n }\n\n const out = new Uint32Array(safeCount);\n let cursor = 0;\n for (let i = 0; i < safeCount; i += 1) {\n const x = positions[i * 2];\n const y = positions[i * 2 + 1];\n if (!isInsideAnyPolygon(x, y, prepared)) continue;\n out[cursor] = i;\n cursor += 1;\n }\n\n const outIndices = out.slice(0, cursor);\n return {\n type: \"roi-clip-index-success\",\n id: msg.id,\n count: cursor,\n indices: outIndices.buffer,\n durationMs: nowMs() - start,\n };\n}\n\nworkerScope.addEventListener(\"message\", (event: MessageEvent<RoiClipWorkerRequest>) => {\n const data = event.data;\n if (!data || (data.type !== \"roi-clip-request\" && data.type !== \"roi-clip-index-request\")) return;\n\n try {\n if (data.type === \"roi-clip-index-request\") {\n const response = handleIndexRequest(data);\n workerScope.postMessage(response, [response.indices]);\n return;\n }\n const response = handleDataRequest(data);\n workerScope.postMessage(response, [response.positions, response.paletteIndices]);\n } catch (error) {\n const fail: RoiClipWorkerResponse = {\n type: \"roi-clip-failure\",\n id: data.id,\n error: toErrorMessage(error),\n };\n workerScope.postMessage(fail);\n }\n});\n"],"names":["nowMs","closeRing","coords","out","x","y","first","last","preparePolygons","polygons","prepared","poly","ring","minX","minY","maxX","maxY","isInsideRing","inside","j","xi","yi","xj","yj","isInsideAnyPolygon","toErrorMessage","error","workerScope","handleDataRequest","msg","start","count","positions","terms","maxCountByPositions","safeCount","nextPositions","nextTerms","cursor","i","outPositions","outTerms","handleIndexRequest","outIndices","event","data","response","fail"],"mappings":"yBAkBA,SAASA,GAAgB,CACvB,OAAI,OAAO,YAAgB,KAAe,OAAO,YAAY,KAAQ,WAC5D,YAAY,IAAA,EAEd,KAAK,IAAA,CACd,CAEA,SAASC,EAAUC,EAAgC,CACjD,GAAI,CAAC,MAAM,QAAQA,CAAM,GAAKA,EAAO,OAAS,EAAG,MAAO,CAAA,EACxD,MAAMC,EAAMD,EAAO,IAAI,CAAC,CAACE,EAAGC,CAAC,IAAM,CAACD,EAAGC,CAAC,CAAkB,EACpDC,EAAQH,EAAI,CAAC,EACbI,EAAOJ,EAAIA,EAAI,OAAS,CAAC,EAC/B,MAAI,CAACG,GAAS,CAACC,EAAa,CAAA,IACxBD,EAAM,CAAC,IAAMC,EAAK,CAAC,GAAKD,EAAM,CAAC,IAAMC,EAAK,CAAC,IAC7CJ,EAAI,KAAK,CAACG,EAAM,CAAC,EAAGA,EAAM,CAAC,CAAC,CAAC,EAExBH,EACT,CAEA,SAASK,EAAgBC,EAA2C,CAClE,MAAMC,EAA8B,CAAA,EACpC,UAAWC,KAAQF,GAAY,GAAI,CACjC,MAAMG,EAAOX,EAAUU,CAAI,EAC3B,GAAIC,EAAK,OAAS,EAAG,SACrB,IAAIC,EAAO,IACPC,EAAO,IACPC,EAAO,KACPC,EAAO,KACX,SAAW,CAACZ,EAAGC,CAAC,IAAKO,EACfR,EAAIS,IAAMA,EAAOT,GACjBA,EAAIW,IAAMA,EAAOX,GACjBC,EAAIS,IAAMA,EAAOT,GACjBA,EAAIW,IAAMA,EAAOX,GAEnB,CAAC,OAAO,SAASQ,CAAI,GAAK,CAAC,OAAO,SAASC,CAAI,GACnDJ,EAAS,KAAK,CAAE,KAAAE,EAAM,KAAAC,EAAM,KAAAC,EAAM,KAAAC,EAAM,KAAAC,EAAM,CAChD,CACA,OAAON,CACT,CAEA,SAASO,EAAab,EAAWC,EAAWO,EAA2B,CACrE,IAAIM,EAAS,GACb,QAAS,EAAI,EAAGC,EAAIP,EAAK,OAAS,EAAG,EAAIA,EAAK,OAAQO,EAAI,EAAG,GAAK,EAAG,CACnE,MAAMC,EAAKR,EAAK,CAAC,EAAE,CAAC,EACdS,EAAKT,EAAK,CAAC,EAAE,CAAC,EACdU,EAAKV,EAAKO,CAAC,EAAE,CAAC,EACdI,EAAKX,EAAKO,CAAC,EAAE,CAAC,EACFE,EAAKhB,GAAMkB,EAAKlB,GAAKD,GAAMkB,EAAKF,IAAOf,EAAIgB,IAAQE,EAAKF,GAAM,OAAO,SAAWD,MAC1E,CAACF,EAC3B,CACA,OAAOA,CACT,CAEA,SAASM,EAAmBpB,EAAWC,EAAWI,EAAsC,CACtF,UAAWE,KAAQF,EACjB,GAAI,EAAAL,EAAIO,EAAK,MAAQP,EAAIO,EAAK,MAAQN,EAAIM,EAAK,MAAQN,EAAIM,EAAK,OAG5DM,EAAab,EAAGC,EAAGM,EAAK,IAAI,EAC9B,MAAO,GAGX,MAAO,EACT,CAEA,SAASc,EAAeC,EAAwB,CAC9C,GAAIA,aAAiB,MAAO,OAAOA,EAAM,QACzC,GAAI,CACF,OAAO,OAAOA,CAAK,CACrB,MAAQ,CACN,MAAO,sBACT,CACF,CAOA,MAAMC,EAAc,KAEpB,SAASC,EAAkBC,EAAqD,CAC9E,MAAMC,EAAQ9B,EAAA,EACR+B,EAAQ,KAAK,IAAI,EAAG,KAAK,MAAMF,EAAI,KAAK,CAAC,EACzCG,EAAY,IAAI,aAAaH,EAAI,SAAS,EAC1CI,EAAQ,IAAI,YAAYJ,EAAI,cAAc,EAE1CK,EAAsB,KAAK,MAAMF,EAAU,OAAS,CAAC,EACrDG,EAAY,KAAK,IAAI,EAAG,KAAK,IAAIJ,EAAOG,EAAqBD,EAAM,MAAM,CAAC,EAC1EvB,EAAWF,EAAgBqB,EAAI,UAAY,CAAA,CAAE,EAEnD,GAAIM,IAAc,GAAKzB,EAAS,SAAW,EACzC,MAAO,CACL,KAAM,mBACN,GAAImB,EAAI,GACR,MAAO,EACP,UAAW,IAAI,aAAa,CAAC,EAAE,OAC/B,eAAgB,IAAI,YAAY,CAAC,EAAE,OACnC,WAAY7B,IAAU8B,CAAA,EAI1B,MAAMM,EAAgB,IAAI,aAAaD,EAAY,CAAC,EAC9CE,EAAY,IAAI,YAAYF,CAAS,EAC3C,IAAIG,EAAS,EAEb,QAASC,EAAI,EAAGA,EAAIJ,EAAWI,GAAK,EAAG,CACrC,MAAMnC,EAAI4B,EAAUO,EAAI,CAAC,EACnBlC,EAAI2B,EAAUO,EAAI,EAAI,CAAC,EACxBf,EAAmBpB,EAAGC,EAAGK,CAAQ,IACtC0B,EAAcE,EAAS,CAAC,EAAIlC,EAC5BgC,EAAcE,EAAS,EAAI,CAAC,EAAIjC,EAChCgC,EAAUC,CAAM,EAAIL,EAAMM,CAAC,EAC3BD,GAAU,EACZ,CAEA,MAAME,EAAeJ,EAAc,MAAM,EAAGE,EAAS,CAAC,EAChDG,EAAWJ,EAAU,MAAM,EAAGC,CAAM,EAE1C,MAAO,CACL,KAAM,mBACN,GAAIT,EAAI,GACR,MAAOS,EACP,UAAWE,EAAa,OACxB,eAAgBC,EAAS,OACzB,WAAYzC,IAAU8B,CAAA,CAE1B,CAEA,SAASY,EAAmBb,EAA2D,CACrF,MAAMC,EAAQ9B,EAAA,EACR+B,EAAQ,KAAK,IAAI,EAAG,KAAK,MAAMF,EAAI,KAAK,CAAC,EACzCG,EAAY,IAAI,aAAaH,EAAI,SAAS,EAC1CK,EAAsB,KAAK,MAAMF,EAAU,OAAS,CAAC,EACrDG,EAAY,KAAK,IAAI,EAAG,KAAK,IAAIJ,EAAOG,CAAmB,CAAC,EAC5DxB,EAAWF,EAAgBqB,EAAI,UAAY,CAAA,CAAE,EAEnD,GAAIM,IAAc,GAAKzB,EAAS,SAAW,EACzC,MAAO,CACL,KAAM,yBACN,GAAImB,EAAI,GACR,MAAO,EACP,QAAS,IAAI,YAAY,CAAC,EAAE,OAC5B,WAAY7B,IAAU8B,CAAA,EAI1B,MAAM3B,EAAM,IAAI,YAAYgC,CAAS,EACrC,IAAIG,EAAS,EACb,QAASC,EAAI,EAAGA,EAAIJ,EAAWI,GAAK,EAAG,CACrC,MAAMnC,EAAI4B,EAAUO,EAAI,CAAC,EACnBlC,EAAI2B,EAAUO,EAAI,EAAI,CAAC,EACxBf,EAAmBpB,EAAGC,EAAGK,CAAQ,IACtCP,EAAImC,CAAM,EAAIC,EACdD,GAAU,EACZ,CAEA,MAAMK,EAAaxC,EAAI,MAAM,EAAGmC,CAAM,EACtC,MAAO,CACL,KAAM,yBACN,GAAIT,EAAI,GACR,MAAOS,EACP,QAASK,EAAW,OACpB,WAAY3C,IAAU8B,CAAA,CAE1B,CAEAH,EAAY,iBAAiB,UAAYiB,GAA8C,CACrF,MAAMC,EAAOD,EAAM,KACnB,GAAI,GAACC,GAASA,EAAK,OAAS,oBAAsBA,EAAK,OAAS,0BAEhE,GAAI,CACF,GAAIA,EAAK,OAAS,yBAA0B,CAC1C,MAAMC,EAAWJ,EAAmBG,CAAI,EACxClB,EAAY,YAAYmB,EAAU,CAACA,EAAS,OAAO,CAAC,EACpD,MACF,CACA,MAAMA,EAAWlB,EAAkBiB,CAAI,EACvClB,EAAY,YAAYmB,EAAU,CAACA,EAAS,UAAWA,EAAS,cAAc,CAAC,CACjF,OAASpB,EAAO,CACd,MAAMqB,EAA8B,CAClC,KAAM,mBACN,GAAIF,EAAK,GACT,MAAOpB,EAAeC,CAAK,CAAA,EAE7BC,EAAY,YAAYoB,CAAI,CAC9B,CACF,CAAC"}
|