@netless/window-manager 1.0.0-canary.7 → 1.0.0-canary.71

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 (112) hide show
  1. package/README.md +30 -6
  2. package/dist/index.js +13625 -0
  3. package/dist/index.mjs +13622 -0
  4. package/dist/index.umd.js +13620 -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} +12 -7
  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 +3 -1
  24. package/dist/{Page → src/Page}/index.d.ts +0 -0
  25. package/dist/{PageState.d.ts → src/PageState.d.ts} +1 -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 +4 -1
  36. package/dist/{Utils → src/Utils}/log.d.ts +0 -0
  37. package/dist/src/View/CameraSynchronizer.d.ts +21 -0
  38. package/dist/{View → src/View}/MainView.d.ts +25 -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} +12 -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} +63 -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} +21 -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/src/App/AppContext.ts +81 -46
  56. package/src/App/AppPageStateImpl.ts +3 -0
  57. package/src/App/AppProxy.ts +249 -141
  58. package/src/App/WhiteboardView.ts +37 -14
  59. package/src/App/index.ts +1 -0
  60. package/src/App/type.ts +22 -0
  61. package/src/AppListener.ts +27 -21
  62. package/src/AppManager.ts +96 -50
  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 +15 -4
  68. package/src/Cursor/icons.ts +6 -0
  69. package/src/Cursor/index.ts +16 -11
  70. package/src/Helper.ts +25 -7
  71. package/src/InternalEmitter.ts +1 -4
  72. package/src/Page/PageController.ts +3 -1
  73. package/src/PageState.ts +8 -1
  74. package/src/ReconnectRefresher.ts +7 -3
  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 +6 -2
  82. package/src/View/CameraSynchronizer.ts +55 -36
  83. package/src/View/MainView.ts +163 -77
  84. package/src/View/ScrollMode.ts +240 -0
  85. package/src/View/ViewSync.ts +138 -6
  86. package/src/callback.ts +8 -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 +197 -60
  92. package/src/storage.ts +15 -0
  93. package/src/style.css +18 -47
  94. package/src/typings.ts +24 -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 -16161
  107. package/pnpm-lock.yaml +0 -6302
  108. package/src/App/AppViewSync.ts +0 -68
  109. package/src/App/Storage/StorageEvent.ts +0 -21
  110. package/src/App/Storage/index.ts +0 -295
  111. package/src/App/Storage/typings.ts +0 -23
  112. package/src/App/Storage/utils.ts +0 -17
@@ -1,74 +1,146 @@
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";
15
+ import type { MoveCameraParams } from "../typings";
13
16
 
14
17
  export class MainViewProxy {
15
18
  private started = false;
16
19
  private mainViewIsAddListener = false;
17
20
  private mainView: View;
18
21
  private store = this.manager.store;
19
- private synchronizer: CameraSynchronizer;
20
22
 
21
23
  private sideEffectManager = new SideEffectManager();
22
24
 
25
+ public camera$ = new Val<ICamera | undefined>(undefined);
26
+ public size$ = new Val<ISize | undefined>(undefined);
27
+ public view$ = new Val<View | undefined>(undefined);
28
+ private cameraUpdatePromise?: Promise<boolean>;
29
+
30
+ public viewSync?: ViewSync;
31
+
23
32
  constructor(private manager: AppManager) {
24
- this.synchronizer = new CameraSynchronizer(camera =>
25
- this.store.setMainViewCamera({ ...camera, id: this.manager.uid })
26
- );
27
33
  this.mainView = this.createMainView();
28
- this.moveCameraSizeByAttributes();
29
34
  emitter.once("mainViewMounted").then(() => {
30
35
  this.addMainViewListener();
31
36
  this.start();
32
37
  this.ensureCameraAndSize();
33
38
  this.startListenWritableChange();
34
39
  });
35
- this.sideEffectManager.add(() => {
36
- return emitter.on("containerSizeRatioUpdate", this.onUpdateContainerSizeRatio);
37
- });
38
- this.sideEffectManager.add(() => {
39
- return emitter.on("startReconnect", () => {
40
+ this.sideEffectManager.add(() => [
41
+ emitter.on("startReconnect", () => {
40
42
  releaseView(this.mainView);
41
- });
42
- });
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
- });
43
+ }),
44
+ ]);
45
+ this.createViewSync();
46
+ this.sideEffectManager.add(() => emitter.on("focusedChange", ({ focused }) => {
47
+ if (focused === undefined) {
48
+ const scenePath = this.store.getMainViewScenePath();
49
+ if (scenePath) {
50
+ setScenePath(this.manager.room, scenePath);
51
+ }
52
+ }
53
+ }));
54
+ this.size$.reaction(size => {
55
+ if (size) {
56
+ callbacks.emit("baseSizeChange", size);
57
+ }
52
58
  });
