@mirage-engine/core 0.3.20 → 0.3.22

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.
@@ -8,9 +8,14 @@ import {
8
8
  USER_LAYER,
9
9
  SELECT_LAYER,
10
10
  travelerClipArea,
11
- THREE_LAYERS,
12
11
  ATTR_TRAVEL,
12
+ THREE_LAYERS,
13
13
  LayerTarget,
14
+ WASM_STRIDE,
15
+ OFFSET_WORLD_X,
16
+ OFFSET_WORLD_Y,
17
+ OFFSET_LOCAL_X,
18
+ OFFSET_LOCAL_Y,
14
19
  } from "../types";
15
20
  import { Painter, TextStyles, BoxStyles } from "@mirage-engine/painter";
16
21
  import { MeshRegistry } from "../store/MeshRegistry";
@@ -38,6 +43,14 @@ export class Renderer {
38
43
  private mountContainer: HTMLElement;
39
44
  private registry: MeshRegistry;
40
45
  private targetRect: DOMRect;
46
+ private targetPageX: number = 0;
47
+ private targetPageY: number = 0;
48
+ public size: { width: number; height: number } = { width: 0, height: 0 };
49
+
50
+ private currentScrollX: number = 0;
51
+ private currentScrollY: number = 0;
52
+ private lastTargetLeft: number = 0;
53
+ private lastTargetTop: number = 0;
41
54
 
42
55
  private travelersByLayer: Set<THREE.Mesh>[] = Array.from(
43
56
  { length: ATTR_TRAVEL.MAX_LAYERS },
@@ -70,10 +83,17 @@ export class Renderer {
70
83
  }
71
84
  }, this.isViewport);
72
85
 
86
+ this.qualityFactor = this.getQualityFactor(config.quality);
87
+
73
88
  this.canvas = document.createElement("canvas");
74
89
  this.scene = new THREE.Scene();
75
90
 
76
91
  this.targetRect = this.target.getBoundingClientRect();
92
+ this.lastTargetLeft = this.targetRect.left;
93
+ this.lastTargetTop = this.targetRect.top;
94
+
95
+ this.targetPageX = this.targetRect.left + this.getScrollX();
96
+ this.targetPageY = this.targetRect.top + this.getScrollY();
77
97
  const width = this.isViewport
78
98
  ? window.innerWidth + this.overscan * 2
79
99
  : this.targetRect.width;
@@ -169,6 +189,31 @@ export class Renderer {
169
189
  }
170
190
  }
171
191
 
