@netless/window-manager 1.0.0-canary.6 → 1.0.0-canary.60

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 (111) hide show
  1. package/README.md +30 -6
  2. package/dist/index.js +13539 -0
  3. package/dist/index.mjs +13536 -0
  4. package/dist/index.umd.js +13534 -46
  5. package/dist/{App → src/App}/AppContext.d.ts +16 -14
  6. package/dist/{App → src/App}/AppPageStateImpl.d.ts +0 -0
  7. package/dist/{App → src/App}/AppProxy.d.ts +30 -11
  8. package/dist/{App → src/App}/MagixEvent/index.d.ts +0 -0
  9. package/dist/src/App/WhiteboardView.d.ts +27 -0
  10. package/dist/{App → src/App}/index.d.ts +1 -0
  11. package/dist/src/App/type.d.ts +21 -0
  12. package/dist/{AppListener.d.ts → src/AppListener.d.ts} +2 -2
  13. package/dist/{AppManager.d.ts → src/AppManager.d.ts} +11 -6
  14. package/dist/{AttributesDelegate.d.ts → src/AttributesDelegate.d.ts} +5 -2
  15. package/dist/{BoxEmitter.d.ts → src/BoxEmitter.d.ts} +0 -0
  16. package/dist/{BoxManager.d.ts → src/BoxManager.d.ts} +12 -6
  17. package/dist/{BuiltinApps.d.ts → src/BuiltinApps.d.ts} +3 -0
  18. package/dist/{Cursor → src/Cursor}/Cursor.d.ts +0 -0
  19. package/dist/{Cursor → src/Cursor}/icons.d.ts +0 -0
  20. package/dist/{Cursor → src/Cursor}/index.d.ts +4 -3
  21. package/dist/{Helper.d.ts → src/Helper.d.ts} +4 -8
  22. package/dist/{InternalEmitter.d.ts → src/InternalEmitter.d.ts} +1 -4
  23. package/dist/{Page → src/Page}/PageController.d.ts +2 -1
  24. package/dist/{Page → src/Page}/index.d.ts +0 -0
  25. package/dist/{PageState.d.ts → src/PageState.d.ts} +0 -0
  26. package/dist/{ReconnectRefresher.d.ts → src/ReconnectRefresher.d.ts} +0 -0
  27. package/dist/{RedoUndo.d.ts → src/RedoUndo.d.ts} +0 -0
  28. package/dist/{Register → src/Register}/index.d.ts +4 -2
  29. package/dist/{Register → src/Register}/loader.d.ts +1 -1
  30. package/dist/src/Register/storage.d.ts +11 -0
  31. package/dist/{Utils → src/Utils}/AppCreateQueue.d.ts +0 -0
  32. package/dist/{Utils → src/Utils}/Common.d.ts +0 -0
  33. package/dist/{Utils → src/Utils}/Reactive.d.ts +1 -1
  34. package/dist/{Utils → src/Utils}/RoomHacker.d.ts +0 -0
  35. package/dist/{Utils → src/Utils}/error.d.ts +1 -1
  36. package/dist/{Utils → src/Utils}/log.d.ts +0 -0
  37. package/dist/src/View/CameraSynchronizer.d.ts +20 -0
  38. package/dist/{View → src/View}/MainView.d.ts +18 -7
  39. package/dist/src/View/ScrollMode.d.ts +32 -0
  40. package/dist/{View → src/View}/ViewManager.d.ts +0 -0
  41. package/dist/src/View/ViewSync.d.ts +32 -0
  42. package/dist/{callback.d.ts → src/callback.d.ts} +13 -1
  43. package/dist/{constants.d.ts → src/constants.d.ts} +12 -5
  44. package/dist/src/image.d.ts +19 -0
  45. package/dist/{index.d.ts → src/index.d.ts} +66 -17
  46. package/dist/src/shim.d.ts +11 -0
  47. package/dist/src/storage.d.ts +7 -0
  48. package/dist/{typings.d.ts → src/typings.d.ts} +18 -8
  49. package/dist/style.css +810 -1
  50. package/docs/api.md +10 -0
  51. package/docs/app-context.md +155 -27
  52. package/docs/mirgrate-to-1.0.md +68 -0
  53. package/package.json +27 -22
  54. package/playwright.config.ts +29 -0
  55. package/pnpm-lock.yaml +3141 -4483
  56. package/src/App/AppContext.ts +81 -46
  57. package/src/App/AppProxy.ts +249 -139
  58. package/src/App/WhiteboardView.ts +38 -14
  59. package/src/App/index.ts +1 -0
  60. package/src/App/type.ts +22 -0
  61. package/src/AppListener.ts +21 -21
  62. package/src/AppManager.ts +84 -43
  63. package/src/AttributesDelegate.ts +6 -3
  64. package/src/BoxManager.ts +76 -38
  65. package/src/BuiltinApps.ts +9 -8
  66. package/src/Cursor/Cursor.svelte +6 -2
  67. package/src/Cursor/Cursor.ts +16 -5
  68. package/src/Cursor/icons.ts +6 -0
  69. package/src/Cursor/index.ts +13 -10
  70. package/src/Helper.ts +25 -7
  71. package/src/InternalEmitter.ts +1 -4
  72. package/src/Page/PageController.ts +2 -1
  73. package/src/PageState.ts +1 -1
  74. package/src/ReconnectRefresher.ts +6 -2
  75. package/src/Register/index.ts +36 -14
  76. package/src/Register/loader.ts +20 -9
  77. package/src/Register/storage.ts +26 -5
  78. package/src/Utils/Common.ts +3 -0
  79. package/src/Utils/Reactive.ts +29 -27
  80. package/src/Utils/RoomHacker.ts +3 -0
  81. package/src/Utils/error.ts +2 -2
  82. package/src/View/CameraSynchronizer.ts +52 -37
  83. package/src/View/MainView.ts +118 -76
  84. package/src/View/ScrollMode.ts +239 -0
  85. package/src/View/ViewSync.ts +139 -6
  86. package/src/callback.ts +9 -1
  87. package/src/constants.ts +11 -3
  88. package/src/image/pencil-eraser-1.svg +3 -0
  89. package/src/image/pencil-eraser-2.svg +3 -0
  90. package/src/image/pencil-eraser-3.svg +3 -0
  91. package/src/index.ts +202 -58
  92. package/src/storage.ts +15 -0
  93. package/src/style.css +18 -47
  94. package/src/typings.ts +21 -7
  95. package/vite.config.js +12 -7
  96. package/dist/App/AppViewSync.d.ts +0 -11
  97. package/dist/App/Storage/StorageEvent.d.ts +0 -8
  98. package/dist/App/Storage/index.d.ts +0 -39
  99. package/dist/App/Storage/typings.d.ts +0 -22
  100. package/dist/App/Storage/utils.d.ts +0 -5
  101. package/dist/App/WhiteboardView.d.ts +0 -21
  102. package/dist/Register/storage.d.ts +0 -8
  103. package/dist/View/CameraSynchronizer.d.ts +0 -17
  104. package/dist/View/ViewSync.d.ts +0 -7
  105. package/dist/index.cjs.js +0 -46
  106. package/dist/index.es.js +0 -16159
  107. package/src/App/AppViewSync.ts +0 -68
  108. package/src/App/Storage/StorageEvent.ts +0 -21
  109. package/src/App/Storage/index.ts +0 -295
  110. package/src/App/Storage/typings.ts +0 -23
  111. package/src/App/Storage/utils.ts +0 -17