53
59
  }
54
60
 
61
+ public createViewSync = () => {
62
+ // 滚动模式下,不需要同步
63
+ if (this.manager.windowManger.viewMode$.value === 'scroll') return
64
+ if (this.manager.boxManager && !this.viewSync) {
65
+ this.viewSync = new ViewSync({
66
+ uid: this.manager.uid,
67
+ view$: this.view$,
68
+ camera$: this.camera$,
69
+ size$: this.size$,
70
+ stageRect$: this.manager.boxManager?.stageRect$,
71
+ viewMode$: this.manager.windowManger.viewMode$,
72
+ storeCamera: this.storeCamera,
73
+ storeSize: this.storeSize,
74
+ });
75
+ }
76
+ };
77
+
55
78
  private startListenWritableChange = () => {
56
- this.sideEffectManager.add(() => {
57
- return emitter.on("writableChange", isWritable => {
79
+ this.sideEffectManager.add(() =>
80
+ emitter.on("writableChange", isWritable => {
58
81
  if (isWritable) {
59
82
  this.ensureCameraAndSize();
60
83
  }
61
- });
62
- });
84
+ })
85
+ );
63
86
  };
64
87
 
65
88
  public ensureCameraAndSize() {
66
89
  if (!this.mainViewCamera || !this.mainViewSize) {
67
90
  this.manager.dispatchInternalEvent(Events.InitMainViewCamera);
68
- this.setCameraAndSize();
91
+ this.storeCamera({
92
+ id: this.manager.uid,
93
+ ...this.view.camera
94
+ });
95
+ // FIX 没有 mainViewSize 需要初始化一个 baseSize
96
+ const stageRect = this.manager.boxManager?.stageRect;
97
+ if (stageRect && !this.mainViewSize) {
98
+ this.storeSize({
99
+ id: this.manager.uid,
100
+ width: stageRect.width,
101
+ height: stageRect.height
102
+ });
103
+ }
69
104
  }
70
105
  }
71
106
 
107
+ public moveCamera = (camera: MoveCameraParams) => {
108
+ this.debouncedStoreCamera();
109
+ this.moveCameraToPromise(camera);
110
+ };
111
+
112
+ public moveCameraToPromise = (camera: MoveCameraParams) => {
113
+ const promise = new Promise<boolean>((resolve) => {
114
+ const cameraListener = debounce(() => {
115
+ this.mainView.callbacks.off("onCameraUpdated", cameraListener);
116
+ this.cameraUpdatePromise = undefined;
117
+ resolve(true);
118
+ }, 50);
119
+ this.mainView.callbacks.on("onCameraUpdated", cameraListener);
120
+ this.mainView.moveCamera(camera);
121
+ });
122
+ this.cameraUpdatePromise = promise;
123
+ return promise;
124
+ }
125
+
126
+ private debouncedStoreCamera = () => {
127
+ this.storeCurrentSize();
128
+ const cameraListener = debounce(() => {
129
+ this.saveToCamera$();
130
+ this.storeCurrentCameraSize();
131
+ this.mainView.callbacks.off("onCameraUpdated", cameraListener);
132
+ }, 50);
133
+ this.mainView.callbacks.on("onCameraUpdated", cameraListener);
134
+ }
135
+
136
+ private storeCurrentCameraSize = debounce(async () => {
137
+ if (this.cameraUpdatePromise) {
138
+ await this.cameraUpdatePromise;
139
+ }
140
+ this.storeCurrentCamera();
141
+ this.storeCurrentSize();
142
+ }, 500);
143
+
72
144
  private get mainViewCamera() {
73
145
  return this.store.getMainViewCamera();
74
146
  }
@@ -81,55 +153,82 @@ export class MainViewProxy {
81
153
  return get(this.view, ["didRelease"]);
82
154
  }