192
+ private getQualityFactor(quality: Quality | undefined): number {
193
+ if (typeof quality === "number") return Math.max(0.1, quality);
194
+ switch (quality) {
195
+ case "low": return 1;
196
+ case "high": return 4;
197
+ default: return 2;
198
+ }
199
+ }
200
+
201
+ public updateScroll() {
202
+ const rect = this.target.getBoundingClientRect();
203
+ this.currentScrollX += (this.lastTargetLeft - rect.left);
204
+ this.currentScrollY += (this.lastTargetTop - rect.top);
205
+ this.lastTargetLeft = rect.left;
206
+ this.lastTargetTop = rect.top;
207
+ }
208
+
209
+ public getScrollX(): number {
210
+ return this.currentScrollX;
211
+ }
212
+
213
+ public getScrollY(): number {
214
+ return this.currentScrollY;
215
+ }
216
+
172
217
  public mount() {
173
218
  this.mountContainer.prepend(this.canvas);
174
219
 
@@ -274,14 +319,20 @@ export class Renderer {
274
319
 
275
320
  if (isResized) {
276
321
  this.targetRect = newRect;
322
+ this.targetPageX = newRect.left + this.getScrollX();
323
+ this.targetPageY = newRect.top + this.getScrollY();
277
324
  this.setSize(newWidth, newHeight);
278
325
 
279
326
  this.updateCanvasLayout();
280
327
  } else if (isMoved) {
281
328
  this.targetRect = newRect;
329
+ this.targetPageX = newRect.left + this.getScrollX();
330
+ this.targetPageY = newRect.top + this.getScrollY();
282
331
  this.updateCanvasLayout();
283
332
  } else {
284
333
  this.targetRect = newRect;
334
+ this.targetPageX = newRect.left + this.getScrollX();
335
+ this.targetPageY = newRect.top + this.getScrollY();
285
336
  }
286
337
 
287
338
  this.renderOrder = 0;
@@ -381,6 +432,7 @@ export class Renderer {
381
432
  }
382
433
 
383
434
  mesh.userData.clipElements = node.clipElements;
435
+ mesh.userData.wasmIndex = node.wasmIndex;
384
436
 
385
437
  if (node.nativeStyles?.transform) {
386
438
  mesh.userData.nativeTransform = node.nativeStyles.transform;
@@ -573,20 +625,21 @@ export class Renderer {
573
625
  width: rect.width,
574
626
  height: rect.height,
575
627
  };
628
+ mesh.userData.needsNativeReset = true;
576
629
 
577
630
  const Z_MICRO_OFFSET = 0.001;
578
631
  this.renderOrder++;
579
632
 
580
- const targetPageX = this.targetRect.left + window.scrollX;
581
- const targetPageY = this.targetRect.top + window.scrollY;
633
+ // const targetPageX = this.targetRect.left + this.getScrollX();
634
+ // const targetPageY = this.targetRect.top + this.getScrollY();
582
635
 
583
636
  let baseX: number, baseY: number;
584
637
  if (this.isViewport) {
585
638
  baseX = rect.x - window.innerWidth / 2 + rect.width / 2;
586
639
  baseY = -rect.y + window.innerHeight / 2 - rect.height / 2;
587
640
  } else {
588
- const localX = rect.x - targetPageX;
589
- const localY = rect.y - targetPageY;
641
+ const localX = rect.x - this.targetPageX;
642
+ const localY = rect.y - this.targetPageY;
590
643
  baseX = localX - canvasWidth / 2 + rect.width / 2;
591
644
  baseY = -localY + canvasHeight / 2 - rect.height / 2;
592
645
  }
@@ -652,7 +705,7 @@ export class Renderer {
652
705
  this.qualityFactor,
653
706
  node.isTraveler
654
707
  ? this.renderTargets[node.captureLayer - 2]?.texture
655
- : this.textureManager.get(node.element),
708
+ : ((node.nativeStyles as any).imageSrc && (node.nativeStyles as any).imageSrc !== (node.styles as any).imageSrc) ? new THREE.TextureLoader().load((node.nativeStyles as any).imageSrc) : this.textureManager.get(node.element),
656
709
  node.shaderHooks,
657
710
  );
658
711
  const nativeMesh = new THREE.Mesh(mesh.geometry, nativeMaterial);
@@ -671,8 +724,8 @@ export class Renderer {
671
724
  window.innerHeight / 2 -
672
725
  node.nativeRect.height / 2;
673
726
  } else {
674
- const nativeLocalX = node.nativeRect.x - targetPageX;
675
- const nativeLocalY = node.nativeRect.y - targetPageY;
727
+ const nativeLocalX = node.nativeRect.x - this.targetPageX;
728
+ const nativeLocalY = node.nativeRect.y - this.targetPageY;
676
729
  nativeBaseX =
677
730
  nativeLocalX - canvasWidth / 2 + node.nativeRect.width / 2;
678
731
  nativeBaseY =
@@ -757,7 +810,7 @@ export class Renderer {
757
810
  this.qualityFactor,
758
811
  node.isTraveler
759
812
  ? this.renderTargets[node.captureLayer - 2]?.texture
760
- : this.textureManager.get(node.element),
813
+ : ((nativeMesh.material as THREE.ShaderMaterial).uniforms.uTexture.value) || this.textureManager.get(node.element),
761
814
  );
762
815
  } else {
763
816
  if (mesh.userData.nativeMesh) {
@@ -943,177 +996,70 @@ export class Renderer {
943
996
  this.renderer.render(this.scene, this.camera);
944
997
  }
945
998
 
946
- public syncMeshesByDOM() {
947
- // If not in viewport mode, we must account for page scroll in absolute positions.
948
- const targetPageX = this.targetRect.left + window.scrollX;
949
- const targetPageY = this.targetRect.top + window.scrollY;
999
+ public saveInitialLocals(sharedArray: Float32Array) {
1000
+ this.scene.children.forEach((child) => {
1001
+ const mesh = child as THREE.Mesh;
1002
+ if (mesh.userData.wasmIndex !== undefined) {
1003
+ const offset = mesh.userData.wasmIndex * WASM_STRIDE;
1004
+ mesh.userData.initialLocalX = sharedArray[offset + OFFSET_LOCAL_X];
1005
+ mesh.userData.initialLocalY = sharedArray[offset + OFFSET_LOCAL_Y];
1006
+ }
1007
+ });
1008
+ }
950
1009
 
1010
+ public syncMeshesByWasm(sharedArray: Float32Array) {
951
1011
  const pixelRatio = this.renderer.getPixelRatio();
952
1012
  const canvasWidth = this.renderer.domElement.width / pixelRatio;
953
1013
  const canvasHeight = this.renderer.domElement.height / pixelRatio;
954
1014
 
955
1015
  this.scene.children.forEach((child) => {
956
1016
  const mesh = child as THREE.Mesh;
957
- if (!mesh.userData || !mesh.userData.domElement) return;
958
-
959
- const element = mesh.userData.domElement as HTMLElement;
960
- if (!element.isConnected) return;
961
- let rect: DOMRect;
962
- if (element.nodeType === Node.TEXT_NODE) {
963
- const range = document.createRange();
964
- range.selectNode(element);
965
- rect = range.getBoundingClientRect();
966
- } else {
967
- rect = element.getBoundingClientRect();
968
- }
969
- const cached = mesh.userData.domRect;
970
-
971
- // Update if position or size changed by more than 0.5px
972
- if (
973
- !cached ||
974
- Math.abs(rect.x - cached.x) > 0.5 ||
975
- Math.abs(rect.y - cached.y) > 0.5 ||
976
- Math.abs(rect.width - cached.width) > 0.5 ||
977
- Math.abs(rect.height - cached.height) > 0.5
978
- ) {
979
- mesh.userData.domRect = {
980
- x: rect.x,
981
- y: rect.y,
982
- width: rect.width,
983
- height: rect.height,
984
- };
985
-
986
- // Compute scissorRect from clipElements (overflow:hidden ancestors)
987
- const clipEls = mesh.userData.clipElements as HTMLElement[] | undefined;
988
- if (clipEls && clipEls.length > 0) {
989
- let clipLeft = -Infinity;
990
- let clipTop = -Infinity;
991
- let clipRight = Infinity;
992
- let clipBottom = Infinity;
993
- for (const clipEl of clipEls) {
994
- const cr = clipEl.getBoundingClientRect();
995
- clipLeft = Math.max(clipLeft, cr.left);
996
- clipTop = Math.max(clipTop, cr.top);
997
- clipRight = Math.min(clipRight, cr.right);
998
- clipBottom = Math.min(clipBottom, cr.bottom);
999
- }
1000
- // Intersect with the mesh's own rect to avoid scissoring larger than the mesh
1001
- mesh.userData.scissorRect = {
1002
- x: clipLeft,
1003
- y: clipTop,
1004
- width: Math.max(0, clipRight - clipLeft),
1005
- height: Math.max(0, clipBottom - clipTop),
1006
- };
1007
- } else {
1008
- mesh.userData.scissorRect = undefined;
1009
- }
1010
-
1011
- let baseX: number, baseY: number;
1012
- if (this.isViewport) {
1013
- baseX = rect.x - window.innerWidth / 2 + rect.width / 2;
1014
- baseY = -rect.y + window.innerHeight / 2 - rect.height / 2;
1015
- } else {
1016
- const localX = rect.x - targetPageX;
1017
- const localY = rect.y - targetPageY;
1018
- baseX = localX - canvasWidth / 2 + rect.width / 2;
1019
- baseY = -localY + canvasHeight / 2 - rect.height / 2;
1020
- }
1017
+ if (!mesh.userData || mesh.userData.wasmIndex === undefined) return;
1021
1018
 
1022
- // Apply new position and scale
1023
- mesh.position.setX(baseX);
1024
- mesh.position.setY(baseY);
1025
- let pad = (mesh.material as THREE.ShaderMaterial).userData?.shadowPadding || 0;
1026
- mesh.scale.set(rect.width + pad * 2, rect.height + pad * 2, 1);
1027
- mesh.updateMatrixWorld();
1028
-
1029
- // Update uniforms so shader-based drawing (like border-radius) doesn't stretch
1030
- if (mesh.material instanceof THREE.ShaderMaterial) {
1031
- Painter.forceUpdateUniforms(mesh.material, {
1032
- width: rect.width,
1033
- height: rect.height,
1034
- });
1035
- }
1036
-
1037
- // Update native mesh if exists
1038
- if (mesh.userData.nativeMesh) {
1039
- const nativeMesh = mesh.userData.nativeMesh as THREE.Mesh;
1040
- let nScaleX = rect.width;
1041
- let nScaleY = rect.height;
1042
- let nPosX = baseX;
1043
- let nPosY = baseY;
1044
-
1045
- if (mesh.userData.nativeTransform) {
1046
- const transformStr = mesh.userData.nativeTransform as string;
1047
-
1048
- const scaleMatch = transformStr.match(/scale\(([\d.]+%?)\)/);
1049
- if (scaleMatch) {
1050
- let scaleVal = parseFloat(scaleMatch[1]);
1051
- if (scaleMatch[1].includes("%")) scaleVal /= 100;
1052
- nScaleX *= scaleVal;
1053
- nScaleY *= scaleVal;
1054
- }
1019
+ const wasmIndex = mesh.userData.wasmIndex as number;
1020
+ const offset = wasmIndex * WASM_STRIDE;
1021
+
1022
+ const worldX = sharedArray[offset + OFFSET_WORLD_X];
1023
+ const worldY = sharedArray[offset + OFFSET_WORLD_Y];
1055
1024
 
1056
- const scaleXMatch = transformStr.match(/scaleX\(([\d.]+%?)\)/);
1057
- if (scaleXMatch) {
1058
- let scaleVal = parseFloat(scaleXMatch[1]);
1059
- if (scaleXMatch[1].includes("%")) scaleVal /= 100;
1060
- nScaleX *= scaleVal;
1061
- }
1025
+ const rect = mesh.userData.domRect;
1026
+ if (!rect) return;
1062
1027
 
1063
- const scaleYMatch = transformStr.match(/scaleY\(([\d.]+%?)\)/);
1064
- if (scaleYMatch) {
1065
- let scaleVal = parseFloat(scaleYMatch[1]);
1066
- if (scaleYMatch[1].includes("%")) scaleVal /= 100;
1067
- nScaleY *= scaleVal;
1068
- }
1028
+ const isFixed = this.fixedMeshes.has(mesh);
1029
+ const effectiveScrollX = isFixed ? 0 : this.getScrollX();
1030
+ const effectiveScrollY = isFixed ? 0 : this.getScrollY();
1069
1031
 
1070
- const translateMatch = transformStr.match(/translate\(([^,]+),\s*([^)]+)\)/);
1071
- if (translateMatch) {
1072
- const txStr = translateMatch[1].trim();
1073
- const tyStr = translateMatch[2].trim();
1074
-
1075
- let tx = parseFloat(txStr);
1076
- if (txStr.includes("%")) tx = (tx / 100) * rect.width;
1077
-
1078
- let ty = parseFloat(tyStr);
1079
- if (tyStr.includes("%")) ty = (ty / 100) * rect.height;
1080
-
1081
- nPosX += tx;
1082
- nPosY -= ty;
1083
- }
1032
+ let baseX: number, baseY: number;
1033
+ if (this.isViewport) {
1034
+ const viewportX = worldX - effectiveScrollX;
1035
+ const viewportY = worldY - effectiveScrollY;
1036
+ baseX = viewportX - window.innerWidth / 2 + rect.width / 2;
1037
+ baseY = -viewportY + window.innerHeight / 2 - rect.height / 2;
1038
+ } else {
1039
+ const localX = worldX - this.targetPageX;
1040
+ const localY = worldY - this.targetPageY;
1041
+ baseX = localX - canvasWidth / 2 + rect.width / 2;
1042
+ baseY = -localY + canvasHeight / 2 - rect.height / 2;
1043
+ }
1084
1044
 
1085
- const translateXMatch = transformStr.match(/translateX\(([^)]+)\)/);
1086
- if (translateXMatch) {
1087
- const txStr = translateXMatch[1].trim();
1088
- let tx = parseFloat(txStr);
1089
- if (txStr.includes("%")) tx = (tx / 100) * rect.width;
1090
- nPosX += tx;
1091
- }
1045
+ const currentZ = mesh.position.z;
1046
+ mesh.position.set(baseX, baseY, currentZ);
1092
1047
 
1093
- const translateYMatch = transformStr.match(/translateY\(([^)]+)\)/);
1094
- if (translateYMatch) {
1095
- const tyStr = translateYMatch[1].trim();
1096
- let ty = parseFloat(tyStr);
1097
- if (tyStr.includes("%")) ty = (ty / 100) * rect.height;
1098
- nPosY -= ty;
1099
- }
1100
- }
1101
-
1102
- let nPad = (nativeMesh.material as THREE.ShaderMaterial).userData?.shadowPadding || 0;
1103
- nativeMesh.position.setX(nPosX);
1104
- nativeMesh.position.setY(nPosY);
1105
- nativeMesh.scale.set(nScaleX + nPad * 2, nScaleY + nPad * 2, 1);
1106
- nativeMesh.updateMatrixWorld();
1107
-
1108
- // Update uniforms so native shader-based drawing doesn't stretch
1109
- if (nativeMesh.material instanceof THREE.ShaderMaterial) {
1110
- Painter.forceUpdateUniforms(nativeMesh.material, {
1111
- width: nScaleX,
1112
- height: nScaleY,
1113
- });
1114
- }
1048
+ if (mesh.userData.nativeMesh) {
1049
+ const nativeMesh = mesh.userData.nativeMesh as THREE.Mesh;
1050
+ if (mesh.userData.needsNativeReset) {
1051
+ mesh.userData.needsNativeReset = false;
1052
+ } else {
1053
+ const deltaX = baseX - mesh.userData.basePosition.x;
1054
+ const deltaY = baseY - mesh.userData.basePosition.y;
1055
+
1056
+ nativeMesh.position.setX(nativeMesh.position.x + deltaX);
1057
+ nativeMesh.position.setY(nativeMesh.position.y + deltaY);
1115
1058
  }
1116
1059
  }
1060
+
1061
+ mesh.userData.basePosition.x = baseX;
1062
+ mesh.userData.basePosition.y = baseY;
1117
1063
  });
1118
1064
  }
1119
1065
  }
@@ -10,6 +10,13 @@ export interface NodeRect {
10
10
  height: number;
11
11
  }
12
12
 
13
+ export const WASM_STRIDE = 5;
14
+ export const OFFSET_PARENT = 0;
15
+ export const OFFSET_LOCAL_X = 1;
16
+ export const OFFSET_LOCAL_Y = 2;
17
+ export const OFFSET_WORLD_X = 3;
18
+ export const OFFSET_WORLD_Y = 4;
19
+
13
20
  export interface SceneNode {
14
21
  id: string;
15
22
  type: NodeType;
@@ -35,5 +42,7 @@ export interface SceneNode {
35
42
  shaderHooks?: ShaderHooks;
36
43
  clipElements?: HTMLElement[];
37
44
 
45
+ wasmIndex?: number;
46
+
38
47
  children: SceneNode[];
39
48
  }
@@ -0,0 +1,44 @@
1
+ import init, { MemoryManager } from "../../../wasm-compute/pkg/wasm_compute.js";
2
+
3
+ export class WasmSynchronizer {
4
+ private memoryManager: MemoryManager | null = null;
5
+ public sharedArray: Float32Array | null = null;
6
+ private isInitialized = false;
7
+
8
+ /**
9
+ * Initialize Wasm instance and allocate a 1D Float32Array shared memory
10
+ * @param capacity The number of Float32 elements to allocate
11
+ */
12
+ async initialize(capacity: number) {
13
+ if (this.isInitialized) return;
14
+
15
+ try {
16
+ const wasm = await init();
17
+
18
+ this.memoryManager = new MemoryManager(capacity);
19
+
20
+ const pointer = this.memoryManager.get_pointer();
21
+ const length = this.memoryManager.get_length();
22
+ this.sharedArray = new Float32Array(wasm.memory.buffer, pointer, length);
23
+
24
+ this.isInitialized = true;
25
+ console.log(`[WasmSynchronizer] Initialized with capacity: ${capacity} floats (${capacity * 4} bytes)`);
26
+ } catch (error) {
27
+ console.error("[WasmSynchronizer] Failed to initialize Wasm:", error);
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ getSharedArray(): Float32Array {
33
+ if (!this.isInitialized || !this.sharedArray) {
34
+ throw new Error("WasmSynchronizer is not initialized yet. Call initialize() first.");
35
+ }
36
+ return this.sharedArray;
37
+ }
38
+
39
+ updatePhysics(nodeCount: number) {
40
+ if (this.isInitialized && this.memoryManager) {
41
+ this.memoryManager.update_physics(nodeCount);
42
+ }
43
+ }
44
+ }