diff --git a/assets/visualizer/index.html b/assets/visualizer/index.html
new file mode 100644
index 0000000..90dc36f
--- /dev/null
+++ b/assets/visualizer/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+ UI Dump Viewer
+
+
+
+
+
+
+
+
+
+
![screenshot]()
+
+
+
+
+
+
diff --git a/assets/visualizer/script.js b/assets/visualizer/script.js
new file mode 100644
index 0000000..c27511f
--- /dev/null
+++ b/assets/visualizer/script.js
@@ -0,0 +1,70 @@
+let dumpWidth = 1080;
+let dumpHeight = 1920;
+
+document.getElementById('xmlInput').addEventListener('change', handleXml);
+document.getElementById('imgInput').addEventListener('change', handleImage);
+
+function handleImage(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+
+ const url = URL.createObjectURL(file);
+ document.getElementById('screenshot').src = url;
+}
+
+async function handleXml(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+
+ const text = await file.text();
+ const parser = new DOMParser();
+ const xml = parser.parseFromString(text, "text/xml");
+
+ const nodes = Array.from(xml.querySelectorAll("node"));
+ const canvas = document.getElementById("canvas");
+ canvas.innerHTML = "";
+
+ // Автоопределение размеров дампа
+ let maxX = 0, maxY = 0;
+ for (const node of nodes) {
+ const bounds = node.getAttribute("bounds");
+ const match = bounds && bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
+ if (!match) continue;
+
+ const [, , , x2, y2] = match.map(Number);
+ if (x2 > maxX) maxX = x2;
+ if (y2 > maxY) maxY = y2;
+ }
+ dumpWidth = maxX;
+ dumpHeight = maxY;
+
+ // Масштабирование по высоте экрана с отступами
+ const padding = 100;
+ const displayHeight = window.innerHeight - padding;
+ const scale = displayHeight / dumpHeight;
+ const displayWidth = dumpWidth * scale;
+
+ // Установка размеров canvas
+ canvas.style.width = `${displayWidth}px`;
+ canvas.style.height = `${displayHeight}px`;
+
+ for (const node of nodes) {
+ const bounds = node.getAttribute("bounds");
+ const match = bounds && bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
+ if (!match) continue;
+
+ const [, x1, y1, x2, y2] = match.map(Number);
+
+ const div = document.createElement("div");
+ div.className = "rect";
+ div.style.left = `${x1 * scale}px`;
+ div.style.top = `${y1 * scale}px`;
+ div.style.width = `${(x2 - x1) * scale}px`;
+ div.style.height = `${(y2 - y1) * scale}px`;
+
+ const text = (node.getAttribute("text") || '').trim();
+ if (text) div.innerText = text;
+
+ canvas.appendChild(div);
+ }
+}
diff --git a/assets/visualizer/style.css b/assets/visualizer/style.css
new file mode 100644
index 0000000..492c800
--- /dev/null
+++ b/assets/visualizer/style.css
@@ -0,0 +1,72 @@
+body, html {
+ margin: 0;
+ height: 100%;
+ font-family: sans-serif;
+ background-color: #f4f4f4; /* вернули светлый фон */
+ color: #333;
+}
+
+#layout {
+ display: flex;
+ height: 100vh;
+}
+
+#sidebar {
+ width: 250px;
+ padding: 20px;
+ background: #ffffff; /* светлая боковая панель */
+ border-right: 1px solid #ccc;
+ box-sizing: border-box;
+}
+
+#sidebar h3, label {
+ color: #333;
+}
+
+#sidebar input[type="file"] {
+ background: #fff;
+ color: #000;
+ border: 1px solid #ccc;
+ padding: 6px;
+ margin-top: 4px;
+ width: 100%;
+}
+
+#content {
+ display: flex;
+ flex: 1;
+ align-items: center;
+ justify-content: space-around;
+ overflow: auto;
+ padding: 50px 20px;
+ box-sizing: border-box;
+ gap: 20px;
+ background-color: #f4f4f4;
+}
+
+#canvas {
+ position: relative;
+ background: #2a2a2a; /* серый фон области дампа */
+ border: 1px solid #444;
+ flex-shrink: 0;
+}
+
+#screenshot-container {
+ flex-shrink: 0;
+}
+
+#screenshot {
+ max-height: calc(100vh - 100px);
+ border: 1px solid #ccc;
+}
+
+.rect {
+ position: absolute;
+ border: 1px dashed #ff9800;
+ background: transparent;
+ color: #ff9800;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+