83
155
 
84
- private moveCameraSizeByAttributes() {
85
- this.synchronizer.onRemoteUpdate(this.mainViewCamera, this.mainViewSize);
86
- }
87
-
88
156
  public start() {
89
157
  if (this.started) return;
90
- this.sizeChangeHandler(this.mainViewSize);
158
+ this.removeCameraListener();
91
159
  this.addCameraListener();
92
160
  this.addCameraReaction();
93
161
  this.started = true;
94
162
  }
95
163
 
96
164
  public addCameraReaction = () => {
97
- this.manager.refresher?.add(Fields.MainViewCamera, this.cameraReaction);
165
+ this.manager.refresher.add(Fields.MainViewCamera, this.cameraReaction);
166
+ this.manager.refresher.add(Fields.MainViewSize, this.sizeReaction);
98
167
  };
99
168
 
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);
169
+ public saveToCamera$ = () => {
170
+ const camera = { ...this.view.camera, id: this.manager.uid };
171
+ this.camera$.setValue(camera, true);
172
+ }
173
+
174
+ public storeCurrentCamera = () => {
175
+ const iCamera = this.view.camera;
176
+ this.storeCamera({
177
+ id: this.manager.uid,
178
+ ...iCamera
179
+ });
180
+ }
181
+
182
+ public storeCurrentSize = () => {
183
+ const rect = this.manager.boxManager?.stageRect;
184
+ if (rect) {
185
+ const size = {
186
+ id: this.manager.uid,
187
+ width: rect.width,
188
+ height: rect.height
189
+ }
190
+ if (!isEqual(size, this.mainViewSize)) {
191
+ this.storeSize(size);
192
+ }
106
193
  }
107
194
  }
108
195
 
196
+ public storeCamera = (camera: ICamera) => {
197
+ this.store.setMainViewCamera(camera);
198
+ };
199
+
200
+ public storeSize = (size: ISize) => {
201
+ this.store.setMainViewSize(size);
202
+ };
203
+
109
204
  private cameraReaction = () => {
110
205
  return reaction(
111
206
  () => this.mainViewCamera,
112
207
  camera => {
113
- if (camera && camera.id !== this.manager.uid) {
114
- this.synchronizer.onRemoteUpdate(camera, this.mainViewSize);
208
+ if (camera) {
209
+ const rawCamera = toJS(camera);
210
+ if (!isEqual(rawCamera, this.camera$.value)) {
211
+ this.camera$.setValue(rawCamera);
212
+ }
115
213
  }
116
214
  },
117
215
  { fireImmediately: true }
118
216
  );
119
217
  };
120
218
 
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
- }
219
+ private sizeReaction = () => {
220
+ return reaction(
221
+ () => this.mainViewSize,
222
+ size => {
223
+ if (size) {
224
+ const rawSize = toJS(size);
225
+ if (!isEqual(rawSize, this.size$.value)) {
226
+ this.size$.setValue(rawSize);
227
+ }
228
+ }
229
+ },
230
+ { fireImmediately: true }
231
+ );
133
232
  };
134
233
 
