@netless/window-manager 1.0.13-test.19 → 1.0.13-test.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netless/window-manager",
3
- "version": "1.0.13-test.19",
3
+ "version": "1.0.13-test.20",
4
4
  "description": "Multi-window mode for Netless Whiteboard",
5
5
  "author": "l1shen <lishen1635@gmail.com> (https://github.com/l1shen)",
6
6
  "license": "MIT",
@@ -30,8 +30,8 @@ export const onObjectByEvent = (event: UpdateEventKind) => {
30
30
  };
31
31
  };
32
32
 
33
- export const safeListenPropsUpdated = <T>(
34
- getProps: () => T,
33
+ export const safeListenPropsUpdated = <T extends Record<string, unknown>>(
34
+ getProps: () => T | null | undefined,
35
35
  callback: AkkoObjectUpdatedListener<T>,
36
36
  onDestroyed?: (props: unknown) => void
37
37
  ) => {
@@ -11,7 +11,6 @@ import type { AppManager } from "../AppManager";
11
11
  import { Events } from "../constants";
12
12
  import { LocalConsole } from "../Utils/log";
13
13
 
14
- (window as any).___local_log = (window as any).___local_log || new Set();
15
14
  export class MainViewProxy {
16
15
  /** Refresh the view's camera in an interval of 1.5s. */
17
16
  public polling = false;
package/src/index.ts CHANGED
@@ -199,6 +199,25 @@ export type MountParams = {
199
199
  useBoxesStatus?: boolean;
200
200
  };
201
201
 
202
+ type DiagnosticColorChannels = {
203
+ raw: string;
204
+ r?: number;
205
+ g?: number;
206
+ b?: number;
207
+ a?: number;
208
+ };
209
+
210
+ type DiagnosticRect = {
211
+ x: number;
212
+ y: number;
213
+ width: number;
214
+ height: number;
215
+ top: number;
216
+ right: number;
217
+ bottom: number;
218
+ left: number;
219
+ };
220
+
202
221
  export const reconnectRefresher = new ReconnectRefresher({ emitter: internalEmitter });
203
222
  export class WindowManager
204
223
  extends InvisiblePlugin<WindowMangerAttributes, any>
@@ -253,16 +272,9 @@ export class WindowManager
253
272
  super(context);
254
273
  WindowManager.displayer = context.displayer;
255
274
  (window as any).NETLESS_DEPS = __APP_DEPENDENCIES__;
256
- this.visibleStateListener();
257
- document.addEventListener("visibilitychange", this.visibleStateListener);
258
275
  this.emitter.on('mainViewScenePathChange', this.onMainViewScenePathChangeHandler)
259
276
  }
260
277
 
261
-
262
- private visibleStateListener = () => {
263
- console.log("[window-manager] visibleStateListener isVisible:" + !document.hidden);
264
- }
265
-
266
278
  public static onCreate(manager: WindowManager) {
267
279
  WindowManager._resolve(manager);
268
280
  }
@@ -400,6 +412,7 @@ export class WindowManager
400
412
 
401
413
  public onMainViewScenePathChangeHandler = (scenePath: string) => {
402
414
  const mainViewElement = this.mainView.divElement;
415
+ this.logMainViewVisibilityDiagnostics("onMainViewScenePathChange", scenePath, mainViewElement);
403
416
  if (mainViewElement) {
404
417
  const backgroundImage = mainViewElement.querySelector('.background img');
405
418
  if (backgroundImage) {
@@ -416,6 +429,415 @@ export class WindowManager
416
429
  console.log("[window-manager] onMainViewScenePathChange scenePath:" + scenePath + ' mainViewElement is not found');
417
430
  }
418
431
 
432
+ public logMainViewVisibilityDiagnostics(
433
+ tag: string,
434
+ scenePath?: string,
435
+ mainViewElement?: HTMLDivElement | null
436
+ ): void {
437
+ const element = mainViewElement ?? this.mainView?.divElement;
438
+ const label = scenePath ? `${tag}:${scenePath}` : tag;
439
+ const payload = this.collectMainViewVisibilityDiagnostics(element, scenePath);
440
+ this.emitMainViewVisibilityDiagnostic(label, payload.summary);
441
+ if (payload.details) {
442
+ this.emitMainViewVisibilityDiagnostic(`${label}:details`, payload.details);
443
+ }
444
+ }
445
+
446
+ private emitMainViewVisibilityDiagnostic(tag: string, payload: unknown): void {
447
+ const content = `[window-manager][visibility][${tag}] ${JSON.stringify(payload)}`;
448
+ // console.log(content);
449
+ this._roomLogger?.info(content);
450
+ }
451
+
452
+ private collectMainViewVisibilityDiagnostics(
453
+ mainViewElement: HTMLDivElement | null | undefined,
454
+ scenePath?: string
455
+ ): {
456
+ summary: Record<string, any>;
457
+ details: Record<string, any> | null;
458
+ } {
459
+ const element = mainViewElement ?? null;
460
+ const backgroundImage = element?.querySelector(".background img") as HTMLImageElement | null;
461
+ const elementDiagnostic = element ? this.collectElementDiagnostic(element) : null;
462
+ const chainDiagnostics = element ? this.collectElementChainDiagnostics(element) : [];
463
+ const elementRect = element?.getBoundingClientRect?.() ?? null;
464
+ const centerPoint = elementRect
465
+ ? {
466
+ x: elementRect.left + elementRect.width / 2,
467
+ y: elementRect.top + elementRect.height / 2,
468
+ }
469
+ : undefined;
470
+ const topElement =
471
+ centerPoint &&
472
+ Number.isFinite(centerPoint.x) &&
473
+ Number.isFinite(centerPoint.y)
474
+ ? document.elementFromPoint(centerPoint.x, centerPoint.y)
475
+ : null;
476
+ const overlayStack =
477
+ centerPoint &&
478
+ Number.isFinite(centerPoint.x) &&
479
+ Number.isFinite(centerPoint.y) &&
480
+ document.elementsFromPoint
481
+ ? document
482
+ .elementsFromPoint(centerPoint.x, centerPoint.y)
483
+ .slice(0, 10)
484
+ .map(item => this.describeElement(item))
485
+ .filter((item): item is string => item !== null)
486
+ : [];
487
+ const topElementDiagnostic = topElement ? this.collectElementDiagnostic(topElement) : null;
488
+ const backgroundImageDiagnostic = backgroundImage
489
+ ? this.collectImageDiagnostic(backgroundImage)
490
+ : null;
491
+ const blockers: string[] = [];
492
+ const warnings: string[] = [];
493
+ const suspiciousAncestors: Array<Record<string, any>> = [];
494
+ const mainViewBlockers: string[] = [];
495
+ const mainViewWarnings: string[] = [];
496
+
497
+ if (!element) {
498
+ blockers.push("mainViewElement.missing");
499
+ }
500
+
501
+ if (document.hidden || document.visibilityState === "hidden") {
502
+ blockers.push("document.hidden");
503
+ }
504
+
505
+ if (elementDiagnostic) {
506
+ this.appendRenderImpactIssues(
507
+ "mainViewElement",
508
+ elementDiagnostic,
509
+ mainViewBlockers,
510
+ mainViewWarnings
511
+ );
512
+ blockers.push(...mainViewBlockers);
513
+ warnings.push(...mainViewWarnings);
514
+ }
515
+
516
+ chainDiagnostics.slice(1).forEach((diagnostic, index) => {
517
+ const ancestorBlockers: string[] = [];
518
+ const ancestorWarnings: string[] = [];
519
+ this.appendRenderImpactIssues(
520
+ `ancestor[${index + 1}]`,
521
+ diagnostic,
522
+ ancestorBlockers,
523
+ ancestorWarnings
524
+ );
525
+ if (ancestorBlockers.length > 0 || ancestorWarnings.length > 0) {
526
+ blockers.push(...ancestorBlockers);
527
+ warnings.push(...ancestorWarnings);
528
+ suspiciousAncestors.push(this.pickRenderRelevantFields(diagnostic));
529
+ }
530
+ });
531
+
532
+ let backgroundImageStatus: Record<string, any> | null = null;
533
+ const backgroundImageBlockers: string[] = [];
534
+ const backgroundImageWarnings: string[] = [];
535
+ if (backgroundImageDiagnostic) {
536
+ backgroundImageStatus = this.pickBackgroundImageStatus(backgroundImageDiagnostic);
537
+ this.appendRenderImpactIssues(
538
+ "backgroundImage",
539
+ backgroundImageDiagnostic,
540
+ backgroundImageBlockers,
541
+ backgroundImageWarnings
542
+ );
543
+ blockers.push(...backgroundImageBlockers);
544
+ warnings.push(...backgroundImageWarnings);
545
+ if (backgroundImageDiagnostic.complete === false) {
546
+ warnings.push("backgroundImage.loading");
547
+ } else if (backgroundImageDiagnostic.naturalWidth === 0) {
548
+ warnings.push("backgroundImage.empty");
549
+ }
550
+ }
551
+
552
+ let topElementSummary: Record<string, any> | null = null;
553
+ if (topElementDiagnostic) {
554
+ const coveredByOutsideElement = Boolean(
555
+ element && topElement && topElement !== element && !element.contains(topElement)
556
+ );
557
+ topElementSummary = {
558
+ node: topElementDiagnostic.node,
559
+ coveredByOutsideElement,
560
+ };
561
+ if (coveredByOutsideElement) {
562
+ warnings.push(`center.coveredBy:${topElementDiagnostic.node}`);
563
+ }
564
+ }
565
+
566
+ const summary: Record<string, any> = {
567
+ scenePath: scenePath || null,
568
+ timestamp: new Date().toISOString(),
569
+ status:
570
+ blockers.length > 0
571
+ ? "blocked"
572
+ : warnings.length > 0
573
+ ? "uncertain"
574
+ : "likely-renderable",
575
+ canRender: blockers.length === 0,
576
+ blockers,
577
+ warnings,
578
+ };
579
+ if (
580
+ backgroundImageStatus &&
581
+ backgroundImageDiagnostic &&
582
+ (backgroundImageDiagnostic.complete === false ||
583
+ backgroundImageDiagnostic.naturalWidth === 0)
584
+ ) {
585
+ summary.backgroundImage = backgroundImageStatus;
586
+ }
587
+ if (topElementSummary?.coveredByOutsideElement) {
588
+ summary.coveringElement = topElementSummary.node;
589
+ }
590
+
591
+ const shouldEmitDetails =
592
+ blockers.length > 0 ||
593
+ warnings.some(
594
+ warning =>
595
+ warning !== "backgroundImage.loading" &&
596
+ warning !== "backgroundImage.empty"
597
+ );
598
+ const details: Record<string, any> = {};
599
+ if ((mainViewBlockers.length > 0 || mainViewWarnings.length > 0) && elementDiagnostic) {
600
+ details.mainViewElement = this.pickRenderRelevantFields(elementDiagnostic);
601
+ }
602
+ if (
603
+ suspiciousAncestors.length > 0
604
+ ) {
605
+ details.suspiciousAncestors = suspiciousAncestors;
606
+ }
607
+ if (
608
+ backgroundImageDiagnostic &&
609
+ (backgroundImageBlockers.length > 0 ||
610
+ backgroundImageWarnings.length > 0 ||
611
+ backgroundImageDiagnostic.complete === false ||
612
+ backgroundImageDiagnostic.naturalWidth === 0)
613
+ ) {
614
+ details.backgroundImage = {
615
+ ...this.pickRenderRelevantFields(backgroundImageDiagnostic),
616
+ loadState: backgroundImageStatus,
617
+ };
618
+ }
619
+ if (topElementSummary?.coveredByOutsideElement && topElementDiagnostic) {
620
+ details.topElementAtCenter = {
621
+ ...this.pickRenderRelevantFields(topElementDiagnostic),
622
+ overlayStack,
623
+ };
624
+ }
625
+
626
+ return {
627
+ summary,
628
+ details: shouldEmitDetails && Object.keys(details).length > 0 ? details : null,
629
+ };
630
+ }
631
+
632
+ private collectElementChainDiagnostics(element: Element): Array<Record<string, any>> {
633
+ const chain: Array<Record<string, any>> = [];
634
+ let current: Element | null = element;
635
+ while (current) {
636
+ const diagnostic = this.collectElementDiagnostic(current);
637
+ if (diagnostic) {
638
+ chain.push(diagnostic);
639
+ }
640
+ current = current.parentElement;
641
+ }
642
+ return chain;
643
+ }
644
+
645
+ private collectImageDiagnostic(image: HTMLImageElement): Record<string, any> {
646
+ const diagnostic = this.collectElementDiagnostic(image);
647
+ return {
648
+ ...diagnostic,
649
+ currentSrc: image.currentSrc,
650
+ src: image.getAttribute("src"),
651
+ complete: image.complete,
652
+ naturalWidth: image.naturalWidth,
653
+ naturalHeight: image.naturalHeight,
654
+ };
655
+ }
656
+
657
+ private appendRenderImpactIssues(
658
+ label: string,
659
+ diagnostic: Record<string, any>,
660
+ blockers: string[],
661
+ warnings: string[]
662
+ ): void {
663
+ const opacity = Number.parseFloat(diagnostic.opacity || "1");
664
+ if (diagnostic.hiddenAttribute) {
665
+ blockers.push(`${label}.hiddenAttribute`);
666
+ }
667
+ if (diagnostic.display === "none") {
668
+ blockers.push(`${label}.display:none`);
669
+ }
670
+ if (diagnostic.visibility === "hidden" || diagnostic.visibility === "collapse") {
671
+ blockers.push(`${label}.visibility:${diagnostic.visibility}`);
672
+ }
673
+ if (Number.isFinite(opacity) && opacity <= 0.01) {
674
+ blockers.push(`${label}.opacity:${diagnostic.opacity}`);
675
+ }
676
+ if (diagnostic.contentVisibility === "hidden") {
677
+ blockers.push(`${label}.contentVisibility:hidden`);
678
+ }
679
+ if (diagnostic.transform !== "none") {
680
+ warnings.push(`${label}.transform`);
681
+ }
682
+ if (diagnostic.filter !== "none") {
683
+ warnings.push(`${label}.filter`);
684
+ }
685
+ if (diagnostic.backdropFilter !== "none") {
686
+ warnings.push(`${label}.backdropFilter`);
687
+ }
688
+ if (diagnostic.clipPath !== "none") {
689
+ warnings.push(`${label}.clipPath`);
690
+ }
691
+ if (diagnostic.maskImage !== "none") {
692
+ warnings.push(`${label}.maskImage`);
693
+ }
694
+ if (diagnostic.mixBlendMode !== "normal") {
695
+ warnings.push(`${label}.mixBlendMode:${diagnostic.mixBlendMode}`);
696
+ }
697
+ }
698
+
699
+ private pickRenderRelevantFields(diagnostic: Record<string, any>): Record<string, any> {
700
+ const result: Record<string, any> = {
701
+ node: diagnostic.node,
702
+ display: diagnostic.display,
703
+ visibility: diagnostic.visibility,
704
+ opacity: diagnostic.opacity,
705
+ hiddenAttribute: diagnostic.hiddenAttribute,
706
+ contentVisibility: diagnostic.contentVisibility,
707
+ backgroundColor: diagnostic.backgroundColor,
708
+ backgroundAlpha: diagnostic.backgroundColorChannels?.a ?? null,
709
+ color: diagnostic.color,
710
+ colorAlpha: diagnostic.colorChannels?.a ?? null,
711
+ textFillColor: diagnostic.textFillColor,
712
+ textFillAlpha: diagnostic.textFillColorChannels?.a ?? null,
713
+ };
714
+ if (diagnostic.transform !== "none") {
715
+ result.transform = diagnostic.transform;
716
+ }
717
+ if (diagnostic.filter !== "none") {
718
+ result.filter = diagnostic.filter;
719
+ }
720
+ if (diagnostic.backdropFilter !== "none") {
721
+ result.backdropFilter = diagnostic.backdropFilter;
722
+ }
723
+ if (diagnostic.mixBlendMode !== "normal") {
724
+ result.mixBlendMode = diagnostic.mixBlendMode;
725
+ }
726
+ if (diagnostic.clipPath !== "none") {
727
+ result.clipPath = diagnostic.clipPath;
728
+ }
729
+ if (diagnostic.maskImage !== "none") {
730
+ result.maskImage = diagnostic.maskImage;
731
+ }
732
+ if (diagnostic.overflow !== "visible") {
733
+ result.overflow = diagnostic.overflow;
734
+ }
735
+ if (diagnostic.zIndex !== "auto") {
736
+ result.zIndex = diagnostic.zIndex;
737
+ }
738
+ return result;
739
+ }
740
+
741
+ private pickBackgroundImageStatus(diagnostic: Record<string, any>): Record<string, any> {
742
+ return {
743
+ found: true,
744
+ complete: diagnostic.complete,
745
+ currentSrc: diagnostic.currentSrc || diagnostic.src || "",
746
+ naturalWidth: diagnostic.naturalWidth,
747
+ naturalHeight: diagnostic.naturalHeight,
748
+ };
749
+ }
750
+
751
+ private collectElementDiagnostic(element: Element | null): Record<string, any> | null {
752
+ if (!element) {
753
+ return null;
754
+ }
755
+ const style = window.getComputedStyle(element);
756
+ const htmlElement = element as HTMLElement;
757
+ return {
758
+ node: this.describeElement(element),
759
+ isConnected: element.isConnected,
760
+ hiddenAttribute: htmlElement.hidden,
761
+ ariaHidden: htmlElement.getAttribute("aria-hidden"),
762
+ opacity: style.opacity,
763
+ alpha: style.opacity,
764
+ display: style.display,
765
+ visibility: style.visibility,
766
+ backgroundColor: style.backgroundColor,
767
+ backgroundColorChannels: this.parseColorChannels(style.backgroundColor),
768
+ color: style.color,
769
+ colorChannels: this.parseColorChannels(style.color),
770
+ textFillColor: style.getPropertyValue("-webkit-text-fill-color"),
771
+ textFillColorChannels: this.parseColorChannels(
772
+ style.getPropertyValue("-webkit-text-fill-color")
773
+ ),
774
+ filter: style.filter,
775
+ backdropFilter: style.getPropertyValue("backdrop-filter"),
776
+ mixBlendMode: style.mixBlendMode,
777
+ transform: style.transform,
778
+ contentVisibility: style.getPropertyValue("content-visibility"),
779
+ clipPath: style.clipPath,
780
+ maskImage:
781
+ style.getPropertyValue("mask-image") ||
782
+ style.getPropertyValue("-webkit-mask-image"),
783
+ overflow: style.overflow,
784
+ zIndex: style.zIndex,
785
+ };
786
+ }
787
+
788
+ private describeElement(element: Element | null): string | null {
789
+ if (!element) {
790
+ return null;
791
+ }
792
+ const tagName = element.tagName.toLowerCase();
793
+ const id = element.id ? `#${element.id}` : "";
794
+ const className =
795
+ typeof (element as HTMLElement).className === "string" &&
796
+ (element as HTMLElement).className.trim()
797
+ ? `.${(element as HTMLElement).className.trim().replace(/\s+/g, ".")}`
798
+ : "";
799
+ return `${tagName}${id}${className}`;
800
+ }
801
+
802
+ private serializeRect(rect?: DOMRect | null): DiagnosticRect | null {
803
+ if (!rect) {
804
+ return null;
805
+ }
806
+ return {
807
+ x: rect.x,
808
+ y: rect.y,
809
+ width: rect.width,
810
+ height: rect.height,
811
+ top: rect.top,
812
+ right: rect.right,
813
+ bottom: rect.bottom,
814
+ left: rect.left,
815
+ };
816
+ }
817
+
818
+ private parseColorChannels(value?: string | null): DiagnosticColorChannels {
819
+ const raw = value?.trim() || "";
820
+ if (!raw) {
821
+ return { raw };
822
+ }
823
+ if (raw === "transparent") {
824
+ return { raw, r: 0, g: 0, b: 0, a: 0 };
825
+ }
826
+ const matches = raw.match(/^rgba?\((.+)\)$/i);
827
+ if (!matches) {
828
+ return { raw };
829
+ }
830
+ const parts = matches[1].split(",").map(part => part.trim());
831
+ const [r, g, b, a] = parts.map(part => Number(part));
832
+ return {
833
+ raw,
834
+ r: Number.isFinite(r) ? r : undefined,
835
+ g: Number.isFinite(g) ? g : undefined,
836
+ b: Number.isFinite(b) ? b : undefined,
837
+ a: Number.isFinite(a) ? a : raw.startsWith("rgb(") ? 1 : undefined,
838
+ };
839
+ }
840
+
419
841
  private static initManager(room: Room): Promise<WindowManager | undefined> {
420
842
  return createInvisiblePlugin(room);
421
843
  }
@@ -488,6 +910,7 @@ export class WindowManager
488
910
  this.appManager.setBoxManager(boxManager);
489
911
  }
490
912
  this.bindMainView(mainViewElement, params.disableCameraTransform);
913
+ this.logMainViewVisibilityDiagnostics("bindContainer.afterBindMainView");
491
914
  if (WindowManager.wrapper) {
492
915
  this.cursorManager?.setupWrapper(WindowManager.wrapper);
493
916
  }
@@ -1083,7 +1506,6 @@ export class WindowManager
1083
1506
  WindowManager.playground.parentNode?.removeChild(WindowManager.playground);
1084
1507
  }
1085
1508
  WindowManager.params = undefined;
1086
- document.removeEventListener("visibilitychange", this.visibleStateListener);
1087
1509
  this.emitter.off('mainViewScenePathChange', this.onMainViewScenePathChangeHandler);
1088
1510
  this._iframeBridge?.destroy();
1089
1511
  this._iframeBridge = undefined;