oomi-ai 0.2.40 → 0.2.41
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/lib/personaRuntimeProcess.js +32 -7
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/templates/persona-app/src/App.css +4 -8
- package/templates/persona-app/src/pages/HomePage.tsx +9 -9
- package/templates/persona-app/src/spatial.ts +1 -1
- package/templates/persona-app/vendor/webspatial/FORK.md +1 -1
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.js +100 -229
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.js.map +1 -1
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.js +100 -229
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.js.map +1 -1
|
@@ -39,7 +39,9 @@ const LEGACY_WEBSPATIAL_TEMPLATE_FILE_RULES = [
|
|
|
39
39
|
relativePath: path.join('src', 'spatial.ts'),
|
|
40
40
|
shouldReplace: (content) =>
|
|
41
41
|
!content.includes('WEBSPATIAL_FORK_REPOSITORY') ||
|
|
42
|
-
!content.includes('configurePersonaScene')
|
|
42
|
+
!content.includes('configurePersonaScene') ||
|
|
43
|
+
content.includes('WEBSPATIAL_FORK_COMMIT = "b2746721e4fe6b4f86dac0ea55938074eea00cda"') ||
|
|
44
|
+
content.includes('WEBSPATIAL_FORK_COMMIT = "8904ac8fec48fe49ee14d1739237bd1afb2894fe"'),
|
|
43
45
|
},
|
|
44
46
|
{
|
|
45
47
|
relativePath: path.join('src', 'main.tsx'),
|
|
@@ -57,21 +59,44 @@ const LEGACY_WEBSPATIAL_TEMPLATE_FILE_RULES = [
|
|
|
57
59
|
{
|
|
58
60
|
relativePath: path.join('src', 'pages', 'HomePage.tsx'),
|
|
59
61
|
shouldReplace: (content) =>
|
|
60
|
-
content.includes('Open Spatial Scene') &&
|
|
61
|
-
content.includes('Open Scene Route')
|
|
62
|
+
(content.includes('Open Spatial Scene') &&
|
|
63
|
+
content.includes('Open Scene Route')) ||
|
|
64
|
+
(content.includes('Launch Spatial Surface') &&
|
|
65
|
+
content.includes('Open Browser Preview') &&
|
|
66
|
+
content.includes('Focused surface') &&
|
|
67
|
+
!content.includes('sceneMode')) ||
|
|
68
|
+
content.includes('enable-xr-monitor={sceneMode || undefined}') ||
|
|
69
|
+
content.includes('enable-xr={sceneMode || undefined}') ||
|
|
70
|
+
(
|
|
71
|
+
content.includes('persona-panel persona-runtime" enable-xr') ||
|
|
72
|
+
content.includes('persona-button" onClick={openPersonaScene} enable-xr') ||
|
|
73
|
+
content.includes('persona-link" to="/scene" target="_blank" enable-xr') ||
|
|
74
|
+
content.includes('persona-card" enable-xr style={xrStyle(')
|
|
75
|
+
),
|
|
62
76
|
},
|
|
63
77
|
{
|
|
64
78
|
relativePath: path.join('src', 'pages', 'ScenePage.tsx'),
|
|
65
79
|
shouldReplace: (content) =>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
(
|
|
81
|
+
!content.includes('enable-xr-monitor') &&
|
|
82
|
+
content.includes(
|
|
83
|
+
'This route is intentionally separate so WebSpatial scene launching has a dedicated',
|
|
84
|
+
)
|
|
85
|
+
) ||
|
|
86
|
+
(
|
|
87
|
+
content.includes('Awaiting AndroidXR interaction') &&
|
|
88
|
+
content.includes('Interaction Console') &&
|
|
89
|
+
content.includes('Fork-backed proof points')
|
|
69
90
|
),
|
|
70
91
|
},
|
|
71
92
|
{
|
|
72
93
|
relativePath: path.join('src', 'App.css'),
|
|
73
94
|
shouldReplace: (content) =>
|
|
74
|
-
content.includes('.scene-panel') && !content.includes('.scene-interaction-grid')
|
|
95
|
+
(content.includes('.scene-panel') && !content.includes('.scene-interaction-grid')) ||
|
|
96
|
+
content.includes('html.is-spatial .persona-runtime {') ||
|
|
97
|
+
content.includes('html.is-spatial .persona-scene-root {') ||
|
|
98
|
+
content.includes('html.is-spatial .persona-button,') ||
|
|
99
|
+
content.includes('html.is-spatial .persona-card,'),
|
|
75
100
|
},
|
|
76
101
|
{
|
|
77
102
|
relativePath: 'vite.config.ts',
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -105,6 +105,10 @@ code {
|
|
|
105
105
|
gap: 24px;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
.persona-scene-root {
|
|
109
|
+
position: relative;
|
|
110
|
+
}
|
|
111
|
+
|
|
108
112
|
.persona-header,
|
|
109
113
|
.persona-grid,
|
|
110
114
|
.scene-interaction-grid,
|
|
@@ -594,19 +598,11 @@ html.is-spatial body {
|
|
|
594
598
|
background: transparent;
|
|
595
599
|
}
|
|
596
600
|
|
|
597
|
-
html.is-spatial .persona-button,
|
|
598
|
-
html.is-spatial .persona-link,
|
|
599
601
|
html.is-spatial .scene-chip,
|
|
600
602
|
html.is-spatial .scene-material-chip {
|
|
601
603
|
--xr-background-material: regular;
|
|
602
604
|
}
|
|
603
605
|
|
|
604
|
-
html.is-spatial .persona-runtime {
|
|
605
|
-
transform: translateZ(20px) rotateX(10deg);
|
|
606
|
-
transform-origin: top right;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
html.is-spatial .persona-card,
|
|
610
606
|
html.is-spatial .scene-ops-card,
|
|
611
607
|
html.is-spatial .scene-interaction-card,
|
|
612
608
|
html.is-spatial .scene-proof-card,
|
|
@@ -47,9 +47,9 @@ export function HomePage({ sceneMode = false }: HomePageProps) {
|
|
|
47
47
|
}, []);
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
|
-
<main className=
|
|
50
|
+
<main className={`persona-shell${sceneMode ? " persona-scene-root" : ""}`}>
|
|
51
51
|
<section className="persona-header">
|
|
52
|
-
<div className="persona-panel persona-hero"
|
|
52
|
+
<div className="persona-panel persona-hero">
|
|
53
53
|
<div>
|
|
54
54
|
<p className="persona-eyebrow">{sceneMode ? "Spatial Persona Surface" : "WebSpatial Persona"}</p>
|
|
55
55
|
<h1 className="persona-title">{personaConfig.name}</h1>
|
|
@@ -59,15 +59,15 @@ export function HomePage({ sceneMode = false }: HomePageProps) {
|
|
|
59
59
|
<div className="persona-actions">
|
|
60
60
|
{!sceneMode ? (
|
|
61
61
|
<>
|
|
62
|
-
<button className="persona-button" onClick={openPersonaScene}
|
|
62
|
+
<button className="persona-button" onClick={openPersonaScene}>
|
|
63
63
|
Launch Spatial Surface
|
|
64
64
|
</button>
|
|
65
|
-
<Link className="persona-link" to="/scene" target="_blank"
|
|
65
|
+
<Link className="persona-link" to="/scene" target="_blank">
|
|
66
66
|
Open Spatial Preview
|
|
67
67
|
</Link>
|
|
68
68
|
</>
|
|
69
69
|
) : (
|
|
70
|
-
<Link className="persona-button" to="/"
|
|
70
|
+
<Link className="persona-button" to="/">
|
|
71
71
|
Return To Browser View
|
|
72
72
|
</Link>
|
|
73
73
|
)}
|
|
@@ -89,7 +89,7 @@ export function HomePage({ sceneMode = false }: HomePageProps) {
|
|
|
89
89
|
</div>
|
|
90
90
|
</div>
|
|
91
91
|
|
|
92
|
-
<aside className="persona-panel persona-runtime"
|
|
92
|
+
<aside className="persona-panel persona-runtime">
|
|
93
93
|
<p className="persona-eyebrow">Runtime</p>
|
|
94
94
|
<div className="runtime-list">
|
|
95
95
|
<div className="runtime-row">
|
|
@@ -117,7 +117,7 @@ export function HomePage({ sceneMode = false }: HomePageProps) {
|
|
|
117
117
|
</section>
|
|
118
118
|
|
|
119
119
|
<section className="persona-grid">
|
|
120
|
-
<article className="persona-panel persona-card"
|
|
120
|
+
<article className="persona-panel persona-card">
|
|
121
121
|
<h2>What Good XR Feels Like</h2>
|
|
122
122
|
<p>
|
|
123
123
|
Treat the generated persona site like a spatial work surface. The XR route should keep
|
|
@@ -131,7 +131,7 @@ export function HomePage({ sceneMode = false }: HomePageProps) {
|
|
|
131
131
|
</p>
|
|
132
132
|
</article>
|
|
133
133
|
|
|
134
|
-
<article className="persona-panel persona-card"
|
|
134
|
+
<article className="persona-panel persona-card">
|
|
135
135
|
<h2>Editing Notes</h2>
|
|
136
136
|
<ul className="persona-note-list">
|
|
137
137
|
{personaNotes.map(note => (
|
|
@@ -140,7 +140,7 @@ export function HomePage({ sceneMode = false }: HomePageProps) {
|
|
|
140
140
|
</ul>
|
|
141
141
|
</article>
|
|
142
142
|
|
|
143
|
-
<article className="persona-panel persona-card persona-scene-card"
|
|
143
|
+
<article className="persona-panel persona-card persona-scene-card">
|
|
144
144
|
<h2>Scene Contract</h2>
|
|
145
145
|
<p>
|
|
146
146
|
The page should import WebSpatial helpers, self-configure the scene, and keep using
|
|
@@ -3,7 +3,7 @@ import { initScene, version as webSpatialSdkVersion } from "@webspatial/react-sd
|
|
|
3
3
|
|
|
4
4
|
export const PERSONA_SCENE_NAME = "personaScene";
|
|
5
5
|
export const WEBSPATIAL_FORK_REPOSITORY = "https://github.com/zill4/webspatial-sdk";
|
|
6
|
-
export const WEBSPATIAL_FORK_COMMIT = "
|
|
6
|
+
export const WEBSPATIAL_FORK_COMMIT = "ac4bd47eb14a894ffef34a4044ddd0bbd47f3e72";
|
|
7
7
|
|
|
8
8
|
type SpatialWindow = Window & {
|
|
9
9
|
webspatialBridge?: unknown;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
This persona template vendors the AndroidXR-capable WebSpatial runtime packages from:
|
|
2
2
|
|
|
3
3
|
- Repository: https://github.com/zill4/webspatial-sdk
|
|
4
|
-
- Commit:
|
|
4
|
+
- Commit: ac4bd47eb14a894ffef34a4044ddd0bbd47f3e72
|
|
5
5
|
|
|
6
6
|
Oomi uses these vendored packages for persona runtimes so OpenClaw installs the AndroidXR-enabled fork instead of the stock npm release.
|
|
@@ -1014,17 +1014,36 @@ function hasTransparentBackground(element) {
|
|
|
1014
1014
|
}
|
|
1015
1015
|
function injectCaptureBackground(element, backgroundColor = DEFAULT_CAPTURE_BACKGROUND) {
|
|
1016
1016
|
const restoreFunctions = [];
|
|
1017
|
-
const originalBg = element.style.backgroundColor;
|
|
1018
1017
|
const wasTransparent = hasTransparentBackground(element);
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
element.style.backgroundColor =
|
|
1022
|
-
|
|
1018
|
+
if (wasTransparent) {
|
|
1019
|
+
const originalBg = element.style.backgroundColor;
|
|
1020
|
+
element.style.backgroundColor = backgroundColor;
|
|
1021
|
+
restoreFunctions.push(() => {
|
|
1022
|
+
element.style.backgroundColor = originalBg;
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
const shouldInjectDescendantBackground = (candidate) => {
|
|
1026
|
+
if (!hasTransparentBackground(candidate)) {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
const style = window.getComputedStyle(candidate);
|
|
1030
|
+
if (style.display === "inline" || style.display === "contents") {
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
const rect = candidate.getBoundingClientRect();
|
|
1034
|
+
const hasMeaningfulBox = rect.width >= 32 && rect.height >= 32;
|
|
1035
|
+
if (!hasMeaningfulBox) {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
const hasNestedLayout = candidate.children.length > 0;
|
|
1039
|
+
const hasVisualContainerTraits = style.borderRadius !== "0px" || style.boxShadow !== "none" || style.backdropFilter !== "none" || style.overflow !== "visible" || style.borderStyle !== "none";
|
|
1040
|
+
return hasNestedLayout || hasVisualContainerTraits;
|
|
1041
|
+
};
|
|
1023
1042
|
const allDescendants = element.querySelectorAll("*");
|
|
1024
1043
|
let injectedCount = 0;
|
|
1025
1044
|
allDescendants.forEach((el) => {
|
|
1026
1045
|
const htmlEl = el;
|
|
1027
|
-
if (
|
|
1046
|
+
if (shouldInjectDescendantBackground(htmlEl)) {
|
|
1028
1047
|
const childOriginalBg = htmlEl.style.backgroundColor;
|
|
1029
1048
|
htmlEl.style.backgroundColor = backgroundColor;
|
|
1030
1049
|
injectedCount++;
|
|
@@ -1133,121 +1152,51 @@ async function captureWithSnapdom(snapdom, element, scale) {
|
|
|
1133
1152
|
return null;
|
|
1134
1153
|
}
|
|
1135
1154
|
}
|
|
1136
|
-
function
|
|
1137
|
-
const
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
const parentLeft = parseFloat(parentStyle.left) || 0;
|
|
1160
|
-
if (parentTop !== 0 || parentLeft !== 0) {
|
|
1161
|
-
console.log(
|
|
1162
|
-
`[WebSpatial] Found offset element: ${parent.tagName}.${parent.className?.split(" ")[0] || ""} top=${parentTop}px, left=${parentLeft}px`
|
|
1163
|
-
);
|
|
1164
|
-
offsetElements.push({
|
|
1165
|
-
element: parent,
|
|
1166
|
-
originalTop: parent.style.top,
|
|
1167
|
-
originalLeft: parent.style.left,
|
|
1168
|
-
topValue: parentTop,
|
|
1169
|
-
leftValue: parentLeft
|
|
1170
|
-
});
|
|
1155
|
+
function createVisibleCaptureClone(element) {
|
|
1156
|
+
const rect = element.getBoundingClientRect();
|
|
1157
|
+
const sandbox = document.createElement("div");
|
|
1158
|
+
sandbox.setAttribute("aria-hidden", "true");
|
|
1159
|
+
sandbox.style.position = "fixed";
|
|
1160
|
+
sandbox.style.left = "-10000px";
|
|
1161
|
+
sandbox.style.top = "0px";
|
|
1162
|
+
sandbox.style.pointerEvents = "none";
|
|
1163
|
+
sandbox.style.zIndex = "-1";
|
|
1164
|
+
sandbox.style.contain = "layout style paint";
|
|
1165
|
+
sandbox.style.opacity = "1";
|
|
1166
|
+
const clone = element.cloneNode(true);
|
|
1167
|
+
const makeCloneVisible = (node) => {
|
|
1168
|
+
node.style.visibility = "visible";
|
|
1169
|
+
node.style.opacity = "1";
|
|
1170
|
+
node.style.transition = "none";
|
|
1171
|
+
node.style.animation = "none";
|
|
1172
|
+
node.style.transform = "none";
|
|
1173
|
+
node.style.top = "0px";
|
|
1174
|
+
node.style.left = "0px";
|
|
1175
|
+
Array.from(node.children).forEach((child) => {
|
|
1176
|
+
if (child instanceof HTMLElement) {
|
|
1177
|
+
makeCloneVisible(child);
|
|
1171
1178
|
}
|
|
1172
|
-
}
|
|
1173
|
-
parent = parent.parentElement;
|
|
1174
|
-
depth++;
|
|
1175
|
-
}
|
|
1176
|
-
return offsetElements;
|
|
1177
|
-
}
|
|
1178
|
-
function getContentOffset(element) {
|
|
1179
|
-
const offsetElements = findOffsetElements(element);
|
|
1180
|
-
return {
|
|
1181
|
-
top: offsetElements.reduce((sum, el) => sum + el.topValue, 0),
|
|
1182
|
-
left: offsetElements.reduce((sum, el) => sum + el.leftValue, 0)
|
|
1183
|
-
};
|
|
1184
|
-
}
|
|
1185
|
-
function makeElementVisible(element) {
|
|
1186
|
-
const restoreFunctions = [];
|
|
1187
|
-
const originalVisibility = element.style.visibility;
|
|
1188
|
-
const computedVisibility = window.getComputedStyle(element).visibility;
|
|
1189
|
-
if (computedVisibility === "hidden") {
|
|
1190
|
-
element.style.visibility = "visible";
|
|
1191
|
-
console.log(`[WebSpatial] Made element visible for capture (was: ${computedVisibility})`);
|
|
1192
|
-
restoreFunctions.push(() => {
|
|
1193
|
-
element.style.visibility = originalVisibility;
|
|
1194
1179
|
});
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1207
|
-
});
|
|
1208
|
-
if (restoreFunctions.length > 1) {
|
|
1209
|
-
console.log(`[WebSpatial] Made ${restoreFunctions.length} elements visible for capture`);
|
|
1210
|
-
}
|
|
1211
|
-
return () => {
|
|
1212
|
-
restoreFunctions.forEach((restore) => restore());
|
|
1180
|
+
};
|
|
1181
|
+
makeCloneVisible(clone);
|
|
1182
|
+
clone.style.position = "relative";
|
|
1183
|
+
clone.style.margin = "0px";
|
|
1184
|
+
clone.style.width = `${Math.ceil(rect.width)}px`;
|
|
1185
|
+
clone.style.minHeight = `${Math.ceil(rect.height)}px`;
|
|
1186
|
+
sandbox.appendChild(clone);
|
|
1187
|
+
document.body.appendChild(sandbox);
|
|
1188
|
+
return {
|
|
1189
|
+
clone,
|
|
1190
|
+
cleanup: () => sandbox.remove()
|
|
1213
1191
|
};
|
|
1214
1192
|
}
|
|
1215
1193
|
async function captureWithHtml2Canvas(html2canvas, element, scale, backgroundColor) {
|
|
1216
1194
|
try {
|
|
1217
1195
|
const rect = element.getBoundingClientRect();
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1196
|
+
console.log(
|
|
1197
|
+
`[WebSpatial] html2canvas capturing via visible clone: rect=(${rect.x.toFixed(0)},${rect.y.toFixed(0)},${rect.width.toFixed(0)},${rect.height.toFixed(0)})`
|
|
1198
|
+
);
|
|
1221
1199
|
const scrollY = window.scrollY || window.pageYOffset || 0;
|
|
1222
|
-
const absoluteX = rect.x + scrollX;
|
|
1223
|
-
const absoluteY = rect.y + scrollY;
|
|
1224
|
-
const offsetElements = findOffsetElements(element);
|
|
1225
|
-
if (offsetElements.length > 0) {
|
|
1226
|
-
console.log(
|
|
1227
|
-
`[WebSpatial] Temporarily resetting ${offsetElements.length} offset elements for capture`
|
|
1228
|
-
);
|
|
1229
|
-
offsetElements.forEach(({ element: el, topValue, leftValue }) => {
|
|
1230
|
-
el.style.top = "0px";
|
|
1231
|
-
el.style.left = "0px";
|
|
1232
|
-
console.log(
|
|
1233
|
-
`[WebSpatial] Reset: ${el.tagName}.${el.className?.split(" ")[0] || ""} from top=${topValue}px, left=${leftValue}px to 0`
|
|
1234
|
-
);
|
|
1235
|
-
});
|
|
1236
|
-
void element.offsetHeight;
|
|
1237
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1238
|
-
console.log("[WebSpatial] Reflow complete after offset reset");
|
|
1239
|
-
}
|
|
1240
|
-
const restorePositions = () => {
|
|
1241
|
-
offsetElements.forEach(({ element: el, originalTop, originalLeft }) => {
|
|
1242
|
-
el.style.top = originalTop;
|
|
1243
|
-
el.style.left = originalLeft;
|
|
1244
|
-
});
|
|
1245
|
-
};
|
|
1246
|
-
if (element.scrollTop !== 0) {
|
|
1247
|
-
console.log(`[WebSpatial] Resetting element scroll from ${element.scrollTop} to 0`);
|
|
1248
|
-
element.scrollTop = 0;
|
|
1249
|
-
}
|
|
1250
|
-
const restoreVisibility = makeElementVisible(element);
|
|
1251
1200
|
const viewportTop = scrollY;
|
|
1252
1201
|
const viewportBottom = scrollY + window.innerHeight;
|
|
1253
1202
|
const elementTop = rect.y + scrollY;
|
|
@@ -1256,14 +1205,20 @@ async function captureWithHtml2Canvas(html2canvas, element, scale, backgroundCol
|
|
|
1256
1205
|
`[WebSpatial] Capture context: viewport=(${viewportTop}-${viewportBottom}), element=(${elementTop}-${elementBottom}), innerHeight=${window.innerHeight}`
|
|
1257
1206
|
);
|
|
1258
1207
|
let canvas;
|
|
1208
|
+
const captureClone = createVisibleCaptureClone(element);
|
|
1209
|
+
const restoreBackground = injectCaptureBackground(
|
|
1210
|
+
captureClone.clone,
|
|
1211
|
+
backgroundColor || DEFAULT_CAPTURE_BACKGROUND
|
|
1212
|
+
);
|
|
1259
1213
|
try {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1214
|
+
await new Promise(
|
|
1215
|
+
(resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))
|
|
1216
|
+
);
|
|
1217
|
+
const cloneRect = captureClone.clone.getBoundingClientRect();
|
|
1263
1218
|
console.log(
|
|
1264
|
-
`[WebSpatial]
|
|
1219
|
+
`[WebSpatial] Visible clone ready: rect=(${cloneRect.x.toFixed(0)},${cloneRect.y.toFixed(0)},${cloneRect.width.toFixed(0)},${cloneRect.height.toFixed(0)})`
|
|
1265
1220
|
);
|
|
1266
|
-
canvas = await html2canvas(
|
|
1221
|
+
canvas = await html2canvas(captureClone.clone, {
|
|
1267
1222
|
backgroundColor,
|
|
1268
1223
|
logging: true,
|
|
1269
1224
|
// Enable logging to debug
|
|
@@ -1272,87 +1227,11 @@ async function captureWithHtml2Canvas(html2canvas, element, scale, backgroundCol
|
|
|
1272
1227
|
allowTaint: true,
|
|
1273
1228
|
imageTimeout: 5e3,
|
|
1274
1229
|
removeContainer: true,
|
|
1275
|
-
foreignObjectRendering: false
|
|
1276
|
-
// Don't set scroll offset - let html2canvas use default (current scroll)
|
|
1277
|
-
// scrollX and scrollY would affect rendering position but we want document coordinates
|
|
1278
|
-
// Crop to element's document position
|
|
1279
|
-
// x/y define the top-left corner of the crop region in document coordinates
|
|
1280
|
-
x: captureX,
|
|
1281
|
-
y: captureY,
|
|
1282
|
-
width: currentRect.width,
|
|
1283
|
-
height: currentRect.height,
|
|
1284
|
-
// Use onclone to MOVE the element to (0,0) in the cloned document
|
|
1285
|
-
// This is the key fix for elements far down the page (like footer at Y=1142)
|
|
1286
|
-
// By moving to absolute position (0,0), html2canvas will render at the top-left
|
|
1287
|
-
onclone: (clonedDoc, clonedElement) => {
|
|
1288
|
-
const originalRect = clonedElement.getBoundingClientRect();
|
|
1289
|
-
console.log(`[WebSpatial] CLONE: Element position in clone (${originalRect.x.toFixed(0)},${originalRect.y.toFixed(0)})`);
|
|
1290
|
-
clonedElement.style.visibility = "visible";
|
|
1291
|
-
const hiddenElements = clonedElement.querySelectorAll("*");
|
|
1292
|
-
let visibilityFixCount = 0;
|
|
1293
|
-
hiddenElements.forEach((el) => {
|
|
1294
|
-
const htmlEl = el;
|
|
1295
|
-
const style = clonedDoc.defaultView?.getComputedStyle(htmlEl);
|
|
1296
|
-
if (style && style.visibility === "hidden") {
|
|
1297
|
-
htmlEl.style.visibility = "visible";
|
|
1298
|
-
visibilityFixCount++;
|
|
1299
|
-
}
|
|
1300
|
-
});
|
|
1301
|
-
if (visibilityFixCount > 0) {
|
|
1302
|
-
console.log(`[WebSpatial] CLONE: Made ${visibilityFixCount + 1} elements visible`);
|
|
1303
|
-
}
|
|
1304
|
-
let parent = clonedElement.parentElement;
|
|
1305
|
-
let depth = 0;
|
|
1306
|
-
const maxDepth = 10;
|
|
1307
|
-
while (parent && depth < maxDepth) {
|
|
1308
|
-
const parentStyle = clonedDoc.defaultView?.getComputedStyle(parent);
|
|
1309
|
-
if (parentStyle && parentStyle.position === "relative") {
|
|
1310
|
-
const parentTop = parseFloat(parentStyle.top) || 0;
|
|
1311
|
-
const parentLeft = parseFloat(parentStyle.left) || 0;
|
|
1312
|
-
if (parentTop !== 0 || parentLeft !== 0) {
|
|
1313
|
-
console.log(
|
|
1314
|
-
`[WebSpatial] CLONE: Reset ${parent.tagName}.${parent.className?.split(" ")[0] || ""} from top=${parentTop}px, left=${parentLeft}px to 0`
|
|
1315
|
-
);
|
|
1316
|
-
parent.style.top = "0px";
|
|
1317
|
-
parent.style.left = "0px";
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
parent = parent.parentElement;
|
|
1321
|
-
depth++;
|
|
1322
|
-
}
|
|
1323
|
-
void clonedElement.offsetHeight;
|
|
1324
|
-
const clonedRect = clonedElement.getBoundingClientRect();
|
|
1325
|
-
const clonedStyle = clonedDoc.defaultView?.getComputedStyle(clonedElement);
|
|
1326
|
-
console.log(
|
|
1327
|
-
`[WebSpatial] Cloned element (after visibility fix): rect=(${clonedRect.x.toFixed(0)},${clonedRect.y.toFixed(0)},${clonedRect.width.toFixed(0)},${clonedRect.height.toFixed(0)}), visibility=${clonedStyle?.visibility}`
|
|
1328
|
-
);
|
|
1329
|
-
const numChildren = Math.min(5, clonedElement.children.length);
|
|
1330
|
-
for (let i = 0; i < numChildren; i++) {
|
|
1331
|
-
const child = clonedElement.children[i];
|
|
1332
|
-
const childRect = child.getBoundingClientRect();
|
|
1333
|
-
const childStyle = clonedDoc.defaultView?.getComputedStyle(child);
|
|
1334
|
-
console.log(
|
|
1335
|
-
`[WebSpatial] Child ${i}: ${child.tagName}.${child.className?.split(" ")[0] || ""}, rect=(${childRect.x.toFixed(0)},${childRect.y.toFixed(0)},${childRect.width.toFixed(0)},${childRect.height.toFixed(0)}), vis=${childStyle?.visibility}, display=${childStyle?.display}`
|
|
1336
|
-
);
|
|
1337
|
-
}
|
|
1338
|
-
const productGrid = clonedElement.querySelector(".auto-fill-grid");
|
|
1339
|
-
if (productGrid) {
|
|
1340
|
-
const gridRect = productGrid.getBoundingClientRect();
|
|
1341
|
-
const gridStyle = clonedDoc.defaultView?.getComputedStyle(productGrid);
|
|
1342
|
-
console.log(
|
|
1343
|
-
`[WebSpatial] Product grid: rect=(${gridRect.x.toFixed(0)},${gridRect.y.toFixed(0)},${gridRect.width.toFixed(0)},${gridRect.height.toFixed(0)}), columns=${gridStyle?.gridTemplateColumns?.substring(0, 100)}`
|
|
1344
|
-
);
|
|
1345
|
-
const firstCard = productGrid.children[0];
|
|
1346
|
-
if (firstCard) {
|
|
1347
|
-
const cardRect = firstCard.getBoundingClientRect();
|
|
1348
|
-
console.log(`[WebSpatial] First product card: rect=(${cardRect.x.toFixed(0)},${cardRect.y.toFixed(0)},${cardRect.width.toFixed(0)},${cardRect.height.toFixed(0)})`);
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1230
|
+
foreignObjectRendering: false
|
|
1352
1231
|
});
|
|
1353
1232
|
} finally {
|
|
1354
|
-
|
|
1355
|
-
|
|
1233
|
+
restoreBackground();
|
|
1234
|
+
captureClone.cleanup();
|
|
1356
1235
|
}
|
|
1357
1236
|
const ctx = canvas.getContext("2d");
|
|
1358
1237
|
if (ctx) {
|
|
@@ -1415,42 +1294,34 @@ async function captureElementBitmap(element, options) {
|
|
|
1415
1294
|
if (options?.waitForImages !== false) {
|
|
1416
1295
|
await waitForContent(element, 500);
|
|
1417
1296
|
}
|
|
1418
|
-
const restoreBackground = injectCaptureBackground(
|
|
1419
|
-
element,
|
|
1420
|
-
options?.backgroundColor || DEFAULT_CAPTURE_BACKGROUND
|
|
1421
|
-
);
|
|
1422
1297
|
let result = null;
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
return result;
|
|
1437
|
-
}
|
|
1298
|
+
const html2canvas = await loadHtml2Canvas();
|
|
1299
|
+
if (html2canvas) {
|
|
1300
|
+
console.log("[WebSpatial] Using html2canvas (primary)");
|
|
1301
|
+
result = await captureWithHtml2Canvas(
|
|
1302
|
+
html2canvas,
|
|
1303
|
+
element,
|
|
1304
|
+
scale,
|
|
1305
|
+
options?.backgroundColor ?? DEFAULT_CAPTURE_BACKGROUND
|
|
1306
|
+
);
|
|
1307
|
+
if (result) {
|
|
1308
|
+
const elapsed = Math.round(performance.now() - startTime);
|
|
1309
|
+
console.log(`[WebSpatial] Capture complete (html2canvas, ${elapsed}ms)`);
|
|
1310
|
+
return result;
|
|
1438
1311
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1312
|
+
}
|
|
1313
|
+
const snapdom = await loadSnapdom();
|
|
1314
|
+
if (snapdom) {
|
|
1315
|
+
console.log("[WebSpatial] Falling back to snapdom");
|
|
1316
|
+
result = await captureWithSnapdom(snapdom, element, scale);
|
|
1317
|
+
if (result) {
|
|
1318
|
+
const elapsed = Math.round(performance.now() - startTime);
|
|
1319
|
+
console.log(`[WebSpatial] Capture complete (snapdom, ${elapsed}ms)`);
|
|
1320
|
+
return result;
|
|
1448
1321
|
}
|
|
1449
|
-
console.error("[WebSpatial] No capture library available");
|
|
1450
|
-
return null;
|
|
1451
|
-
} finally {
|
|
1452
|
-
restoreBackground();
|
|
1453
1322
|
}
|
|
1323
|
+
console.error("[WebSpatial] No capture library available");
|
|
1324
|
+
return null;
|
|
1454
1325
|
}
|
|
1455
1326
|
function observeContentChanges(element, onContentChange) {
|
|
1456
1327
|
const observer = new MutationObserver((_mutations) => {
|