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 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 stamp, and ROI term stats.
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-circle"
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={handleDraw}
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"}