@dimina-kit/view-anchor 0.1.0-dev.20260616102751 → 0.1.0-dev.20260618090552
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/README.md +10 -6
- package/docs/bidirectional-design.md +17 -28
- package/docs/mechanism.mdx +27 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,22 +66,26 @@ handle.update(publish) // 换 publish(IPC 通道),并立即把当前尺寸
|
|
|
66
66
|
handle.dispose() // 停止观察;此后不再上报
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
它从 `ResizeObserver` 的 border-box 取主导轴尺寸(不调 `getBoundingClientRect`、不强制 reflow),`Math.round` +
|
|
69
|
+
它从 `ResizeObserver` 的 border-box 取主导轴尺寸(不调 `getBoundingClientRect`、不强制 reflow),`Math.round` + 钳零,经反向专用的 RAF 合并 + 去重发出(正向同步、反向 RAF,刻意不对称)。宿主收到尺寸后调整占位、再由正向把视图贴上去——两个单向原语经占位 div 串成一座双向桥。**注意 footgun**:`target` 必须在主导轴上 shrink-to-fit(其尺寸不能被宿主灌入的视图尺寸反向决定),否则跨进程环不收敛。详见 [`docs/bidirectional-design.md`](./docs/bidirectional-design.md)。
|
|
70
70
|
|
|
71
71
|
## 文档
|
|
72
72
|
|
|
73
|
-
- [`docs/mechanism.mdx`](./docs/mechanism.mdx) ——
|
|
74
|
-
- [`docs/bidirectional-design.md`](./docs/bidirectional-design.md) ——
|
|
73
|
+
- [`docs/mechanism.mdx`](./docs/mechanism.mdx) —— 正向完整机制:同步发布与陈旧帧安全性、`present` / 零矩形 / 卸载契约、React 18 StrictMode 生命周期。内嵌可交互 3D 演示 [`docs/anchor-3d.html`](./docs/anchor-3d.html)。
|
|
74
|
+
- [`docs/bidirectional-design.md`](./docs/bidirectional-design.md) —— 双向几何桥:正向同步 / 反向 RAF 的刻意不对称、单轴所有权与收敛性、信任边界、以及两个原语如何「锚」到一起。
|
|
75
75
|
|
|
76
76
|
## API
|
|
77
77
|
|
|
78
78
|
| 导出 | 类型 | 作用 |
|
|
79
79
|
|---|---|---|
|
|
80
|
-
| `createViewAnchor(target, opts)` | 函数 | 正向命令式核心,返回 `{ update, dispose }
|
|
81
|
-
| `
|
|
80
|
+
| `createViewAnchor(target, opts)` | 函数 | 正向命令式核心,返回 `{ update, dispose }`。`present:false` 用零矩形 `{0,0,0,0}` 表示收起。不依赖 React、不依赖 Electron。 |
|
|
81
|
+
| `createPlacementAnchor(target, opts)` | 函数 | 正向核心的显式 `Placement` 变体:可见性是判别式 `{ visible:true, bounds }` / `{ visible:false }`,绝不从零尺寸推断——真正 0×0 但在屏的视图与隐藏视图就此可区分。另支持 opt-in 的 `guardDisplayNone` / `followScroll` / `followGeometry`(按需开窗的 RAF 几何哨兵)+ `pulse()`。 |
|
|
82
|
+
| `measurePlacement(target)` | 函数 | 纯测量:读 `target` 矩形,包成 `{ visible:true, bounds }`。 |
|
|
83
|
+
| `useViewAnchor(opts)` | Hook | 正向 React 适配层(基于 `createViewAnchor`),返回一个挂到占位元素上的 ref 回调。 |
|
|
82
84
|
| `createSizeAdvertiser(target, opts)` | 函数 | 反向命令式核心,返回 `{ update, dispose }`。下游量内容尺寸回流给宿主。 |
|
|
83
85
|
| `Bounds` | 类型 | `{ x, y, width, height }`,单位为 CSS 像素。 |
|
|
84
|
-
| `
|
|
86
|
+
| `Placement` | 类型 | `{ visible:true; bounds:Bounds } \| { visible:false }`,显式可见性判别式。 |
|
|
87
|
+
| `ViewAnchorOptions` / `ViewAnchorHandle` | 类型 | 正向零矩形核心的选项与句柄形状。 |
|
|
88
|
+
| `PlacementAnchorOptions` / `PlacementAnchorHandle` | 类型 | 正向 `Placement` 核心的选项与句柄形状。 |
|
|
85
89
|
| `UseViewAnchorOptions` / `ViewAnchorRef` | 类型 | 正向适配层的选项与 ref 回调形状。 |
|
|
86
90
|
| `AdvertisedAxis` / `AdvertisedSize` | 类型 | 反向的轴(`'block'\|'inline'`)与帧载荷 `{ axis, extent }`。 |
|
|
87
91
|
| `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | 类型 | 反向核心的选项与句柄形状。 |
|
|
@@ -1,27 +1,21 @@
|
|
|
1
|
-
# view-anchor
|
|
1
|
+
# view-anchor 双向几何桥
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
view-anchor 是一座**双向几何桥**,引擎无关、传输注入:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- **正向** `createViewAnchor(target, { present, publish })` —— 宿主渲染进程量 DOM 占位矩形 → `publish(bounds)` → IPC → 主进程 `WebContentsView.setBounds`。
|
|
6
|
+
- **反向** `createSizeAdvertiser(target, { axis, publish })` —— 下游 WebContentsView 自己的渲染进程量自身内容尺寸 → `publish(size)` → IPC → 宿主,宿主据此调整占位尺寸,再经正向把视图贴上去。
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
适用场景:toolbar 这类「**交给下游控制**」的 WebContentsView,尺寸的事实来源在下游一侧(典型:宽由宿主主导、高由下游内容主导),占位映射是动态的。
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
- **反向(新增)** `createSizeAdvertiser(target, { axis, publish })` —— 下游 WebContentsView 自己的渲染进程量自身内容尺寸 → `publish(size)` → IPC → 宿主,宿主据此调整占位尺寸,再经正向把视图贴上去。
|
|
10
|
+
范围之外:不是同层渲染(合成器层面、另一套机制);包里不提供 React 反向适配、不提供宿主决策 `decide`(见 §4/§7)。
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
## 2. 正反向不共享发射核心——两个方向最优时机不同
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
正反向的最优发射时机本就不同,所以它们**不共享发射核心**,采取**正向同步、反向 RAF** 的刻意不对称。
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
**正向 `createViewAnchor` = 同步发布,不走 RAF。** 原生 overlay 的 `setBounds` 是跨进程、本就晚约 1 合成帧;RAF 再叠一帧 → 拖拽可见拖尾。所以正向在每个 `ResizeObserver` / window `resize` 触发里**同步**测量+发布,抗洪由 `lastPublished` 同值去重承担。撤销安全靠「每次发布开头同步读 `disposed`/`present`」——没有排队帧可跑赢状态变化。正向的 sink 是 IPC→主进程 `setBounds`,**不碰本渲染进程 DOM**,所以同步发布不会形成 RO 重入循环。
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
> 的生产正向合并,发现这是**错的**:正反向的最优发射时机本就不同,强行共享会让核心
|
|
20
|
-
> 长出 by-direction flag。最终采取**正向同步、反向 RAF** 的刻意不对称。
|
|
21
|
-
|
|
22
|
-
**正向 `createViewAnchor` = 同步发布,不走 RAF。** 原生 overlay 的 `setBounds` 是跨进程、本就晚约 1 合成帧;RAF 再叠一帧 → 拖拽可见拖尾(refactor-simulator 生产验证过的修复)。所以正向在每个 `ResizeObserver` / window `resize` 触发里**同步**测量+发布,抗洪由 `lastPublished` 同值去重承担。撤销安全靠「每次发布开头同步读 `disposed`/`present`」——没有排队帧可跑赢状态变化。正向的 sink 是 IPC→主进程 `setBounds`,**不碰本渲染进程 DOM**,所以同步发布不会形成 RO 重入循环。
|
|
23
|
-
|
|
24
|
-
**反向 `createSizeAdvertiser` = RAF 合并(内部 `createMeasureLoop`,反向专用)。** 反向是一条**跨进程反馈环**:advertise → 宿主 resize 这块视图 → 下游内容 remeasure → 再 advertise。RAF 的「每帧≤1 次发布」对这条环是合理的阻尼。`createMeasureLoop<T>` 只服务反向,通过注入 `produce`/`same`/`sink` 三元组把方向细节关在外层,核心零 flag:
|
|
18
|
+
**反向 `createSizeAdvertiser` = RAF 合并(内部 `createMeasureLoop`,反向专用)。** 反向是一条**跨进程反馈环**:advertise → 宿主 resize 这块视图 → 下游内容 remeasure → 再 advertise。RAF 的「每帧≤1 次发布」对这条环是合理的阻尼。`createMeasureLoop<T>` 只服务反向(正向不用它),通过注入 `produce`/`same`/`sink` 三元组把方向细节关在外层,核心零 flag:
|
|
25
19
|
|
|
26
20
|
```ts
|
|
27
21
|
function createMeasureLoop<T>(cfg: {
|
|
@@ -31,7 +25,7 @@ function createMeasureLoop<T>(cfg: {
|
|
|
31
25
|
}): { schedule, emitNow, setActive, cancel, dispose }
|
|
32
26
|
```
|
|
33
27
|
|
|
34
|
-
**为什么不对称是对的**:正向是单向跟随者,少一帧直接消除可见拖尾;反向是反馈环,多一帧阻尼更稳。把两者塞进一个核心要么逼出 `if(direction)` flag
|
|
28
|
+
**为什么不对称是对的**:正向是单向跟随者,少一帧直接消除可见拖尾;反向是反馈环,多一帧阻尼更稳。把两者塞进一个核心要么逼出 `if(direction)` flag,要么把正向也拖进 RAF(带回拖尾)。所以**正向内联同步、反向用 RAF 引擎**,各取所需。
|
|
35
29
|
|
|
36
30
|
## 3. 反向原语契约
|
|
37
31
|
|
|
@@ -50,7 +44,7 @@ export interface SizeAdvertiserOptions {
|
|
|
50
44
|
}
|
|
51
45
|
|
|
52
46
|
export interface SizeAdvertiserHandle {
|
|
53
|
-
update(
|
|
47
|
+
update(publish: (size: AdvertisedSize) => void): void // 只能换 publish(换 IPC 通道);axis 不可变
|
|
54
48
|
dispose(): void // 停 observe、取消 RAF,此后永不再 publish
|
|
55
49
|
}
|
|
56
50
|
|
|
@@ -84,7 +78,7 @@ export function createSizeAdvertiser(target: HTMLElement, opts: SizeAdvertiserOp
|
|
|
84
78
|
| 白名单轴 | 宿主 | 用 payload 的 `axis` 常量比对,`axis !== expected` 则丢弃 |
|
|
85
79
|
| 位置 / 另一轴 / z-order 锁定 | 宿主 | 下游无任何途径改变 → 杜绝全窗覆盖类点击劫持 |
|
|
86
80
|
|
|
87
|
-
**target 选错(反馈环)的防护——克制为主,不过度防御。** 关键事实:违反单轴所有权时,RAF 合并把「死循环」**封顶为每帧一次**——它退化成可见的抖动 / 每帧一次冗余 IPC,而**不是冻死 UI
|
|
81
|
+
**target 选错(反馈环)的防护——克制为主,不过度防御。** 关键事实:违反单轴所有权时,RAF 合并把「死循环」**封顶为每帧一次**——它退化成可见的抖动 / 每帧一次冗余 IPC,而**不是冻死 UI**。既然不是灾难性故障,就不在每帧路径上堆检测器。只保留:
|
|
88
82
|
|
|
89
83
|
- **一条构造期廉价守卫**:`target` 是 `<body>`/`<html>` 时 `console.warn`——这是「stable-but-wrong」(占位永远撑成视图高、不缩到内容)最常见的成因,一次性、零每帧成本。
|
|
90
84
|
- **其余靠文档**:JSDoc 与本节把「target 必须主导轴 shrink-to-fit、不被宿主灌入的尺寸反向决定」讲清。
|
|
@@ -137,15 +131,10 @@ export function createSizeAdvertiser(target: HTMLElement, opts: SizeAdvertiserOp
|
|
|
137
131
|
|
|
138
132
|
## 7. 与 Electron preferred-size 的关系(宿主侧可选数据源,非替代)
|
|
139
133
|
|
|
140
|
-
宿主若在 Electron 且能接受零下游代码,可用 `enablePreferredSizeMode` + `preferred-size-changed` 作为反向「**源**」喂给同一个 `decide`,省掉下游注入。但它是 Electron
|
|
141
|
-
|
|
142
|
-
view-anchor 的反向原语是**引擎无关/可移植**那条路(非 Electron / iframe 宿主、需选子 target 的场景)。两者并存:preferred-size 是 Electron 捷径,advertiser 是可移植机制,`decide` + §5 安全红线是二者共用的公共底座。这也是反向逻辑该留在 view-anchor、而非写死成 Electron 事件的理由。spike 结果只决定宿主选哪个**源**,不改变 view-anchor 要不要建反向原语。
|
|
143
|
-
|
|
144
|
-
## 8. 包定位守恒(仍是一个原语,不是框架)
|
|
134
|
+
宿主若在 Electron 且能接受零下游代码,可用 `enablePreferredSizeMode` + `preferred-size-changed` 作为反向「**源**」喂给同一个 `decide`,省掉下游注入。但它是 Electron 私有,且依赖两个前提:① 在 `WebContentsView` 上触发且上报值吸收 zoom;② `setBounds.height` 不回灌 layout(否则正反馈震荡)。
|
|
145
135
|
|
|
146
|
-
|
|
136
|
+
view-anchor 的反向原语是**引擎无关/可移植**那条路(非 Electron / iframe 宿主、需选子 target 的场景)。两者并存:preferred-size 是 Electron 捷径,advertiser 是可移植机制,`decide` + §5 安全红线是二者共用的公共底座。这也是反向逻辑留在 view-anchor、而非写死成 Electron 事件的理由——宿主选哪个**源**不改变反向原语本身。
|
|
147
137
|
|
|
148
|
-
##
|
|
138
|
+
## 8. 包定位守恒(一个原语,不是框架)
|
|
149
139
|
|
|
150
|
-
|
|
151
|
-
仍待定:React 反向适配 `useSizeAdvertiser` 是否要做(默认否,等出现真实 React 下游)。
|
|
140
|
+
view-anchor 是「DOM 几何 ↔ 跨进程视图」的**单一职责双向桥**。守住四条即不滑向框架:① 导出面只含 `createViewAnchor` / `createPlacementAnchor` / `createSizeAdvertiser` + 其类型;② `createMeasureLoop` 不导出;③ 不提供 React 反向适配(下游不保证是 React);④ `decide` 留宿主、不进包。
|
package/docs/mechanism.mdx
CHANGED
|
@@ -61,6 +61,27 @@ const handle = createViewAnchor(target, {
|
|
|
61
61
|
- **`{0,0,0,0}`(ZERO)** —— 收起信号。宿主把零面积读作「摘除子视图,但保留其 `WebContents` 存活」,即收起而非销毁,重新挂载瞬时且状态完整。
|
|
62
62
|
- **dispose 后保持静默** —— 不补发零矩形。元素真正消失时,应由调用方先发 ZERO 再 dispose(适配层已替你处理)。
|
|
63
63
|
|
|
64
|
+
## createPlacementAnchor(target, opts) — 显式可见性变体
|
|
65
|
+
|
|
66
|
+
`createViewAnchor` 用零矩形 `{0,0,0,0}` 兼任「收起」信号,可见性是从几何推断的。`createPlacementAnchor` 把可见性提升为显式判别式,发的是 `Placement` 而非 `Bounds`:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const handle = createPlacementAnchor(target, {
|
|
70
|
+
visible: true, // 调用方意图:视图是否该在屏
|
|
71
|
+
publish: (placement) => { ... },
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| 状态 / 选项 | 行为 |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `visible: true` | 发 `measurePlacement(target)`(`{ visible:true, bounds }`),并在每次 `ResizeObserver` / `resize` 触发**同步**重发。 |
|
|
78
|
+
| `visible: false` | 发 `{ visible:false }`(**不是**零矩形),停止观察。 |
|
|
79
|
+
| `guardDisplayNone`(默认 false) | 开启后,测出零面积的 target(display:none / 卸载 / 首帧未稳)发 `{ visible:false }` 而非 `{ visible:true, bounds:0×0 }`,并挂 `IntersectionObserver` 捕捉 `ResizeObserver` 不上报的 display:none 切换。 |
|
|
80
|
+
| `followScroll`(默认 false) | 捕获阶段监听 `window` 的 `scroll`,祖先滚动容器滚动 target 时重测重发。 |
|
|
81
|
+
| `followGeometry`(默认 false) | 按需开窗的 RAF 几何哨兵,捕捉无 DOM 事件上报的祖先 transform / reflow 位移;几何静止几帧后自动关窗,空闲时零成本。可由 `pulse(durationMs?)` 显式开窗。 |
|
|
82
|
+
|
|
83
|
+
去重携带判别式(`samePlacement`),所以可见性翻转绝不会被同值合并掉——即使几何看上去一样。真正 0×0 但在屏的视图(`{ visible:true, bounds:{...,width:0} }`)与隐藏视图(`{ visible:false }`)由此可区分,而零矩形约定下二者会塌缩成同一个值。
|
|
84
|
+
|
|
64
85
|
## useViewAnchor(opts)
|
|
65
86
|
|
|
66
87
|
React 适配层,返回一个挂到占位元素上的 ref 回调。
|
|
@@ -88,9 +109,11 @@ return <div ref={ref} />
|
|
|
88
109
|
|
|
89
110
|
| 文件 | 作用 |
|
|
90
111
|
|---|---|
|
|
91
|
-
| `src/view-anchor.ts` |
|
|
92
|
-
| `src/react.ts` | React 适配层 `useViewAnchor
|
|
93
|
-
| `src/
|
|
112
|
+
| `src/view-anchor.ts` | 正向命令式核心 `createViewAnchor` / `createPlacementAnchor` / `measurePlacement`。无 React、无 Electron。 |
|
|
113
|
+
| `src/react.ts` | React 适配层 `useViewAnchor`(基于 `createViewAnchor`)。 |
|
|
114
|
+
| `src/size-advertiser.ts` | 反向命令式核心 `createSizeAdvertiser`。 |
|
|
115
|
+
| `src/measure-loop.ts` | 反向专用的 RAF 合并 / 去重 / dispose 引擎 `createMeasureLoop`(不导出)。 |
|
|
116
|
+
| `src/types.ts` | `Bounds`、`Placement`、各核心的选项与句柄类型、反向的 `AdvertisedAxis` / `AdvertisedSize`。 |
|
|
94
117
|
| `src/index.ts` | 对外公开面。 |
|
|
95
118
|
|
|
96
|
-
正向运行时依赖只有 `react`(仅适配层)和浏览器 API(`ResizeObserver`、`getBoundingClientRect`、`window` 的 `resize`
|
|
119
|
+
正向运行时依赖只有 `react`(仅适配层)和浏览器 API(`ResizeObserver`、`getBoundingClientRect`、`window` 的 `resize` 监听)。`requestAnimationFrame` 只在反向 `createSizeAdvertiser`(经 `createMeasureLoop`)和 `createPlacementAnchor` 的 opt-in `followGeometry` 哨兵里用;正向的 `createViewAnchor` 同步发布、不走 RAF。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dimina-kit/view-anchor",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.20260618090552",
|
|
4
4
|
"description": "Engine-agnostic primitive that keeps a main-process native view (Electron WebContentsView) aligned to a DOM element's geometry.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dimina",
|