view-anchor 0.1.2 → 0.2.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.
Files changed (44) hide show
  1. package/README.md +118 -34
  2. package/README.zh-CN.md +128 -44
  3. package/dist/index.d.ts +4 -16
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +2 -14
  6. package/dist/measure-loop.d.ts +9 -28
  7. package/dist/measure-loop.d.ts.map +1 -1
  8. package/dist/measure-loop.js +37 -16
  9. package/dist/protocol-publisher.d.ts +41 -0
  10. package/dist/protocol-publisher.d.ts.map +1 -0
  11. package/dist/protocol-publisher.js +191 -0
  12. package/dist/protocol-types.d.ts +36 -0
  13. package/dist/protocol-types.d.ts.map +1 -0
  14. package/dist/protocol-types.js +10 -0
  15. package/dist/protocol.d.ts +35 -0
  16. package/dist/protocol.d.ts.map +1 -0
  17. package/dist/protocol.js +131 -0
  18. package/dist/react.d.ts +18 -30
  19. package/dist/react.d.ts.map +1 -1
  20. package/dist/react.js +125 -122
  21. package/dist/size-advertiser.d.ts +9 -14
  22. package/dist/size-advertiser.d.ts.map +1 -1
  23. package/dist/size-advertiser.js +20 -28
  24. package/dist/types.d.ts +29 -73
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/types.js +1 -15
  27. package/dist/view-anchor.d.ts +36 -77
  28. package/dist/view-anchor.d.ts.map +1 -1
  29. package/dist/view-anchor.js +206 -174
  30. package/docs/bidirectional-design.md +64 -96
  31. package/docs/{anchor-3d.html → index.html} +215 -73
  32. package/docs/mechanism.mdx +55 -49
  33. package/docs/performance-report.md +63 -0
  34. package/docs/protocol.md +79 -0
  35. package/package.json +30 -4
  36. package/src/index.ts +6 -15
  37. package/src/measure-loop.ts +36 -41
  38. package/src/protocol-publisher.ts +236 -0
  39. package/src/protocol-types.ts +43 -0
  40. package/src/protocol.ts +193 -0
  41. package/src/react.ts +186 -141
  42. package/src/size-advertiser.ts +24 -31
  43. package/src/types.ts +34 -79
  44. package/src/view-anchor.ts +228 -212
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # view-anchor
2
2
 