@@ -1,14 +1,16 @@
1
1
  import { callbacks } from "../callback";
2
- import { CameraSynchronizer } from "./CameraSynchronizer";
3
2
  import { createView } from "./ViewManager";
4
3
  import { debounce, get, isEqual } from "lodash";
5
4
  import { emitter } from "../InternalEmitter";
6
5
  import { Events } from "../constants";
7
6
  import { Fields } from "../AttributesDelegate";
8
- import { reaction } from "white-web-sdk";
9
- import { releaseView, setViewFocusScenePath } from "../Utils/Common";
7
+ import { AnimationMode, reaction, toJS } from "white-web-sdk";
8
+ import { releaseView, setScenePath, setViewFocusScenePath } from "../Utils/Common";
10
9
  import { SideEffectManager } from "side-effect-manager";
11
- import type { Camera, Size, View } from "white-web-sdk";
10
+ import { Val } from "value-enhancer";
11
+ import { ViewSync } from "./ViewSync";
12
+ import type { ICamera, ISize } from "../AttributesDelegate";
13
+ import type { Size, View } from "white-web-sdk";
12
14
  import type { AppManager } from "../AppManager";
13
15
 
14
16
  export class MainViewProxy {
@@ -16,56 +18,90 @@ export class MainViewProxy {
16
18
  private mainViewIsAddListener = false;
17
19
  private mainView: View;
18
20
  private store = this.manager.store;
19
- private synchronizer: CameraSynchronizer;
20
21
 
21
22
  private sideEffectManager = new SideEffectManager();
22
23
 
24
+ public camera$ = new Val<ICamera | undefined>(undefined);
25
+ public size$ = new Val<ISize | undefined>(undefined);
26
+ public view$ = new Val<View | undefined>(undefined);
27
+
28
+ public viewSync?: ViewSync;
29
+
23
30
  constructor(private manager: AppManager) {
24
- this.synchronizer = new CameraSynchronizer(camera =>
25
- this.store.setMainViewCamera({ ...camera, id: this.manager.uid })
26
- );
27
31
  this.mainView = this.createMainView();
28
- this.moveCameraSizeByAttributes();
29
32
  emitter.once("mainViewMounted").then(() => {
30
33
  this.addMainViewListener();
31
34
  this.start();
32
35
  this.ensureCameraAndSize();
33
36
  this.startListenWritableChange();
34
37
  });
35
- this.sideEffectManager.add(() => {
36
- return emitter.on("containerSizeRatioUpdate", this.onUpdateContainerSizeRatio);
37
- });
38
- this.sideEffectManager.add(() => {
39
- return emitter.on("startReconnect", () => {
38
+ this.sideEffectManager.add(() => [
39
+ emitter.on("startReconnect", () => {
40
40
  releaseView(this.mainView);
41
- });
41
+ }),
42
+ ]);
43
+ this.createViewSync();
44
+ this.sideEffectManager.add(() => emitter.on("focusedChange", ({ focused }) => {
45
+ if (focused === undefined) {
46
+ const scenePath = this.store.getMainViewScenePath();
47
+ if (scenePath) {
48
+ setScenePath(this.manager.room, scenePath);
49
+ }
50
+ }
51
+ }));
52
+ this.camera$.reaction(camera => {
53
+ if (camera) {
54
+ callbacks.emit("baseCameraChange", camera);
55
+ }
42
56
  });
43
- const rect = this.manager.boxManager?.stageRect;
44
- if (rect) {
45
- this.synchronizer.setRect(rect);
46
- }
47
- this.sideEffectManager.add(() => {
48
- return emitter.on("playgroundSizeChange", rect => {
49
- this.synchronizer.setRect(rect);
50
- // this.synchronizer.onLocalSizeUpdate(rect);
51
- });
57
+ this.size$.reaction(size => {
58
+ if (size) {
59
+ callbacks.emit("baseSizeChange", size);
60
+ }
52
61
  });
53
62
  }
54
63
 
64
+ public createViewSync = () => {
65
+ if (this.manager.boxManager && !this.viewSync) {
66
+ this.viewSync = new ViewSync({
67
+ uid: this.manager.uid,
68
+ view$: this.view$,
69
+ camera$: this.camera$,
70
+ size$: this.size$,
71
+ stageRect$: this.manager.boxManager?.stageRect$,
72
+ viewMode$: this.manager.windowManger.viewMode$,
73
+ storeCamera: this.storeCamera,
74
+ storeSize: this.storeSize,
75
+ });
76
+ }
77
+ };
78
+
55
79
  private startListenWritableChange = () => {
56
- this.sideEffectManager.add(() => {
57
- return emitter.on("writableChange", isWritable => {
80
+ this.sideEffectManager.add(() =>
81
+ emitter.on("writableChange", isWritable => {
58
82
  if (isWritable) {
59
83
  this.ensureCameraAndSize();
60
84
  }
61
- });
62
- });
85
+ })
86
+ );
63
87
  };
64
88
 
65
89
  public ensureCameraAndSize() {
66
90
  if (!this.mainViewCamera || !this.mainViewSize) {
67
91
  this.manager.dispatchInternalEvent(Events.InitMainViewCamera);
68
- this.setCameraAndSize();
92
+ this.storeCamera({
93
+ id: this.manager.uid,
94
+ ...this.view.camera
95
+ });
96
+ // FIX 没有 mainViewSize 需要初始化一个 baseSize
97
+ const stageRect = this.manager.boxManager?.stageRect;
98
+ if (stageRect && !this.mainViewSize) {
99
+ this.storeSize({
100
+ id: this.manager.uid,
101
+ width: stageRect.width,
102
+ height: stageRect.height
103
+ });
104
+ }
69
105
  }
70
106
  }
71
107
 
@@ -81,55 +117,74 @@ export class MainViewProxy {
81
117
  return get(this.view, ["didRelease"]);
82
118
  }
83
119
 
84
- private moveCameraSizeByAttributes() {
85
- this.synchronizer.onRemoteUpdate(this.mainViewCamera, this.mainViewSize);
86
- }
87
-
88
120
  public start() {
89
121
  if (this.started) return;
90
- this.sizeChangeHandler(this.mainViewSize);
122
+ this.removeCameraListener();
91
123
  this.addCameraListener();
92
124
  this.addCameraReaction();
93
125
  this.started = true;
94
126
  }
95
127
 
96
128
  public addCameraReaction = () => {
97
- this.manager.refresher?.add(Fields.MainViewCamera, this.cameraReaction);
129
+ this.manager.refresher.add(Fields.MainViewCamera, this.cameraReaction);
130
+ this.manager.refresher.add(Fields.MainViewSize, this.sizeReaction);
98
131
  };
99
132
 
100
- public setCameraAndSize(): void {
101
- const stageSize = this.getStageSize();
102
- if (stageSize) {
103
- const camera = { ...this.mainView.camera, id: this.manager.uid };
104
- const size = { ...stageSize, id: this.manager.uid };
105
- this.store.setMainViewCameraAndSize(camera, size);
133
+ public storeCurrentCamera = () => {
134
+ const iCamera = this.view.camera;
135
+ this.storeCamera({
136
+ id: this.manager.uid,
137
+ ...iCamera
138
+ });
139
+ }
140
+
141
+ public storeCurrentSize = () => {
142
+ const rect = this.manager.boxManager?.stageRect;
143
+ if (rect) {
144
+ this.storeSize({
145
+ id: this.manager.uid,
146
+ width: rect.width,
147
+ height: rect.height
148
+ });
106
149
  }
107
150
  }
108
151
 
152
+ public storeCamera = (camera: ICamera) => {
153
+ this.store.setMainViewCamera(camera);
154
+ };
155
+
156
+ public storeSize = (size: ISize) => {
157
+ this.store.setMainViewSize(size);
158
+ };
159
+
109
160
  private cameraReaction = () => {
110
161
  return reaction(
111
162
  () => this.mainViewCamera,
112
163
  camera => {
113
- if (camera && camera.id !== this.manager.uid) {
114
- this.synchronizer.onRemoteUpdate(camera, this.mainViewSize);
164
+ if (camera) {
165
+ const rawCamera = toJS(camera);
166
+ if (!isEqual(rawCamera, this.camera$.value)) {
167
+ this.camera$.setValue(rawCamera);
168
+ }
115
169
  }
116
170
  },
117
171
  { fireImmediately: true }
118
172
  );
119
173
  };
120
174
 
121
- public sizeChangeHandler = debounce((size: Size) => {
122
- if (size) {
123
- // this.synchronizer.onLocalSizeUpdate(size);
124
- }
125
- }, 30);
126
-
127
- public onUpdateContainerSizeRatio = () => {
128
- const size = this.store.getMainViewSize();
129
- this.sizeChangeHandler(size);
130
- if (size.id === this.manager.uid) {
131
- this.setCameraAndSize();
132
- }
175
+ private sizeReaction = () => {
176
+ return reaction(
177
+ () => this.mainViewSize,
178
+ size => {
179
+ if (size) {
180
+ const rawSize = toJS(size);
181
+ if (!isEqual(rawSize, this.size$.value)) {
182
+ this.size$.setValue(rawSize);
183
+ }
184
+ }
185
+ },
186
+ { fireImmediately: true }
187
+ );
133
188
  };
134
189
 
135
190
  public get view(): View {
@@ -146,7 +201,7 @@ export class MainViewProxy {
146
201
  if (mainViewScenePath) {
147
202
  setViewFocusScenePath(mainView, mainViewScenePath);
148
203
  }
149
- this.synchronizer.setView(mainView);
204
+ this.view$.setValue(mainView);
150
205
  return mainView;
151
206
  }
152
207
 
@@ -168,31 +223,18 @@ export class MainViewProxy {
168
223
  public rebind(): void {
169
224
  const divElement = this.mainView.divElement;
170
225
  const disableCameraTransform = this.mainView.disableCameraTransform;
226
+ const camera = { ...this.mainView.camera };
171
227
  this.stop();
172
228
  releaseView(this.mainView);
173
229
  this.removeMainViewListener();
174
230
  this.mainView = this.createMainView();
175
231
  this.mainView.disableCameraTransform = disableCameraTransform;
176
232
  this.mainView.divElement = divElement;
233
+ this.mainView.moveCamera({ ...camera, animationMode: AnimationMode.Immediately });
177
234
  this.addMainViewListener();
178
235
  this.start();
179
236
  }
180
237
 
181
- private onCameraUpdatedByDevice = (camera: Camera) => {
182
- this.synchronizer.onLocalCameraUpdate(camera);
183
- const size = this.getStageSize();
184
- if (size && !isEqual(size, this.mainViewSize)) {
185
- this.setMainViewSize(size);
186
- }
187
- };
188
-
189
- private getStageSize(): Size | undefined {
190
- const stage = this.manager.boxManager?.stageRect;
191
- if (stage) {
192
- return { width: stage.width, height: stage.height };
193
- }
194
- }
195
-
196
238
  public addMainViewListener(): void {
197
239
  if (this.mainViewIsAddListener) return;
198
240
  if (this.view.divElement) {
@@ -225,13 +267,11 @@ export class MainViewProxy {
225
267
  }, 50);
226
268
 
227
269
  private addCameraListener() {
228
- this.view.callbacks.on("onCameraUpdatedByDevice", this.onCameraUpdatedByDevice);
229
270
  this.view.callbacks.on("onCameraUpdated", this.onCameraOrSizeUpdated);
230
271
  this.view.callbacks.on("onSizeUpdated", this.onCameraOrSizeUpdated);
231
272
  }
232
273
 
233
274
  private removeCameraListener() {
234
- this.view.callbacks.off("onCameraUpdatedByDevice", this.onCameraUpdatedByDevice);
235
275
  this.view.callbacks.off("onCameraUpdated", this.onCameraOrSizeUpdated);
236
276
  this.view.callbacks.off("onSizeUpdated", this.onCameraOrSizeUpdated);
237
277
  }
@@ -241,13 +281,15 @@ export class MainViewProxy {
241
281
  };
242
282
 
243
283
  public stop() {
244
- this.removeCameraListener();
245
- this.manager.refresher?.remove(Fields.MainViewCamera);
246
- this.manager.refresher?.remove(Fields.MainViewSize);
284
+ this.manager.refresher.remove(Fields.MainViewCamera);
285
+ this.manager.refresher.remove(Fields.MainViewSize);
247
286
  this.started = false;
248
287
  }
249
288
 
250
289
  public destroy() {
290
+ this.camera$.destroy();
291
+ this.size$.destroy();
292
+ this.view$.destroy();
251
293
  this.removeMainViewListener();
252
294
  this.stop();
253
295
  this.sideEffectManager.flushAll();
@@ -0,0 +1,239 @@
1
+ import { AnimationMode } from "white-web-sdk";
2
+ import { callbacks } from "../callback";
3
+ import { combine, derive, Val } from "value-enhancer";
4
+ import { createScrollStorage } from "../storage";
5
+ import { SCROLL_MODE_BASE_HEIGHT, SCROLL_MODE_BASE_WIDTH } from "../constants";
6
+ import { SideEffectManager } from "side-effect-manager";
7
+ import { round } from "lodash";
8
+ import type { ReadonlyVal } from "value-enhancer";
9
+ import type { AppManager } from "../AppManager";
10
+ import type { ScrollStorage } from "../storage";
11
+ import type { Camera, Size, View } from "white-web-sdk";
12
+
13
+ function clamp(x: number, min: number, max: number): number {
14
+ return x < min ? min : x > max ? max : x;
15
+ }
16
+
17
+ export type ScrollState = {
18
+ scrollTop: number;
19
+ page: number;
20
+ maxScrollPage: number;
21
+ };
22
+
23
+ export class ScrollMode {
24
+ public readonly sideEffect = new SideEffectManager();
25
+
26
+ private readonly _root$: Val<HTMLElement | null>;
27
+ private readonly _whiteboard$: ReadonlyVal<HTMLElement | null>;
28
+ private readonly _scrollTop$: Val<number>;
29
+ public readonly _page$: ReadonlyVal<number>;
30
+ private readonly _scale$: ReadonlyVal<number>;
31
+ private readonly _size$: Val<Size>;
32
+ private readonly _mainView$: Val<View>;
33
+
34
+ private baseWidth = SCROLL_MODE_BASE_WIDTH;
35
+ private baseHeight = SCROLL_MODE_BASE_HEIGHT;
36
+
37
+ public scrollStorage: ScrollStorage;
38
+ public readonly scrollState$: ReadonlyVal<ScrollState>;
39
+
40
+ public setRoot(root: HTMLElement): void {
41
+ this._root$.setValue(root);
42
+ }
43
+
44
+ constructor(private manager: AppManager) {
45
+ this._root$ = new Val<HTMLElement | null>(null);
46
+ this._mainView$ = new Val<View>(this.manager.mainView);
47
+ // 滚动模式下确保 disableCameraTransform 为 false, 否则触摸屏无法滚动
48
+ this._mainView$.value.disableCameraTransform = false;
49
+
50
+ if (manager.scrollBaseSize$?.value) {
51
+ this.baseWidth = manager.scrollBaseSize$.value.width;
52
+ this.baseHeight = manager.scrollBaseSize$.value.height;
53
+ }
54
+
55
+ this.scrollStorage = createScrollStorage(manager);
56
+ const scrollTop$ = new Val(this.scrollStorage.state.scrollTop);
57
+ this._scrollTop$ = scrollTop$;
58
+
59
+ this.sideEffect.push(
60
+ this.scrollStorage.on("stateChanged", () => {
61
+ this._scrollTop$.setValue(this.scrollStorage.state.scrollTop);
62
+ })
63
+ );
64
+
65
+ const size$ = new Val<Size>(
66
+ { width: 0, height: 0 },
67
+ { compare: (a, b) => a.width === b.width && a.height === b.height }
68
+ );
69
+ this._size$ = size$;
70
+ this.sideEffect.add(() => {
71
+ const onSizeUpdated = size$.setValue.bind(size$);
72
+ onSizeUpdated(this._mainView$.value.size);
73
+ this._mainView$.value.callbacks.on("onSizeUpdated", onSizeUpdated);
74
+ return () => this._mainView$.value.callbacks.off("onSizeUpdated", onSizeUpdated);
75
+ });
76
+
77
+ this.sideEffect.add(() => {
78
+ const onCameraUpdated = (camera: Camera): void => {
79
+ const halfWbHeight = size$.value.height / 2 / scale$.value;
80
+ const scrollTop = camera.centerY;
81
+ this.scrollStorage.setState({
82
+ scrollTop: clamp(scrollTop, halfWbHeight, this.baseHeight - halfWbHeight),
83
+ });
84
+ callbacks.emit("userScroll");
85
+ };
86
+ this._mainView$.value.callbacks.on("onCameraUpdatedByDevice", onCameraUpdated);
87
+ return () =>
88
+ this._mainView$.value.callbacks.off("onCameraUpdatedByDevice", onCameraUpdated);
89
+ });
90
+
91
+ const scale$ = derive(size$, size => size.width / this.baseWidth);
92
+ this._scale$ = scale$;
93
+
94
+ const page$ = new Val(0);
95
+ this.sideEffect.push(
96
+ combine([scrollTop$, size$, scale$]).subscribe(([scrollTop, size, scale]) => {
97
+ if (scale > 0) {
98
+ const wbHeight = size.height / scale;
99
+ page$.setValue(Math.max(scrollTop / wbHeight - 0.5, 0));
100
+ }
101
+ })
102
+ );
103
+ this._page$ = page$;
104
+
105
+ // 5. bound$ = { contentMode: () => scale$, centerX: W / 2, centerY: H / 2, width: W, height: H }
106
+ this.sideEffect.push(
107
+ combine([scrollTop$, scale$]).subscribe(([scrollTop, scale]) => {
108
+ this.updateBound(scrollTop, size$.value, scale);
109
+ })
110
+ );
111
+
112
+ this.sideEffect.push(
113
+ size$.reaction(() => {
114
+ this.updateScroll(scrollTop$.value);
115
+ })
116
+ );
117
+
118
+ const whiteboard$ = derive(this._root$, this.getWhiteboardElement);
119
+ this._whiteboard$ = whiteboard$;
120
+ this.sideEffect.push(
121
+ whiteboard$.reaction(el => {
122
+ if (el?.parentElement) {
123
+ this.sideEffect.addEventListener(
124
+ el.parentElement,
125
+ "wheel",
126
+ this.onWheel,
127
+ { capture: true, passive: false },
128
+ "wheel"
129
+ );
130
+ }
131
+ })
132
+ );
133
+
134
+ const maxScrollPage$ = combine([this._size$, this._scale$], ([size, scale]) => {
135
+ const halfWbHeight = size.height / 2 / scale;
136
+ return (this.baseHeight - halfWbHeight) / halfWbHeight / 2 - 0.51;
137
+ });
138
+
139
+ this.scrollState$ = combine(
140
+ [this._scrollTop$, this._page$, maxScrollPage$],
141
+ ([scrollTop, page, maxScrollPage]) => {
142
+ return {
143
+ scrollTop: round(scrollTop, 2),
144
+ page: round(page, 2),
145
+ maxScrollPage: round(maxScrollPage, 2),
146
+ };
147
+ }
148
+ );
149
+
150
+ this.updateScroll(scrollTop$.value);
151
+ this.sideEffect.push(
152
+ this.scrollState$.subscribe(state => callbacks.emit("scrollStateChange", state))
153
+ );
154
+
155
+ this.sideEffect.push(
156
+ combine([this._size$, this._scale$]).subscribe(([size, scale]) => {
157
+ if (size.height > 0 && scale > 0) {
158
+ this.initScroll();
159
+ this.sideEffect.flush("initScroll");
160
+ }
161
+ }),
162
+ "initScroll"
163
+ );
164
+ }
165
+
166
+ private initScroll = (): void => {
167
+ const halfWbHeight = this._size$.value.height / 2 / this._scale$.value;
168
+ const scrollTop = this._scrollTop$.value;
169
+ // HACK: set a different value (+0.01) to trigger all effects above
170
+ this._scrollTop$.setValue(
171
+ clamp(scrollTop, halfWbHeight, this.baseHeight - halfWbHeight) - 0.01
172
+ );
173
+ };
174
+
175
+ private updateScroll(scrollTop: number): void {
176
+ this._mainView$.value.moveCamera({
177
+ centerY: scrollTop,
178
+ animationMode: AnimationMode.Immediately,
179
+ });
180
+ }
181
+
182
+ private updateBound(scrollTop: number, { height }: Size, scale: number): void {
183
+ if (scale > 0) {
184
+ this._mainView$.value.moveCameraToContain({
185
+ originX: 0,
186
+ originY: scrollTop - height / scale / 2,
187
+ width: this.baseWidth,
188
+ height: height / scale,
189
+ animationMode: AnimationMode.Immediately,
190
+ });
191
+
192
+ this._mainView$.value.setCameraBound({
193
+ damping: 1,
194
+ maxContentMode: () => scale,
195
+ minContentMode: () => scale,
196
+ centerX: this.baseWidth / 2,
197
+ centerY: this.baseHeight / 2,
198
+ width: this.baseWidth,
199
+ height: this.baseHeight,
200
+ });
201
+ }
202
+ }
203
+
204
+ public dispose(): void {
205
+ this.sideEffect.flushAll();
206
+ this.scrollStorage.disconnect();
207
+ this._root$.destroy();
208
+ this._scale$.destroy();
209
+ this._scrollTop$.destroy();
210
+ this._whiteboard$.destroy();
211
+ this.scrollState$.destroy();
212
+ this._page$.destroy();
213
+ this._size$.destroy();
214
+ this._mainView$.destroy();
215
+ }
216
+
217
+ private getWhiteboardElement = (root: HTMLElement | null): HTMLElement | null => {
218
+ const className = ".netless-window-manager-main-view";
219
+ return root && root.querySelector(className);
220
+ };
221
+
222
+ private onWheel = (ev: WheelEvent): void => {
223
+ const target = ev.target as HTMLElement | null;
224
+ if (this.manager.canOperate && this._whiteboard$.value?.contains(target)) {
225
+ ev.preventDefault();
226
+ ev.stopPropagation();
227
+ const dy = ev.deltaY || 0;
228
+ const { width } = this._size$.value;
229
+ if (dy && width > 0) {
230
+ const halfWbHeight = this._size$.value.height / 2 / this._scale$.value;
231
+ const scrollTop = this._scrollTop$.value + dy / this._scale$.value;
232
+ this.scrollStorage.setState({
233
+ scrollTop: clamp(scrollTop, halfWbHeight, this.baseHeight - halfWbHeight),
234
+ });
235
+ callbacks.emit("userScroll");
236
+ }
237
+ }
238
+ };
239
+ }