135
234
  public get view(): View {
@@ -146,7 +245,7 @@ export class MainViewProxy {
146
245
  if (mainViewScenePath) {
147
246
  setViewFocusScenePath(mainView, mainViewScenePath);
148
247
  }
149
- this.synchronizer.setView(mainView);
248
+ this.view$.setValue(mainView);
150
249
  return mainView;
151
250
  }
152
251
 
@@ -168,31 +267,18 @@ export class MainViewProxy {
168
267
  public rebind(): void {
169
268
  const divElement = this.mainView.divElement;
170
269
  const disableCameraTransform = this.mainView.disableCameraTransform;
270
+ const camera = { ...this.mainView.camera };
171
271
  this.stop();
172
272
  releaseView(this.mainView);
173
273
  this.removeMainViewListener();
174
274
  this.mainView = this.createMainView();
175
275
  this.mainView.disableCameraTransform = disableCameraTransform;
176
276
  this.mainView.divElement = divElement;
277
+ this.mainView.moveCamera({ ...camera, animationMode: AnimationMode.Immediately });
177
278
  this.addMainViewListener();
178
279
  this.start();
179
280
  }
180
281
 
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
282
  public addMainViewListener(): void {
197
283
  if (this.mainViewIsAddListener) return;
198
284
  if (this.view.divElement) {
@@ -225,13 +311,11 @@ export class MainViewProxy {
225
311
  }, 50);
226
312
 
227
313
  private addCameraListener() {
228
- this.view.callbacks.on("onCameraUpdatedByDevice", this.onCameraUpdatedByDevice);
229
314
  this.view.callbacks.on("onCameraUpdated", this.onCameraOrSizeUpdated);
230
315
  this.view.callbacks.on("onSizeUpdated", this.onCameraOrSizeUpdated);
231
316
  }
232
317
 
233
318
  private removeCameraListener() {
234
- this.view.callbacks.off("onCameraUpdatedByDevice", this.onCameraUpdatedByDevice);
235
319
  this.view.callbacks.off("onCameraUpdated", this.onCameraOrSizeUpdated);
236
320
  this.view.callbacks.off("onSizeUpdated", this.onCameraOrSizeUpdated);
237
321
  }
@@ -241,13 +325,15 @@ export class MainViewProxy {
241
325
  };
242
326
 
243
327
  public stop() {
244
- this.removeCameraListener();
245
- this.manager.refresher?.remove(Fields.MainViewCamera);
246
- this.manager.refresher?.remove(Fields.MainViewSize);
328
+ this.manager.refresher.remove(Fields.MainViewCamera);
329
+ this.manager.refresher.remove(Fields.MainViewSize);
247
330
  this.started = false;
248
331
  }
249
332
 
250
333
  public destroy() {
334
+ this.camera$.destroy();
335
+ this.size$.destroy();
336
+ this.view$.destroy();
251
337
  this.removeMainViewListener();
252
338
  this.stop();
253
339
  this.sideEffectManager.flushAll();
@@ -0,0 +1,240 @@
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
+ if (!this.manager.canOperate) return;
80
+ const halfWbHeight = size$.value.height / 2 / scale$.value;
81
+ const scrollTop = camera.centerY;
82
+ this.scrollStorage.setState({
83
+ scrollTop: clamp(scrollTop, halfWbHeight, this.baseHeight - halfWbHeight),
84
+ });
85
+ callbacks.emit("userScroll");
86
+ };
87
+ this._mainView$.value.callbacks.on("onCameraUpdatedByDevice", onCameraUpdated);
88
+ return () =>
89
+ this._mainView$.value.callbacks.off("onCameraUpdatedByDevice", onCameraUpdated);
90
+ });
91
+
92
+ const scale$ = derive(size$, size => size.width / this.baseWidth);
93
+ this._scale$ = scale$;
94
+
95
+ const page$ = new Val(0);
96
+ this.sideEffect.push(
97
+ combine([scrollTop$, size$, scale$]).subscribe(([scrollTop, size, scale]) => {
98
+ if (scale > 0) {
99
+ const wbHeight = size.height / scale;
100
+ page$.setValue(Math.max(scrollTop / wbHeight - 0.5, 0));
101
+ }
102
+ })
103
+ );
104
+ this._page$ = page$;
105
+
106
+ // 5. bound$ = { contentMode: () => scale$, centerX: W / 2, centerY: H / 2, width: W, height: H }
107
+ this.sideEffect.push(
108
+ combine([scrollTop$, scale$]).subscribe(([scrollTop, scale]) => {
109
+ this.updateBound(scrollTop, size$.value, scale);
110
+ })
111
+ );
112
+
113
+ this.sideEffect.push(
114
+ size$.reaction(() => {
115
+ this.updateScroll(scrollTop$.value);
116
+ })
117
+ );
118
+
119
+ const whiteboard$ = derive(this._root$, this.getWhiteboardElement);
120
+ this._whiteboard$ = whiteboard$;
121
+ this.sideEffect.push(
122
+ whiteboard$.reaction(el => {
123
+ if (el?.parentElement) {
124
+ this.sideEffect.addEventListener(
125
+ el.parentElement,
126
+ "wheel",
127
+ this.onWheel,
128
+ { capture: true, passive: false },
129
+ "wheel"
130
+ );
131
+ }
132
+ })
133
+ );
134
+
135
+ const maxScrollPage$ = combine([this._size$, this._scale$], ([size, scale]) => {
136
+ const halfWbHeight = size.height / 2 / scale;
137
+ return (this.baseHeight - halfWbHeight) / halfWbHeight / 2 - 0.51;
138
+ });
139
+
140
+ this.scrollState$ = combine(
141
+ [this._scrollTop$, this._page$, maxScrollPage$],
142
+ ([scrollTop, page, maxScrollPage]) => {
143
+ return {
144
+ scrollTop: round(scrollTop, 2),
145
+ page: round(page, 2),
146
+ maxScrollPage: round(maxScrollPage, 2),
147
+ };
148
+ }
149
+ );
150
+
151
+ this.updateScroll(scrollTop$.value);
152
+ this.sideEffect.push(
153
+ this.scrollState$.subscribe(state => callbacks.emit("scrollStateChange", state))
154
+ );
155
+
156
+ this.sideEffect.push(
157
+ combine([this._size$, this._scale$]).subscribe(([size, scale]) => {
158
+ if (size.height > 0 && scale > 0) {
159
+ this.initScroll();
160
+ this.sideEffect.flush("initScroll");
161
+ }
162
+ }),
163
+ "initScroll"
164
+ );
165
+ }
166
+
167
+ private initScroll = (): void => {
168
+ const halfWbHeight = this._size$.value.height / 2 / this._scale$.value;
169
+ const scrollTop = this._scrollTop$.value;
170
+ // HACK: set a different value (+0.01) to trigger all effects above
171
+ this._scrollTop$.setValue(
172
+ clamp(scrollTop, halfWbHeight, this.baseHeight - halfWbHeight) - 0.01
173
+ );
174
+ };
175
+
176
+ private updateScroll(scrollTop: number): void {
177
+ this._mainView$.value.moveCamera({
178
+ centerY: scrollTop,
179
+ animationMode: AnimationMode.Immediately,
180
+ });
181
+ }
182
+
183
+ private updateBound(scrollTop: number, { height }: Size, scale: number): void {
184
+ if (scale > 0) {
185
+ this._mainView$.value.moveCameraToContain({
186
+ originX: 0,
187
+ originY: scrollTop - height / scale / 2,
188
+ width: this.baseWidth,
189
+ height: height / scale,
190
+ animationMode: AnimationMode.Immediately,
191
+ });
192
+
193
+ this._mainView$.value.setCameraBound({
194
+ damping: 1,
195
+ maxContentMode: () => scale,
196
+ minContentMode: () => scale,
197
+ centerX: this.baseWidth / 2,
198
+ centerY: this.baseHeight / 2,
199
+ width: this.baseWidth,
200
+ height: this.baseHeight,
201
+ });
202
+ }
203
+ }
204
+
205
+ public dispose(): void {
206
+ this.sideEffect.flushAll();
207
+ this.scrollStorage.disconnect();
208
+ this._root$.destroy();
209
+ this._scale$.destroy();
210
+ this._scrollTop$.destroy();
211
+ this._whiteboard$.destroy();
212
+ this.scrollState$.destroy();
213
+ this._page$.destroy();
214
+ this._size$.destroy();
215
+ this._mainView$.destroy();
216
+ }
217
+
218
+ private getWhiteboardElement = (root: HTMLElement | null): HTMLElement | null => {
219
+ const className = ".netless-window-manager-main-view";
220
+ return root && root.querySelector(className);
221
+ };
222
+
223
+ private onWheel = (ev: WheelEvent): void => {
224
+ const target = ev.target as HTMLElement | null;
225
+ if (this.manager.canOperate && this._whiteboard$.value?.contains(target)) {
226
+ ev.preventDefault();
227
+ ev.stopPropagation();
228
+ const dy = ev.deltaY || 0;
229
+ const { width } = this._size$.value;
230
+ if (dy && width > 0) {
231
+ const halfWbHeight = this._size$.value.height / 2 / this._scale$.value;
232
+ const scrollTop = this._scrollTop$.value + dy / this._scale$.value;
233
+ this.scrollStorage.setState({
234
+ scrollTop: clamp(scrollTop, halfWbHeight, this.baseHeight - halfWbHeight),
235
+ });
236
+ callbacks.emit("userScroll");
237
+ }
238
+ }
239
+ };
240
+ }