3
- > An engine-agnostic primitive that keeps a main-process native view aligned to a DOM element's geometry.
3
+ > A high-performance geometry bridge that keeps anything living outside the DOM aligned to a DOM element: an Electron `WebContentsView`, a native webview in another desktop shell, a cross-origin iframe, or any surface you position from a rectangle. Every move and resize is published synchronously with no duplicate frames, and the whole package is about 2.6 KB gzipped.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/view-anchor)](https://www.npmjs.com/package/view-anchor)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/view-anchor)](https://www.npmjs.com/package/view-anchor)
@@ -9,19 +9,45 @@
9
9
 
10
10
  [English](./README.md) · [简体中文](./README.zh-CN.md)
11
11
 
12
- In Electron, a native `WebContentsView` lives in the main process while your layout lives in the renderer. Layout libraries (flexbox, dockview, react-resizable-panels…) only move DOM nodes — they have no idea where the process boundary is. `view-anchor` is the bridge across it: it measures a target element's `getBoundingClientRect()`, hands the rectangle to a `publish` callback (your IPC → `setBounds`), and re-publishes whenever the element moves or resizes.
12
+ > 🎮 **Live demo**: the [3D interactive demo](https://lbb00.github.io/view-anchor/) runs the real core in your browser. Drag the splitter, toggle the panel, and watch the native view follow.
13
13
 
14
- The core has no dependencies on React, Electron, or any host layout engine. React code lives only in the adapter layer.
14
+ ## The problem
15
15
 
16
- ## Features
16
+ Some things you want to place inside your layout are not DOM nodes. An Electron `WebContentsView` is positioned by the main process. A native webview in another desktop shell is positioned by host code. The document inside a cross-origin iframe only knows what you tell it over `postMessage`. Your layout, whether it is flexbox, dockview, or react-resizable-panels, only moves DOM nodes and has no idea that something else is supposed to sit exactly on top of one of them.
17
17
 
18
- - **One-to-one binding** a single native view follows a single DOM element. `update()` applies new options and re-publishes immediately.
19
- - **Synchronous, deduplicated publishes** — measures and publishes in the same observer tick, so the native view never trails the DOM by more than the unavoidable cross-process frame. Rects identical to the last publish are dropped.
20
- - **Collapse without destroying** `present: false` publishes a zero rect and stops observing; the host can detach the subview while keeping the `WebContents` alive.
21
- - **Explicit visibility** — the `Placement` API distinguishes a genuinely 0×0-but-visible view from a hidden one, instead of inferring visibility from geometry.
22
- - **Content-driven sizing (reverse direction)** — `createSizeAdvertiser` reports a view's own content size back so a DOM placeholder can grow to match.
23
- - **React adapter** — `useViewAnchor` hook that attaches to a placeholder element.
24
- - **Zero-dependency core** no React, no Electron, no layout-engine imports in the core.
18
+ `view-anchor` closes that gap. You point it at a placeholder element. It measures the element and, every time the element moves or resizes, hands the new rectangle to your `publish` callback. What happens next is up to you: `ipcRenderer.send` plus `view.setBounds` in Electron, `postMessage` to an iframe, or a direct call into whatever positions the surface.
19
+
20
+ The core has no dependency on Electron, a browser shell, React, or any layout library. It only uses `ResizeObserver`, `requestAnimationFrame`, and `getBoundingClientRect`. React support lives in a separate `view-anchor/react` entry.
21
+
22
+ ## Built for the hot path
23
+
24
+ Geometry updates fire on every resize and, when following a drag, on every animation frame. The library is written for that path and the numbers are measured, not assumed:
25
+
26
+ - **Synchronous delivery.** Measurement and publish happen inside the same `ResizeObserver` callback. No timers, no extra frame of lag.
27
+ - **Dedupe before allocate.** A rectangle identical to the last accepted one is rejected by comparing four numbers, before any object is created.
28
+ - **Frame following only when needed.** `followGeometry` polls `requestAnimationFrame` during a scroll burst, a splitter drag, or an explicit `pulse()`, then closes itself once the rectangle settles. Idle cost is zero, and hidden or invalid targets are capped at 30 frames.
29
+ - **O(1) generation changes.** In the protocol layer, moving an anchor to a new generation or clearing it does not touch other anchors.
30
+ - **Latest-wins batching.** Messages queued in the same task are merged in a microtask and only the newest geometry per anchor is sent.
31
+ - **Small, tree-shakeable output.** Every function is a separate export with `sideEffects: false`. If you only need `createViewAnchor`, you pay for 528 bytes gzipped.
32
+
33
+ Numbers from `pnpm benchmark` on Node.js 24, Apple M4, median of three fresh processes:
34
+
35
+ | Operation | Volume | Time |
36
+ | --- | ---: | ---: |
37
+ | `measurePlacement` | 1,000,000 calls | 8.9 ms |
38
+ | Publish a placement message | 1,000,000 calls | 7.3 ms |
39
+ | Decode a valid batch | 100,000 messages | 3.1 ms |
40
+ | Move all anchors to a new generation | 10,000 anchors | 1.7 ms |
41
+ | Flush one message with 100,000 anchors already tracked | 1 message | 0.008 ms |
42
+
43
+ | Entry | Gzipped |
44
+ | --- | ---: |
45
+ | `view-anchor` (everything) | 2.6 KB |
46
+ | `createViewAnchor` alone | 528 B |
47
+ | `view-anchor/protocol` | 1.4 KB |
48
+ | `view-anchor/react` | 2.0 KB |
49
+
50
+ These are same-machine Node.js microbenchmarks. They do not include DOM layout, Electron IPC, or structured clone, so measure those in your own app. Methodology, memory figures, and V8 traces are in [docs/performance-report.md](./docs/performance-report.md).
25
51
 
26
52
  ## Installation
27
53
 
@@ -31,11 +57,11 @@ pnpm add view-anchor
31
57
  npm install view-anchor
32
58
  ```
33
59
 
34
- React is an optional peer dependency; you only need it for the `useViewAnchor` adapter.
60
+ React is an optional peer dependency. Import the hooks from `view-anchor/react`. The root entry also re-exports `useViewAnchor` for compatibility with `v0.1.2`, so any app that imports `view-anchor` needs React installed. `view-anchor/protocol` does not.
35
61
 
36
- ## Quick start
62
+ ## Usage
37
63
 
38
- ### Imperative core
64
+ ### Follow a DOM element
39
65
 
40
66
  ```ts
41
67
  import { createViewAnchor } from 'view-anchor'
@@ -45,14 +71,16 @@ const handle = createViewAnchor(target, {
45
71
  publish: (bounds) => { ... }, // receive live rectangles; wire IPC → setBounds
46
72
  })
47
73
 
48
- handle.update({ present, publish }) // apply new options, re-publishes immediately
74
+ handle.update({ present, publish }) // apply new options and publish right away
49
75
  handle.dispose() // stop observing; never publishes again
50
76
  ```
51
77
 
78
+ Set `present: false` to collapse the view. The core publishes a zero rectangle and stops observing. The host can detach the subview while keeping the `WebContents` alive, so re-showing it later is instant.
79
+
52
80
  ### React
53
81
 
54
82
  ```tsx
55
- import { useViewAnchor } from 'view-anchor'
83
+ import { useViewAnchor } from 'view-anchor/react'
56
84
 
57
85
  function DebugPanel({ visible }: { visible: boolean }) {
58
86
  const ref = useViewAnchor({
@@ -65,49 +93,105 @@ function DebugPanel({ visible }: { visible: boolean }) {
65
93
  }
66
94
  ```
67
95
 
68
- ### Reverse: content size reporting
96
+ The hook survives React 18 and 19 StrictMode double-mounting without publishing stale frames.
69
97
 
70
- When a `WebContentsView`'s size is driven by its own content (for example a toolbar owned by downstream code), run inside that view's own renderer process:
98
+ ### Explicit visibility
99
+
100
+ A zero rectangle cannot tell a hidden view from one that is visible but currently 0×0. When that distinction matters, use the `Placement` API. It publishes `{ visible: true, bounds }` or `{ visible: false }` and adds opt-in scroll and geometry following:
101
+
102
+ ```ts
103
+ import { createPlacementAnchor } from 'view-anchor'
104
+
105
+ const handle = createPlacementAnchor(target, {
106
+ publish: (placement) => { ... },
107
+ followScroll: true, // re-measure when any ancestor scrolls
108
+ followGeometry: true, // poll animation frames during scrolls / drags, stop when steady
109
+ guardDisplayNone: true, // zero-area or display:none target → { visible: false }
110
+ })
111
+
112
+ handle.pulse() // open a short frame-following window, e.g. during a CSS transition
113
+ ```
114
+
115
+ The React version is `usePlacementAnchor` from `view-anchor/react`.
116
+
117
+ ### Let content drive the size
118
+
119
+ Sometimes the hosted surface's size should come from its own content, for example a toolbar rendered by downstream code. Run `createSizeAdvertiser` inside the hosted document. It reports the content size back so a DOM placeholder in the host can grow to match:
71
120
 
72
121
  ```ts
73
122
  import { createSizeAdvertiser } from 'view-anchor'
74
123
 
75
124
  const handle = createSizeAdvertiser(contentWrapper, {
76
- axis: 'block', // this advertiser owns one axis only (block=height / inline=width)
125
+ axis: 'block', // one axis per advertiser: block = height, inline = width
77
126
  publish: (size) => { ... }, // receives { axis, extent }; wire IPC → host
78
127
  })
79
128
 
80
- handle.update(publish) // swap the publish channel and immediately report the current size
129
+ handle.update(publish) // swap the publish channel and report the current size again
81
130
  handle.dispose() // stop observing; never reports again
82
131
  ```
83
132
 
84
- > **Warning:** the `target` must shrink-to-fit on the owned axis if its size is set by the hosted view instead, the cross-process loop cannot converge. See [docs/bidirectional-design.md](./docs/bidirectional-design.md).
133
+ > **Warning:** the target must shrink to fit its content on the owned axis. If the host sets that size instead, the two sides keep reacting to each other and never settle. See [docs/bidirectional-design.md](./docs/bidirectional-design.md).
134
+
135
+ ### Versioned transport across a boundary
136
+
137
+ The core hands you plain `Bounds`, `Placement`, and `AdvertisedSize` values. Once those values cross a process or origin boundary, over IPC or `postMessage`, you usually want validation and ordering. The optional `view-anchor/protocol` entry adds versioned message envelopes, bounded decoding of untrusted input, a per-anchor sequence guard that drops stale messages, and a microtask batcher:
138
+
139
+ ```ts
140
+ import {
141
+ createGeometryBatcher,
142
+ createPlacementMessagePublisher,
143
+ decodeGeometryWireValue,
144
+ } from 'view-anchor/protocol'
145
+
146
+ // sending side (renderer, iframe, ...)
147
+ const batcher = createGeometryBatcher((batch) => ipc.send('geometry', batch))
148
+ const publish = createPlacementMessagePublisher(
149
+ { anchorId: 'editor', generation: 3 },
150
+ batcher.publish,
151
+ )
152
+
153
+ // receiving side (main process, host page, ...)
154
+ const decoded = decodeGeometryWireValue(received, { maxMessages: 100 })
155
+ if (decoded.ok) { /* check the sender, then apply only newer messages */ }
156
+ ```
157
+
158
+ Two rules keep the ordering correct:
159
+
160
+ - **Keep one publisher per `{ anchorId, generation }`.** The batcher and `createGeometrySequenceGuard` remember the highest sequence number seen for each anchor. A publisher rebuilt for the same address restarts at sequence 1 and its messages are dropped as stale. In React, hold it in `useMemo` or `useRef`. Bump `generation` when you really want a fresh start.
161
+ - **A synchronous publisher returns `false` to say "not accepted".** The core then retries the same geometry on the next trigger. A batching publisher returns `true` once queued and owns any later retries.
162
+
163
+ The full contract is in [docs/protocol.md](./docs/protocol.md).
85
164
 
86
165
  ## API
87
166
 
88
167
  | Export | Kind | Purpose |
89
168
  |---|---|---|
90
- | `createViewAnchor(target, opts)` | function | Imperative core: measure and publish live bounds. Zero rect means collapsed. |
91
- | `createPlacementAnchor(target, opts)` | function | Same core with explicit `Placement` visibility, plus opt-in `followScroll` / `followGeometry` / `guardDisplayNone` and `pulse()`. |
169
+ | `createViewAnchor(target, opts)` | function | Measure a DOM element and publish live bounds. A zero rect means collapsed. |
170
+ | `createPlacementAnchor(target, opts)` | function | Same core with explicit `Placement` visibility, opt-in `followScroll` / `followGeometry` / `guardDisplayNone`, and `pulse()`. |
92
171
  | `measurePlacement(target)` | function | Pure measurement: wraps the target rect as `{ visible: true, bounds }`. |
93
- | `useViewAnchor(opts)` | hook | React adapter returning a ref callback for a placeholder element. |
94
- | `createSizeAdvertiser(target, opts)` | function | Reverse core: report the view's own content size to the host. |
172
+ | `createSizeAdvertiser(target, opts)` | function | Reverse direction: report the view's own content size to the host. |
173
+ | `useViewAnchor(opts)` from `view-anchor/react` | hook | Returns a ref callback for a placeholder element. |
174
+ | `usePlacementAnchor(opts)` from `view-anchor/react` | hook | React adapter for the `Placement` API, including `followScroll` and `followGeometry`. |
95
175
  | `Bounds` | type | `{ x, y, width, height }` in CSS pixels. |
96
- | `Placement` | type | `{ visible: true; bounds } \| { visible: false }` — explicit visibility. |
97
- | `ViewAnchorOptions` / `ViewAnchorHandle` | type | Options and handle for the forward zero-rect core. |
98
- | `PlacementAnchorOptions` / `PlacementAnchorHandle` | type | Options and handle for the `Placement` core. |
99
- | `UseViewAnchorOptions` / `ViewAnchorRef` | type | Options and ref shape for the React adapter. |
100
- | `AdvertisedAxis` / `AdvertisedSize` | type | Reverse axis and frame payload types. |
101
- | `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | type | Options and handle for the reverse core. |
176
+ | `Placement` | type | `{ visible: true; bounds } \| { visible: false }`. |
177
+ | `ViewAnchorOptions` / `ViewAnchorHandle` | type | Options and handle for `createViewAnchor`. |
178
+ | `PlacementAnchorOptions` / `PlacementAnchorHandle` | type | Options and handle for `createPlacementAnchor`. |
179
+ | `UseViewAnchorOptions` / `ViewAnchorRef` from `view-anchor/react` | type | Options and ref shape for `useViewAnchor`. |
180
+ | `UsePlacementAnchorOptions` / `PlacementAnchorRef` from `view-anchor/react` | type | Options and ref shape for `usePlacementAnchor`. |
181
+ | `AdvertisedAxis` / `AdvertisedSize` | type | Axis and payload types for the reverse direction. |
182
+ | `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | type | Options and handle for `createSizeAdvertiser`. |
183
+ | `view-anchor/protocol` | functions + types | Versioned messages, strict decoding, sequence guards, message publishers, and microtask batching. |
102
184
 
103
185
  ## Documentation
104
186
 
105
- - [docs/mechanism.mdx](./docs/mechanism.mdx) the forward mechanism in depth: synchronous publishing and stale-frame safety, the `present` / zero-rect / unmount contract, React 18 StrictMode behavior. Includes an interactive 3D demo at [docs/anchor-3d.html](./docs/anchor-3d.html).
106
- - [docs/bidirectional-design.md](./docs/bidirectional-design.md) the bidirectional geometry bridge: the intentional sync/RAF asymmetry, single-axis ownership and convergence, trust boundaries.
187
+ - [docs/mechanism.mdx](./docs/mechanism.mdx): how the forward direction works. Synchronous publishing, stale-frame safety, the `present` / zero-rect / unmount contract, StrictMode behaviour. Includes the interactive 3D demo at [docs/index.html](./docs/index.html).
188
+ - [docs/bidirectional-design.md](./docs/bidirectional-design.md): running both directions at once. Why the forward path is synchronous while the reverse path uses animation frames, single-axis ownership, and where the trust boundary sits.
189
+ - [docs/protocol.md](./docs/protocol.md): message envelopes, validation, ordering, batching, and what happens on failure.
190
+ - [docs/performance-report.md](./docs/performance-report.md): reproducible CPU, heap, RSS, extreme-case, V8, and export-size measurements.
107
191
 
108
192
  ## Contributing
109
193
 
110
- Issues and pull requests are welcome. Before submitting, run the checks locally: `pnpm lint`, `pnpm check-types`, `pnpm test`, `pnpm build`.
194
+ Issues and pull requests are welcome. Before submitting, run `pnpm lint`, `pnpm check-types`, `pnpm test`, and `pnpm build`. `pnpm benchmark` regenerates the performance report.
111
195
 
112
196
  ## License
113
197
 
package/README.zh-CN.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # view-anchor
2
2
 
3
- > 让主进程的原生视图(Electron `WebContentsView`)始终对齐某个 DOM 元素的几何位置的引擎无关原语。
3
+ > 高性能几何桥接库,让任何"不在 DOM 里的东西"始终贴合某个 DOM 元素:Electron `WebContentsView`、其他桌面壳里的原生 webview、跨域 iframe,或者任何你能用一个矩形来定位的画面。每次移动和缩放都同步发布、不会重复发帧,整个包 gzip 后约 2.6 KB。
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/view-anchor)](https://www.npmjs.com/package/view-anchor)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/view-anchor)](https://www.npmjs.com/package/view-anchor)
@@ -9,19 +9,45 @@
9
9
 
10
10
  [English](./README.md) · [简体中文](./README.zh-CN.md)
11
11
 
12
- Electron 里,原生 `WebContentsView` 住主进程,你的布局在渲染进程。DOM 布局库(flexbox、dockview、react-resizable-panels……)只移动 DOM 节点,不知道这道进程边界的存在。`view-anchor` 就是跨过这道边界的桥:它测量目标元素的 `getBoundingClientRect()`,把矩形交给一个 `publish` 回调(由你接上 IPC → `setBounds`),并在元素位移或缩放时重新发布。
12
+ > 🎮 **在线 Demo**:[3D 交互演示](https://lbb00.github.io/view-anchor/) 在浏览器里跑的是真实核心代码。拖动分栏、切换面板显示,看原生视图实时跟随。
13
13
 
14
- 核心不依赖 React、Electron 或任何宿主布局引擎。涉及 React 的代码只在适配层。
14
+ ## 要解决的问题
15
15
 
16
- ## 特性
16
+ 有些你想放进布局里的东西并不是 DOM 节点。Electron 的 `WebContentsView` 由主进程定位;其他桌面壳里的原生 webview 由宿主代码定位;跨域 iframe 里的文档只知道你通过 `postMessage` 告诉它的信息。而你的布局,不管是 flexbox、dockview 还是 react-resizable-panels,只会移动 DOM 节点,并不知道有别的东西需要正好盖在其中某个节点上面。
17
17
 
18
- - **一对一绑定** —— 一个原生视图跟随一个 DOM 元素。`update()` 应用新选项并立即重新发布。
19
- - **同步、去重的发布** —— 在同一个观察回调里测量并发布,原生视图不会比 DOM 多拖一帧(除不可避免的跨进程帧外)。与上次逐字段相同的矩形会被跳过。
20
- - **收起而不销毁** —— `present: false` 发布零矩形并停止观察;宿主可以摘除子视图但保留 `WebContents` 存活。
21
- - **显式可见性** —— `Placement` API 能区分「真正 0×0 但在屏」和「隐藏」的视图,而不是从尺寸推断可见性。
22
- - **反向:内容尺寸回流** —— `createSizeAdvertiser` 把视图自身内容的尺寸回报给宿主,让 DOM 占位跟着内容长。
23
- - **React 适配层** —— `useViewAnchor` hook,挂到一个占位元素上即可。
24
- - **核心零依赖** —— 核心不 import React、Electron 或任何布局引擎。
18
+ `view-anchor` 负责补上这一段。你把它指向一个占位元素,它测量这个元素,元素每次移动或缩放时,把新的矩形交给你的 `publish` 回调。接下来怎么用由你决定:在 Electron 里接 `ipcRenderer.send` 和 `view.setBounds`,对 iframe 就 `postMessage`,或者直接调用任何负责定位那个画面的代码。
19
+
20
+ 核心不依赖 Electron、任何浏览器壳、React 或布局库,只用到 `ResizeObserver`、`requestAnimationFrame` `getBoundingClientRect`。React 支持放在单独的 `view-anchor/react` 入口里。
21
+
22
+ ## 为热路径而写
23
+
24
+ 几何更新在每次缩放时都会触发,跟随拖拽时更是每一帧都触发。这个库就是为这条路径写的,下面的数字是实测,不是推断:
25
+
26
+ - **同步发布。** 测量和发布在同一个 `ResizeObserver` 回调里完成。没有定时器,不多等一帧。
27
+ - **先去重,再分配。** 和上一次已接受的矩形完全相同的结果,只比较四个数字就会被丢弃,不会创建任何对象。
28
+ - **只在需要时逐帧跟随。** `followGeometry` 只在滚动、拖动分栏或显式调用 `pulse()` 时才开始按 `requestAnimationFrame` 轮询,矩形稳定后自动停止。空闲时开销为零,隐藏或无效目标最多跟 30 帧。
29
+ - **generation 切换是 O(1)。** 协议层里,把某个锚点换到新的 generation 或清除它,不会碰到其他锚点。
30
+ - **合并批量,只发最新。** 同一个任务里排队的消息在一个微任务里合并,每个锚点只发送最新的几何数据。
31
+ - **体积小,可摇树。** 每个函数都是独立导出,并声明了 `sideEffects: false`。只用 `createViewAnchor` 的话,你只为 gzip 后 528 字节买单。
32
+
33
+ 以下数字来自 `pnpm benchmark`,环境是 Node.js 24、Apple M4,取三个全新进程的中位数:
34
+
35
+ | 操作 | 数据量 | 耗时 |
36
+ | --- | ---: | ---: |
37
+ | `measurePlacement` | 1,000,000 次 | 8.9 ms |
38
+ | 发布一条 placement 消息 | 1,000,000 次 | 7.3 ms |
39
+ | 解码合法 batch | 100,000 条 | 3.1 ms |
40
+ | 所有锚点切换到新 generation | 10,000 个锚点 | 1.7 ms |
41
+ | 已有 100,000 个锚点时只 flush 一条 | 1 条 | 0.008 ms |
42
+
43
+ | 入口 | gzip 后 |
44
+ | --- | ---: |
45
+ | `view-anchor`(全部) | 2.6 KB |
46
+ | 单独 `createViewAnchor` | 528 B |
47
+ | `view-anchor/protocol` | 1.4 KB |
48
+ | `view-anchor/react` | 2.0 KB |
49
+
50
+ 这些是同一台机器上的 Node.js 微基准,不包含 DOM layout、Electron IPC 和 structured clone,这几项请在你自己的应用里测。方法、内存数据和 V8 trace 见 [docs/performance-report.md](./docs/performance-report.md)。
25
51
 
26
52
  ## 安装
27
53
 
@@ -31,84 +57,142 @@ pnpm add view-anchor
31
57
  npm install view-anchor
32
58
  ```
33
59
 
34
- React 是可选 peer 依赖,只有用 `useViewAnchor` 适配层时才需要。
60
+ React 是可选的 peer dependency。Hook `view-anchor/react` 导入。为了兼容 `v0.1.2`,根入口也会重新导出 `useViewAnchor`,所以只要导入了 `view-anchor` 就需要装 React。`view-anchor/protocol` 不需要。
35
61
 
36
- ## 快速上手
62
+ ## 用法
37
63
 
38
- ### 命令式核心
64
+ ### 跟随一个 DOM 元素
39
65
 
40
66
  ```ts
41
67
  import { createViewAnchor } from 'view-anchor'
42
68
 
43
69
  const handle = createViewAnchor(target, {
44
70
  present: true, // 挂载原生视图
45
- publish: (bounds) => { ... }, // 接收实时矩形;由它负责 IPC → setBounds
71
+ publish: (bounds) => { ... }, // 收到实时矩形;接 IPC → setBounds
46
72
  })
47
73
 
48
- handle.update({ present, publish }) // 应用新选项(会立即重新发布)
49
- handle.dispose() // 停止观察;此后不再发布
74
+ handle.update({ present, publish }) // 应用新选项并立刻发布一次
75
+ handle.dispose() // 停止观察;之后不会再发布
50
76
  ```
51
77
 
78
+ 把 `present` 设为 `false` 可以收起视图。核心会发布一个零矩形并停止观察。宿主可以把子视图摘下来但保留 `WebContents`,这样之后再显示时是即时的。
79
+
52
80
  ### React
53
81
 
54
82
  ```tsx
55
- import { useViewAnchor } from 'view-anchor'
83
+ import { useViewAnchor } from 'view-anchor/react'
56
84
 
57
85
  function DebugPanel({ visible }: { visible: boolean }) {
58
86
  const ref = useViewAnchor({
59
87
  present: visible,
60
88
  publish: publishPanelBounds,
61
89
  })
62
- // 原生视图跟随这个占位 div。隐藏面板
63
- // (visible=false 或卸载)会让它收起,但不销毁。
90
+ // 原生视图跟随这个占位 div。隐藏面板(visible=false 或卸载)
91
+ // 只会收起视图,不会销毁它。
64
92
  return <div ref={ref} className="h-full w-full" />
65
93
  }
66
94
  ```
67
95
 
68
- ### 反向:内容尺寸上报
96
+ Hook 在 React 18 和 19 的 StrictMode 双重挂载下不会发出过期的帧。
69
97
 
70
- 当一块 `WebContentsView` 的尺寸由它**自己的内容**主导时(例如交给下游控制的 toolbar),在**下游视图自己的渲染进程**里跑:
98
+ ### 显式可见性
99
+
100
+ 零矩形分不清"视图被隐藏了"和"视图可见但此刻正好是 0×0"。需要区分这两种情况时,用 `Placement` API。它发布的是 `{ visible: true, bounds }` 或 `{ visible: false }`,并提供可选的滚动跟随和几何跟随:
101
+
102
+ ```ts
103
+ import { createPlacementAnchor } from 'view-anchor'
104
+
105
+ const handle = createPlacementAnchor(target, {
106
+ publish: (placement) => { ... },
107
+ followScroll: true, // 任意祖先滚动时重新测量
108
+ followGeometry: true, // 滚动 / 拖拽期间逐帧轮询,稳定后停止
109
+ guardDisplayNone: true, // 零面积或 display:none 的目标 → { visible: false }
110
+ })
111
+
112
+ handle.pulse() // 打开一小段逐帧跟随窗口,比如 CSS 过渡期间
113
+ ```
114
+
115
+ React 版本是 `view-anchor/react` 里的 `usePlacementAnchor`。
116
+
117
+ ### 由内容决定尺寸
118
+
119
+ 有时被嵌入的那个画面的尺寸应该由它自己的内容决定,比如一条由下游代码渲染的工具栏。这种情况在被嵌入的文档里运行 `createSizeAdvertiser`,它把内容尺寸报告回去,宿主里的 DOM 占位元素就能跟着长到一样大:
71
120
 
72
121
  ```ts
73
122
  import { createSizeAdvertiser } from 'view-anchor'
74
123
 
75
124
  const handle = createSizeAdvertiser(contentWrapper, {
76
- axis: 'block', // 这个 advertiser 只主导一条轴(block=高 / inline=宽)
77
- publish: (size) => { ... }, // 接收 { axis, extent },由它负责 IPC → 宿主
125
+ axis: 'block', // 一个 advertiser 只管一个轴:block = 高度,inline = 宽度
126
+ publish: (size) => { ... }, // 收到 { axis, extent };接 IPC → 宿主
78
127
  })
79
128
 
80
- handle.update(publish) // 换 publish(IPC 通道),并立即把当前尺寸发给它
81
- handle.dispose() // 停止观察;此后不再上报
129
+ handle.update(publish) // 换一个发布通道,并立刻再报告一次当前尺寸
130
+ handle.dispose() // 停止观察;之后不会再报告
82
131
  ```
83
132
 
84
- > **注意 footgun**:`target` 必须在主导轴上 shrink-to-fit——如果它的尺寸由宿主灌入的视图尺寸反向决定,跨进程环不会收敛。详见 [docs/bidirectional-design.md](./docs/bidirectional-design.md)。
133
+ > **注意:** 目标元素在它负责的那个轴上必须是随内容收缩的。如果这个尺寸反过来由宿主设置,两边会互相触发、永远停不下来。见 [docs/bidirectional-design.md](./docs/bidirectional-design.md)。
134
+
135
+ ### 跨边界的带版本传输
136
+
137
+ 核心交给你的是普通的 `Bounds`、`Placement` 和 `AdvertisedSize` 值。这些值一旦通过 IPC 或 `postMessage` 跨过进程或源的边界,通常就需要校验和排序。可选的 `view-anchor/protocol` 入口提供带版本的消息信封、对不可信输入的有界解码、按锚点丢弃过期消息的序列守卫,以及一个微任务批处理器:
138
+
139
+ ```ts
140
+ import {
141
+ createGeometryBatcher,
142
+ createPlacementMessagePublisher,
143
+ decodeGeometryWireValue,
144
+ } from 'view-anchor/protocol'
145
+
146
+ // 发送方(渲染进程、iframe 等)
147
+ const batcher = createGeometryBatcher((batch) => ipc.send('geometry', batch))
148
+ const publish = createPlacementMessagePublisher(
149
+ { anchorId: 'editor', generation: 3 },
150
+ batcher.publish,
151
+ )
152
+
153
+ // 接收方(主进程、宿主页面等)
154
+ const decoded = decodeGeometryWireValue(received, { maxMessages: 100 })
155
+ if (decoded.ok) { /* 先校验发送方,再只应用更新的消息 */ }
156
+ ```
157
+
158
+ 两条规则保证顺序正确:
159
+
160
+ - **每个 `{ anchorId, generation }` 只保留一个 publisher。** batcher 和 `createGeometrySequenceGuard` 会记住每个锚点见过的最大序号。针对同一地址重建 publisher 会让序号从 1 重新开始,它发的消息会被当成过期而丢弃。在 React 里用 `useMemo` 或 `useRef` 持有它。确实需要重新开始时,把 `generation` 加一。
161
+ - **同步 publisher 返回 `false` 表示"未接受"。** 核心会在下一次触发时重试同一份几何数据。批处理 publisher 入队后就返回 `true`,之后的重试由它自己负责。
162
+
163
+ 完整约定见 [docs/protocol.md](./docs/protocol.md)。
85
164
 
86
165
  ## API
87
166
 
88
- | 导出 | 类型 | 作用 |
167
+ | 导出 | 类型 | 用途 |
89
168
  |---|---|---|
90
- | `createViewAnchor(target, opts)` | 函数 | 正向命令式核心:测量并发布实时边界。零矩形表示收起。 |
91
- | `createPlacementAnchor(target, opts)` | 函数 | 同款核心的显式 `Placement` 变体,另有 opt-in 的 `followScroll` / `followGeometry` / `guardDisplayNone` `pulse()`。 |
169
+ | `createViewAnchor(target, opts)` | 函数 | 测量 DOM 元素并发布实时 bounds。零矩形表示已收起。 |
170
+ | `createPlacementAnchor(target, opts)` | 函数 | 同一个核心,带显式 `Placement` 可见性、可选的 `followScroll` / `followGeometry` / `guardDisplayNone`,以及 `pulse()`。 |
92
171
  | `measurePlacement(target)` | 函数 | 纯测量:把目标矩形包成 `{ visible: true, bounds }`。 |
93
- | `useViewAnchor(opts)` | Hook | React 适配层,返回挂占位元素的 ref 回调。 |
94
- | `createSizeAdvertiser(target, opts)` | 函数 | 反向核心:把视图自身内容尺寸回报给宿主。 |
95
- | `Bounds` | 类型 | `{ x, y, width, height }`,单位 CSS 像素。 |
96
- | `Placement` | 类型 | `{ visible: true; bounds } \| { visible: false }` —— 显式可见性。 |
97
- | `ViewAnchorOptions` / `ViewAnchorHandle` | 类型 | 正向零矩形核心的选项与句柄形状。 |
98
- | `PlacementAnchorOptions` / `PlacementAnchorHandle` | 类型 | `Placement` 核心的选项与句柄形状。 |
99
- | `UseViewAnchorOptions` / `ViewAnchorRef` | 类型 | React 适配层的选项与 ref 形状。 |
100
- | `AdvertisedAxis` / `AdvertisedSize` | 类型 | 反向的轴与帧载荷类型。 |
101
- | `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | 类型 | 反向核心的选项与句柄形状。 |
172
+ | `createSizeAdvertiser(target, opts)` | 函数 | 反向:把视图自身的内容尺寸报告给宿主。 |
173
+ | `useViewAnchor(opts)`,来自 `view-anchor/react` | Hook | 返回用于占位元素的 ref 回调。 |
174
+ | `usePlacementAnchor(opts)`,来自 `view-anchor/react` | Hook | `Placement` API React 适配,包含 `followScroll` `followGeometry`。 |
175
+ | `Bounds` | 类型 | CSS 像素单位的 `{ x, y, width, height }`。 |
176
+ | `Placement` | 类型 | `{ visible: true; bounds } \| { visible: false }`。 |
177
+ | `ViewAnchorOptions` / `ViewAnchorHandle` | 类型 | `createViewAnchor` 的选项和句柄。 |
178
+ | `PlacementAnchorOptions` / `PlacementAnchorHandle` | 类型 | `createPlacementAnchor` 的选项和句柄。 |
179
+ | `UseViewAnchorOptions` / `ViewAnchorRef`,来自 `view-anchor/react` | 类型 | `useViewAnchor` 的选项和 ref 形状。 |
180
+ | `UsePlacementAnchorOptions` / `PlacementAnchorRef`,来自 `view-anchor/react` | 类型 | `usePlacementAnchor` 的选项和 ref 形状。 |
181
+ | `AdvertisedAxis` / `AdvertisedSize` | 类型 | 反向方向的轴和负载类型。 |
182
+ | `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | 类型 | `createSizeAdvertiser` 的选项和句柄。 |
183
+ | `view-anchor/protocol` | 函数 + 类型 | 带版本的消息、严格解码、序列守卫、消息 publisher 和微任务批处理。 |
102
184
 
103
185
  ## 文档
104
186
 
105
- - [docs/mechanism.mdx](./docs/mechanism.mdx) —— 正向机制的完整说明:同步发布与陈旧帧安全、`present` / 零矩形 / 卸载契约、React 18 StrictMode 行为。内含可交互 3D 演示 [docs/anchor-3d.html](./docs/anchor-3d.html)
106
- - [docs/bidirectional-design.md](./docs/bidirectional-design.md) —— 双向几何桥:同步 / RAF 的刻意不对称、单轴所有权与收敛性、信任边界。
187
+ - [docs/mechanism.mdx](./docs/mechanism.mdx):正向方向的原理。同步发布、防止过期帧、`present` / 零矩形 / 卸载的约定、StrictMode 下的行为。内含 [docs/index.html](./docs/index.html) 的 3D 交互演示。
188
+ - [docs/bidirectional-design.md](./docs/bidirectional-design.md):两个方向同时运行时的设计。为什么正向是同步的而反向走动画帧、单轴归属,以及信任边界在哪。
189
+ - [docs/protocol.md](./docs/protocol.md):消息信封、校验、排序、批处理,以及失败时会发生什么。
190
+ - [docs/performance-report.md](./docs/performance-report.md):可复现的 CPU、堆、RSS、极端场景、V8 和导出体积测量。
107
191
 
108
- ## 贡献
192
+ ## 参与贡献
109
193
 
110
- 欢迎提 issue 和 PR。提交前请在本地跑一遍:`pnpm lint`、`pnpm check-types`、`pnpm test`、`pnpm build`。
194
+ 欢迎提 issue 和 pull request。提交前请运行 `pnpm lint`、`pnpm check-types`、`pnpm test` 和 `pnpm build`。`pnpm benchmark` 会重新生成性能报告。
111
195
 
112
- ## License
196
+ ## 许可证
113
197
 
114
198
  [MIT](./LICENSE) © lbb00
package/dist/index.d.ts CHANGED
@@ -1,23 +1,11 @@
1
1
  /**
2
- * view-anchor engine-agnostic primitive that keeps a main-process native
3
- * view (Electron `WebContentsView`) aligned to a DOM element's geometry.
4
- *
5
- * Public surface:
6
- * - `createViewAnchor` — forward: DOM rect → native view bounds.
7
- * - `useViewAnchor` — React adapter returning a ref callback.
8
- * - `createSizeAdvertiser`— reverse: downstream content size → host.
9
- * - `Bounds` / `AdvertisedSize` / option + handle types.
10
- *
11
- * Self-contained on purpose: the only runtime deps are `react` (adapter
12
- * only) and browser APIs (`ResizeObserver` / `requestAnimationFrame` /
13
- * `getBoundingClientRect`). See the design notes and the interactive 3D
14
- * walkthrough in `docs/` (`mechanism.mdx` / `anchor-3d.html`).
2
+ * view-anchor: keeps an external surface aligned with a DOM element's geometry.
15
3
  */
16
4
  export { createViewAnchor, measurePlacement, createPlacementAnchor, } from './view-anchor.js';
17
5
  export type { PlacementAnchorOptions, PlacementAnchorHandle, } from './view-anchor.js';
18
- export type { Bounds, Placement, ViewAnchorOptions, ViewAnchorHandle, } from './types.js';
19
- export { useViewAnchor } from './react.js';
20
- export type { UseViewAnchorOptions, ViewAnchorRef } from './react.js';
6
+ export type { Bounds, Placement, Publisher, PublishResult, ViewAnchorOptions, ViewAnchorHandle, } from './types.js';
21
7
  export { createSizeAdvertiser } from './size-advertiser.js';
8
+ export { useViewAnchor } from './react.js';
22
9
  export type { AdvertisedAxis, AdvertisedSize, SizeAdvertiserOptions, SizeAdvertiserHandle, } from './types.js';
10
+ export type { UseViewAnchorOptions, ViewAnchorRef } from './react.js';
23
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,MAAM,EACN,SAAS,EACT,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC1C,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3D,YAAY,EACV,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,MAAM,EACN,SAAS,EACT,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC1C,YAAY,EACV,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -1,18 +1,6 @@
1
1
  /**
2
- * view-anchor engine-agnostic primitive that keeps a main-process native
3
- * view (Electron `WebContentsView`) aligned to a DOM element's geometry.
4
- *
5
- * Public surface:
6
- * - `createViewAnchor` — forward: DOM rect → native view bounds.
7
- * - `useViewAnchor` — React adapter returning a ref callback.
8
- * - `createSizeAdvertiser`— reverse: downstream content size → host.
9
- * - `Bounds` / `AdvertisedSize` / option + handle types.
10
- *
11
- * Self-contained on purpose: the only runtime deps are `react` (adapter
12
- * only) and browser APIs (`ResizeObserver` / `requestAnimationFrame` /
13
- * `getBoundingClientRect`). See the design notes and the interactive 3D
14
- * walkthrough in `docs/` (`mechanism.mdx` / `anchor-3d.html`).
2
+ * view-anchor: keeps an external surface aligned with a DOM element's geometry.
15
3
  */
16
4
  export { createViewAnchor, measurePlacement, createPlacementAnchor, } from './view-anchor.js';
17
- export { useViewAnchor } from './react.js';
18
5
  export { createSizeAdvertiser } from './size-advertiser.js';
6
+ export { useViewAnchor } from './react.js';
@@ -1,29 +1,9 @@
1
1
  /**
2
- * Internal the RAF-coalesced measure/dedupe/dispose engine behind the REVERSE
3
- * primitive `createSizeAdvertiser`.
2
+ * Internal helper: animation-frame scheduling and deduplication loop for
3
+ * `createSizeAdvertiser`.
4
4
  *
5
- * The forward `createViewAnchor` deliberately does NOT use this: it publishes
6
- * SYNCHRONOUSLY (a native overlay's `setBounds` already lands a cross-process
7
- * frame late, and a RAF stacked a second frame of visible trailing). The
8
- * reverse direction is different — it is a cross-process FEEDBACK loop
9
- * (advertise → host resizes the view → content re-measures → re-advertise), so
10
- * the RAF's one-publish-per-frame coalescing is a useful damper. The two
11
- * directions thus have different optimal emit timing; this engine serves only
12
- * the reverse.
13
- *
14
- * NOT exported from the package: it is pure mechanism with no knowledge of
15
- * direction, the DOM, `ResizeObserver`, or the structure of the value `T` it
16
- * carries. The wrapping primitive injects `produce` / `same` / `sink` and
17
- * drives the lifecycle.
18
- *
19
- * - `schedule()` — coalesce a burst of triggers into ONE RAF; the frame
20
- * body re-`produce()`s, dedupes against the last emit (`same`), and `sink`s.
21
- * Bails if inactive or disposed (stale-RAF safe).
22
- * - `emitNow(v)` — explicit synchronous emit (create / update path). Always
23
- * fires, bypassing the dedupe check, and refreshes the dedupe baseline.
24
- * - `setActive` — gate the observer stream; a queued frame bails on `!active`.
25
- * - `cancel` — drop any in-flight RAF.
26
- * - `dispose` — cancel + go inert; after dispose nothing emits again.
5
+ * Coalesces resize triggers into a single requestAnimationFrame, drops
6
+ * duplicate measurements, and manages disposal.
27
7
  */
28
8
  export interface MeasureLoop<T> {
29
9
  schedule(): void;
@@ -33,11 +13,12 @@ export interface MeasureLoop<T> {
33
13
  dispose(): void;
34
14
  }
35
15
  export declare function createMeasureLoop<T>(cfg: {
36
- /** Produce the value to emit in the RAF body. Return `null` to decline the
37
- * frame entirely (no dedupe, no sink, baseline untouched) e.g. a
38
- * non-finite or unavailable measurement. */
16
+ /**
17
+ * Produce the value to emit in the animation frame. Return null to skip
18
+ * the frame (e.g. for non-finite measurements).
19
+ */
39
20
  produce: () => T | null;
40
21
  same: (a: T, b: T) => boolean;
41
- sink: (value: T) => void;
22
+ sink: import('./types.js').Publisher<T>;
42
23
  }): MeasureLoop<T>;
43
24
  //# sourceMappingURL=measure-loop.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"measure-loop.d.ts","sourceRoot":"","sources":["../src/measure-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,QAAQ,IAAI,IAAI,CAAA;IAChB,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAA;IACvB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC5B,MAAM,IAAI,IAAI,CAAA;IACd,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE;IACxC;;iDAE6C;IAC7C,OAAO,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;IACvB,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;IAC7B,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAA;CACzB,GAAG,WAAW,CAAC,CAAC,CAAC,CA4CjB"}
1
+ {"version":3,"file":"measure-loop.d.ts","sourceRoot":"","sources":["../src/measure-loop.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,QAAQ,IAAI,IAAI,CAAA;IAChB,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAA;IACvB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC5B,MAAM,IAAI,IAAI,CAAA;IACd,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE;IACxC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;IACvB,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;IAC7B,IAAI,EAAE,OAAO,YAAY,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;CACxC,GAAG,WAAW,CAAC,CAAC,CAAC,CAyDjB"}