senangwebs-tour 1.0.2 → 1.0.3
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/README.md +190 -217
- package/dist/swt-editor.css +759 -730
- package/dist/swt-editor.css.map +1 -1
- package/dist/swt-editor.js +550 -602
- package/dist/swt-editor.js.map +1 -1
- package/dist/swt-editor.min.css +1 -1
- package/dist/swt-editor.min.js +1 -1
- package/dist/swt.js +56531 -867
- package/dist/swt.js.map +1 -1
- package/dist/swt.min.js +12 -1
- package/package.json +6 -3
- package/src/editor/css/main.css +610 -581
- package/src/editor/js/export-manager.js +175 -262
- package/src/editor/js/preview-controller.js +36 -17
- package/src/editor/js/ui-controller.js +377 -362
- package/src/index.js +45 -33
package/dist/swt-editor.js
CHANGED
|
@@ -913,7 +913,7 @@
|
|
|
913
913
|
console.error("Error removing scene:", error);
|
|
914
914
|
}
|
|
915
915
|
}
|
|
916
|
-
|
|
916
|
+
|
|
917
917
|
// Clear any remaining children (loading overlays, empty state, etc)
|
|
918
918
|
while (this.previewContainer.firstChild) {
|
|
919
919
|
this.previewContainer.removeChild(this.previewContainer.firstChild);
|
|
@@ -921,9 +921,9 @@
|
|
|
921
921
|
} else {
|
|
922
922
|
// First load - only remove non-A-Frame elements (like empty state divs)
|
|
923
923
|
const children = Array.from(this.previewContainer.children);
|
|
924
|
-
children.forEach(child => {
|
|
924
|
+
children.forEach((child) => {
|
|
925
925
|
// Only remove if it's NOT an a-scene (shouldn't be any, but be safe)
|
|
926
|
-
if (child.tagName.toLowerCase() !==
|
|
926
|
+
if (child.tagName.toLowerCase() !== "a-scene") {
|
|
927
927
|
this.previewContainer.removeChild(child);
|
|
928
928
|
}
|
|
929
929
|
});
|
|
@@ -1132,6 +1132,7 @@
|
|
|
1132
1132
|
|
|
1133
1133
|
/**
|
|
1134
1134
|
* Get current camera rotation
|
|
1135
|
+
* Works with A-Frame's look-controls component by reading its internal state
|
|
1135
1136
|
*/
|
|
1136
1137
|
getCameraRotation() {
|
|
1137
1138
|
const aframeScene = this.previewContainer?.querySelector("a-scene");
|
|
@@ -1144,7 +1145,18 @@
|
|
|
1144
1145
|
return null;
|
|
1145
1146
|
}
|
|
1146
1147
|
|
|
1147
|
-
//
|
|
1148
|
+
// Try to get rotation from look-controls internal state (more reliable)
|
|
1149
|
+
const lookControls = camera.components?.["look-controls"];
|
|
1150
|
+
if (lookControls && lookControls.pitchObject && lookControls.yawObject) {
|
|
1151
|
+
const savedRotation = {
|
|
1152
|
+
x: lookControls.pitchObject.rotation.x,
|
|
1153
|
+
y: lookControls.yawObject.rotation.y,
|
|
1154
|
+
z: 0,
|
|
1155
|
+
};
|
|
1156
|
+
return savedRotation;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Fallback to object3D rotation
|
|
1148
1160
|
const rotation = camera.object3D.rotation;
|
|
1149
1161
|
const savedRotation = {
|
|
1150
1162
|
x: rotation.x,
|
|
@@ -1156,6 +1168,7 @@
|
|
|
1156
1168
|
|
|
1157
1169
|
/**
|
|
1158
1170
|
* Set camera rotation
|
|
1171
|
+
* Works with A-Frame's look-controls component by setting its internal state
|
|
1159
1172
|
*/
|
|
1160
1173
|
setCameraRotation(rotation) {
|
|
1161
1174
|
if (!rotation) {
|
|
@@ -1172,17 +1185,30 @@
|
|
|
1172
1185
|
return;
|
|
1173
1186
|
}
|
|
1174
1187
|
|
|
1175
|
-
// Set rotation
|
|
1188
|
+
// Set rotation via look-controls internal pitchObject/yawObject
|
|
1189
|
+
// This is required because look-controls overrides object3D.rotation on each tick
|
|
1176
1190
|
const setRotation = () => {
|
|
1177
|
-
|
|
1178
|
-
|
|
1191
|
+
const cam = this.previewContainer?.querySelector("[camera]");
|
|
1192
|
+
if (!cam) return;
|
|
1193
|
+
|
|
1194
|
+
const lookControls = cam.components?.["look-controls"];
|
|
1195
|
+
if (lookControls && lookControls.pitchObject && lookControls.yawObject) {
|
|
1196
|
+
// Set pitch (x rotation) on pitchObject
|
|
1197
|
+
lookControls.pitchObject.rotation.x = rotation.x;
|
|
1198
|
+
// Set yaw (y rotation) on yawObject
|
|
1199
|
+
lookControls.yawObject.rotation.y = rotation.y;
|
|
1200
|
+
} else if (cam.object3D) {
|
|
1201
|
+
// Fallback to direct object3D if look-controls not ready
|
|
1202
|
+
cam.object3D.rotation.set(rotation.x, rotation.y, rotation.z);
|
|
1179
1203
|
}
|
|
1180
1204
|
};
|
|
1181
1205
|
|
|
1182
|
-
// Try immediately and also after
|
|
1206
|
+
// Try immediately and also after delays to ensure it sticks
|
|
1207
|
+
// A-Frame look-controls may not be fully initialized immediately
|
|
1183
1208
|
setRotation();
|
|
1184
1209
|
setTimeout(setRotation, 100);
|
|
1185
1210
|
setTimeout(setRotation, 300);
|
|
1211
|
+
setTimeout(setRotation, 500);
|
|
1186
1212
|
}
|
|
1187
1213
|
|
|
1188
1214
|
/**
|
|
@@ -1191,15 +1217,8 @@
|
|
|
1191
1217
|
async refresh() {
|
|
1192
1218
|
const scene = this.editor.sceneManager.getCurrentScene();
|
|
1193
1219
|
if (scene) {
|
|
1194
|
-
//
|
|
1195
|
-
|
|
1196
|
-
// Reload scene
|
|
1197
|
-
await this.loadScene(scene);
|
|
1198
|
-
|
|
1199
|
-
// Restore camera rotation
|
|
1200
|
-
if (savedRotation) {
|
|
1201
|
-
this.setCameraRotation(savedRotation);
|
|
1202
|
-
}
|
|
1220
|
+
// loadScene with preserveCameraRotation=true handles save/restore internally
|
|
1221
|
+
await this.loadScene(scene, true);
|
|
1203
1222
|
}
|
|
1204
1223
|
}
|
|
1205
1224
|
|
|
@@ -1375,670 +1394,599 @@
|
|
|
1375
1394
|
// UI Controller - Handles DOM manipulation and rendering
|
|
1376
1395
|
|
|
1377
1396
|
let UIController$1 = class UIController {
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1397
|
+
constructor(editor) {
|
|
1398
|
+
this.editor = editor;
|
|
1399
|
+
this.sceneList = document.getElementById("sceneList");
|
|
1400
|
+
this.hotspotList = document.getElementById("hotspotList");
|
|
1401
|
+
this.draggedElement = null;
|
|
1402
|
+
}
|
|
1384
1403
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1404
|
+
/**
|
|
1405
|
+
* Render scene list
|
|
1406
|
+
*/
|
|
1407
|
+
renderSceneList() {
|
|
1408
|
+
if (!this.sceneList) return;
|
|
1390
1409
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1410
|
+
this.sceneList.innerHTML = "";
|
|
1411
|
+
const scenes = this.editor.sceneManager.getAllScenes();
|
|
1412
|
+
const currentIndex = this.editor.sceneManager.currentSceneIndex;
|
|
1394
1413
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1414
|
+
if (scenes.length === 0) {
|
|
1415
|
+
const empty = document.createElement("div");
|
|
1416
|
+
empty.className = "empty-state";
|
|
1417
|
+
empty.innerHTML = `
|
|
1399
1418
|
<p>No scenes yet</p>
|
|
1400
1419
|
<p class="hint">Click "Add Scene" to upload a 360° panorama</p>
|
|
1401
1420
|
`;
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
scenes.forEach((scene, index) => {
|
|
1407
|
-
const card = this.createSceneCard(scene, index, index === currentIndex);
|
|
1408
|
-
this.sceneList.appendChild(card);
|
|
1409
|
-
});
|
|
1421
|
+
this.sceneList.appendChild(empty);
|
|
1422
|
+
return;
|
|
1410
1423
|
}
|
|
1411
1424
|
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
card.className = 'scene-card' + (isActive ? ' active' : '');
|
|
1418
|
-
card.draggable = true;
|
|
1419
|
-
card.dataset.index = index;
|
|
1420
|
-
|
|
1421
|
-
// Drag handle
|
|
1422
|
-
const dragHandle = document.createElement('div');
|
|
1423
|
-
dragHandle.className = 'drag-handle';
|
|
1424
|
-
dragHandle.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free v6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M0 96C0 78.3 14.3 64 32 64l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 128C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32l384 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 288c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32L32 448c-17.7 0-32-14.3-32-32s14.3-32 32-32l384 0c17.7 0 32 14.3 32 32z"/></svg>';
|
|
1425
|
-
|
|
1426
|
-
// Thumbnail
|
|
1427
|
-
const thumbnail = document.createElement('img');
|
|
1428
|
-
thumbnail.src = scene.thumbnail || scene.imageUrl;
|
|
1429
|
-
thumbnail.alt = scene.name;
|
|
1430
|
-
|
|
1431
|
-
// Info
|
|
1432
|
-
const info = document.createElement('div');
|
|
1433
|
-
info.className = 'scene-info';
|
|
1434
|
-
|
|
1435
|
-
const name = document.createElement('div');
|
|
1436
|
-
name.className = 'scene-name';
|
|
1437
|
-
name.textContent = scene.name;
|
|
1438
|
-
|
|
1439
|
-
const meta = document.createElement('div');
|
|
1440
|
-
meta.className = 'scene-meta';
|
|
1441
|
-
meta.textContent = `${scene.hotspots.length} hotspot${scene.hotspots.length !== 1 ? 's' : ''}`;
|
|
1442
|
-
|
|
1443
|
-
info.appendChild(name);
|
|
1444
|
-
info.appendChild(meta);
|
|
1445
|
-
|
|
1446
|
-
// Actions
|
|
1447
|
-
const actions = document.createElement('div');
|
|
1448
|
-
actions.className = 'scene-actions';
|
|
1449
|
-
|
|
1450
|
-
const deleteBtn = document.createElement('button');
|
|
1451
|
-
deleteBtn.className = 'btn-icon';
|
|
1452
|
-
deleteBtn.innerHTML = '🗑️';
|
|
1453
|
-
deleteBtn.title = 'Delete scene';
|
|
1454
|
-
deleteBtn.onclick = (e) => {
|
|
1455
|
-
e.stopPropagation();
|
|
1456
|
-
this.editor.removeScene(index);
|
|
1457
|
-
};
|
|
1425
|
+
scenes.forEach((scene, index) => {
|
|
1426
|
+
const card = this.createSceneCard(scene, index, index === currentIndex);
|
|
1427
|
+
this.sceneList.appendChild(card);
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1458
1430
|
|
|
1459
|
-
|
|
1431
|
+
/**
|
|
1432
|
+
* Create scene card element
|
|
1433
|
+
*/
|
|
1434
|
+
createSceneCard(scene, index, isActive) {
|
|
1435
|
+
const card = document.createElement("div");
|
|
1436
|
+
card.className = "scene-card" + (isActive ? " active" : "");
|
|
1437
|
+
card.draggable = true;
|
|
1438
|
+
card.dataset.index = index;
|
|
1439
|
+
|
|
1440
|
+
// Drag handle
|
|
1441
|
+
const dragHandle = document.createElement("div");
|
|
1442
|
+
dragHandle.className = "drag-handle";
|
|
1443
|
+
dragHandle.innerHTML =
|
|
1444
|
+
'<ss-icon icon="arrow-up-down-left-right" thickness="2.2"></ss-icon>';
|
|
1445
|
+
|
|
1446
|
+
// Thumbnail
|
|
1447
|
+
const thumbnail = document.createElement("img");
|
|
1448
|
+
thumbnail.src = scene.thumbnail || scene.imageUrl;
|
|
1449
|
+
thumbnail.alt = scene.name;
|
|
1450
|
+
|
|
1451
|
+
// Info
|
|
1452
|
+
const info = document.createElement("div");
|
|
1453
|
+
info.className = "scene-info";
|
|
1454
|
+
|
|
1455
|
+
const name = document.createElement("div");
|
|
1456
|
+
name.className = "scene-name";
|
|
1457
|
+
name.textContent = scene.name;
|
|
1458
|
+
|
|
1459
|
+
const meta = document.createElement("div");
|
|
1460
|
+
meta.className = "scene-meta";
|
|
1461
|
+
meta.textContent = `${scene.hotspots.length} hotspot${
|
|
1462
|
+
scene.hotspots.length !== 1 ? "s" : ""
|
|
1463
|
+
}`;
|
|
1464
|
+
|
|
1465
|
+
info.appendChild(name);
|
|
1466
|
+
info.appendChild(meta);
|
|
1467
|
+
|
|
1468
|
+
// Actions
|
|
1469
|
+
const actions = document.createElement("div");
|
|
1470
|
+
actions.className = "scene-actions";
|
|
1471
|
+
|
|
1472
|
+
const deleteBtn = document.createElement("button");
|
|
1473
|
+
deleteBtn.className = "btn-icon";
|
|
1474
|
+
deleteBtn.innerHTML = '<ss-icon icon="trash" thickness="2.2"></ss-icon>';
|
|
1475
|
+
deleteBtn.title = "Delete scene";
|
|
1476
|
+
deleteBtn.onclick = (e) => {
|
|
1477
|
+
e.stopPropagation();
|
|
1478
|
+
this.editor.removeScene(index);
|
|
1479
|
+
};
|
|
1460
1480
|
|
|
1461
|
-
|
|
1462
|
-
card.appendChild(thumbnail);
|
|
1463
|
-
card.appendChild(info);
|
|
1464
|
-
card.appendChild(actions);
|
|
1481
|
+
actions.appendChild(deleteBtn);
|
|
1465
1482
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1483
|
+
card.appendChild(dragHandle);
|
|
1484
|
+
card.appendChild(thumbnail);
|
|
1485
|
+
card.appendChild(info);
|
|
1486
|
+
card.appendChild(actions);
|
|
1470
1487
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1488
|
+
// Click handler
|
|
1489
|
+
card.onclick = () => {
|
|
1490
|
+
this.editor.selectScene(index);
|
|
1491
|
+
};
|
|
1473
1492
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1493
|
+
// Drag and drop handlers
|
|
1494
|
+
this.setupDragAndDrop(card);
|
|
1476
1495
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
*/
|
|
1480
|
-
setupDragAndDrop(card) {
|
|
1481
|
-
card.addEventListener('dragstart', (e) => {
|
|
1482
|
-
this.draggedElement = card;
|
|
1483
|
-
card.classList.add('dragging');
|
|
1484
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
1485
|
-
});
|
|
1496
|
+
return card;
|
|
1497
|
+
}
|
|
1486
1498
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1499
|
+
/**
|
|
1500
|
+
* Setup drag and drop for scene reordering
|
|
1501
|
+
*/
|
|
1502
|
+
setupDragAndDrop(card) {
|
|
1503
|
+
card.addEventListener("dragstart", (e) => {
|
|
1504
|
+
this.draggedElement = card;
|
|
1505
|
+
card.classList.add("dragging");
|
|
1506
|
+
e.dataTransfer.effectAllowed = "move";
|
|
1507
|
+
});
|
|
1491
1508
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1509
|
+
card.addEventListener("dragend", () => {
|
|
1510
|
+
card.classList.remove("dragging");
|
|
1511
|
+
this.draggedElement = null;
|
|
1512
|
+
});
|
|
1495
1513
|
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1514
|
+
card.addEventListener("dragover", (e) => {
|
|
1515
|
+
e.preventDefault();
|
|
1516
|
+
e.dataTransfer.dropEffect = "move";
|
|
1499
1517
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
} else {
|
|
1504
|
-
card.style.borderTop = '2px solid var(--accent-color)';
|
|
1505
|
-
card.style.borderBottom = '';
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
});
|
|
1518
|
+
if (this.draggedElement && this.draggedElement !== card) {
|
|
1519
|
+
const bounding = card.getBoundingClientRect();
|
|
1520
|
+
const offset = bounding.y + bounding.height / 2;
|
|
1509
1521
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
}
|
|
1522
|
+
if (e.clientY - offset > 0) {
|
|
1523
|
+
card.style.borderBottom = "2px solid var(--accent-color)";
|
|
1524
|
+
card.style.borderTop = "";
|
|
1525
|
+
} else {
|
|
1526
|
+
card.style.borderTop = "2px solid var(--accent-color)";
|
|
1527
|
+
card.style.borderBottom = "";
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1514
1531
|
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1532
|
+
card.addEventListener("dragleave", () => {
|
|
1533
|
+
card.style.borderTop = "";
|
|
1534
|
+
card.style.borderBottom = "";
|
|
1535
|
+
});
|
|
1519
1536
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1537
|
+
card.addEventListener("drop", (e) => {
|
|
1538
|
+
e.preventDefault();
|
|
1539
|
+
card.style.borderTop = "";
|
|
1540
|
+
card.style.borderBottom = "";
|
|
1541
|
+
|
|
1542
|
+
if (this.draggedElement && this.draggedElement !== card) {
|
|
1543
|
+
const fromIndex = parseInt(this.draggedElement.dataset.index);
|
|
1544
|
+
const toIndex = parseInt(card.dataset.index);
|
|
1545
|
+
this.editor.reorderScenes(fromIndex, toIndex);
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Render hotspot list
|
|
1552
|
+
*/
|
|
1553
|
+
renderHotspotList() {
|
|
1554
|
+
if (!this.hotspotList) return;
|
|
1555
|
+
|
|
1556
|
+
this.hotspotList.innerHTML = "";
|
|
1557
|
+
const hotspots = this.editor.hotspotEditor.getAllHotspots();
|
|
1558
|
+
const currentIndex = this.editor.hotspotEditor.currentHotspotIndex;
|
|
1559
|
+
|
|
1560
|
+
if (hotspots.length === 0) {
|
|
1561
|
+
const empty = document.createElement("div");
|
|
1562
|
+
empty.className = "empty-state";
|
|
1563
|
+
empty.textContent = 'No hotspots. Click "Add Hotspot" to create one.';
|
|
1564
|
+
this.hotspotList.appendChild(empty);
|
|
1565
|
+
return;
|
|
1526
1566
|
}
|
|
1527
1567
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
if (hotspots.length === 0) {
|
|
1539
|
-
const empty = document.createElement('div');
|
|
1540
|
-
empty.className = 'empty-state';
|
|
1541
|
-
empty.textContent = 'No hotspots. Click "Add Hotspot" to create one.';
|
|
1542
|
-
this.hotspotList.appendChild(empty);
|
|
1543
|
-
return;
|
|
1544
|
-
}
|
|
1568
|
+
hotspots.forEach((hotspot, index) => {
|
|
1569
|
+
const item = this.createHotspotItem(
|
|
1570
|
+
hotspot,
|
|
1571
|
+
index,
|
|
1572
|
+
index === currentIndex
|
|
1573
|
+
);
|
|
1574
|
+
this.hotspotList.appendChild(item);
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1545
1577
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1578
|
+
/**
|
|
1579
|
+
* Create hotspot list item
|
|
1580
|
+
*/
|
|
1581
|
+
createHotspotItem(hotspot, index, isActive) {
|
|
1582
|
+
const item = document.createElement("div");
|
|
1583
|
+
item.className = "hotspot-item" + (isActive ? " active" : "");
|
|
1584
|
+
|
|
1585
|
+
const color = document.createElement("div");
|
|
1586
|
+
color.className = "hotspot-color";
|
|
1587
|
+
color.style.backgroundColor = hotspot.color;
|
|
1588
|
+
|
|
1589
|
+
const info = document.createElement("div");
|
|
1590
|
+
info.className = "hotspot-info";
|
|
1591
|
+
|
|
1592
|
+
const title = document.createElement("div");
|
|
1593
|
+
title.className = "hotspot-title";
|
|
1594
|
+
title.textContent = hotspot.title || "Untitled Hotspot";
|
|
1595
|
+
|
|
1596
|
+
const target = document.createElement("div");
|
|
1597
|
+
target.className = "hotspot-target";
|
|
1598
|
+
if (hotspot.targetSceneId) {
|
|
1599
|
+
const targetScene = this.editor.sceneManager.getSceneById(
|
|
1600
|
+
hotspot.targetSceneId
|
|
1601
|
+
);
|
|
1602
|
+
target.textContent = targetScene
|
|
1603
|
+
? `→ ${targetScene.name}`
|
|
1604
|
+
: `→ ${hotspot.targetSceneId}`;
|
|
1605
|
+
} else {
|
|
1606
|
+
target.textContent = "No target";
|
|
1550
1607
|
}
|
|
1551
1608
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
*/
|
|
1555
|
-
createHotspotItem(hotspot, index, isActive) {
|
|
1556
|
-
const item = document.createElement('div');
|
|
1557
|
-
item.className = 'hotspot-item' + (isActive ? ' active' : '');
|
|
1609
|
+
info.appendChild(title);
|
|
1610
|
+
info.appendChild(target);
|
|
1558
1611
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
color.style.backgroundColor = hotspot.color;
|
|
1612
|
+
const actions = document.createElement("div");
|
|
1613
|
+
actions.className = "hotspot-actions";
|
|
1562
1614
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
target.className = 'hotspot-target';
|
|
1572
|
-
if (hotspot.targetSceneId) {
|
|
1573
|
-
const targetScene = this.editor.sceneManager.getSceneById(hotspot.targetSceneId);
|
|
1574
|
-
target.textContent = targetScene ? `→ ${targetScene.name}` : `→ ${hotspot.targetSceneId}`;
|
|
1575
|
-
} else {
|
|
1576
|
-
target.textContent = 'No target';
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
info.appendChild(title);
|
|
1580
|
-
info.appendChild(target);
|
|
1581
|
-
|
|
1582
|
-
const actions = document.createElement('div');
|
|
1583
|
-
actions.className = 'hotspot-actions';
|
|
1584
|
-
|
|
1585
|
-
const deleteBtn = document.createElement('button');
|
|
1586
|
-
deleteBtn.className = 'btn-delete';
|
|
1587
|
-
deleteBtn.innerHTML = '🗑️';
|
|
1588
|
-
deleteBtn.title = 'Delete';
|
|
1589
|
-
deleteBtn.onclick = (e) => {
|
|
1590
|
-
e.stopPropagation();
|
|
1591
|
-
this.editor.removeHotspot(index);
|
|
1592
|
-
};
|
|
1615
|
+
const deleteBtn = document.createElement("button");
|
|
1616
|
+
deleteBtn.className = "btn-delete";
|
|
1617
|
+
deleteBtn.innerHTML = '<ss-icon icon="trash" thickness="2.2"></ss-icon>';
|
|
1618
|
+
deleteBtn.title = "Delete";
|
|
1619
|
+
deleteBtn.onclick = (e) => {
|
|
1620
|
+
e.stopPropagation();
|
|
1621
|
+
this.editor.removeHotspot(index);
|
|
1622
|
+
};
|
|
1593
1623
|
|
|
1594
|
-
|
|
1624
|
+
actions.appendChild(deleteBtn);
|
|
1595
1625
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1626
|
+
item.appendChild(color);
|
|
1627
|
+
item.appendChild(info);
|
|
1628
|
+
item.appendChild(actions);
|
|
1599
1629
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1630
|
+
item.onclick = () => {
|
|
1631
|
+
this.editor.selectHotspot(index);
|
|
1632
|
+
};
|
|
1603
1633
|
|
|
1604
|
-
|
|
1605
|
-
|
|
1634
|
+
return item;
|
|
1635
|
+
}
|
|
1606
1636
|
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1637
|
+
/**
|
|
1638
|
+
* Update properties panel for hotspot
|
|
1639
|
+
*/
|
|
1640
|
+
updateHotspotProperties(hotspot) {
|
|
1641
|
+
const hotspotAll = document.getElementById("hotspotAll");
|
|
1642
|
+
const hotspotProperties = document.getElementById("hotspotProperties");
|
|
1643
|
+
|
|
1644
|
+
if (!hotspot) {
|
|
1645
|
+
// No hotspot selected - show list, hide properties
|
|
1646
|
+
if (hotspotAll) hotspotAll.style.display = "block";
|
|
1647
|
+
if (hotspotProperties) hotspotProperties.style.display = "none";
|
|
1648
|
+
|
|
1649
|
+
// Clear form
|
|
1650
|
+
document.getElementById("hotspotTitle").value = "";
|
|
1651
|
+
document.getElementById("hotspotDescription").value = "";
|
|
1652
|
+
document.getElementById("hotspotTarget").value = "";
|
|
1653
|
+
document.getElementById("hotspotColor").value = "#00ff00";
|
|
1654
|
+
const colorText = document.getElementById("hotspotColorText");
|
|
1655
|
+
if (colorText) colorText.value = "#00ff00";
|
|
1656
|
+
document.getElementById("hotspotPosX").value = "";
|
|
1657
|
+
document.getElementById("hotspotPosY").value = "";
|
|
1658
|
+
document.getElementById("hotspotPosZ").value = "";
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1631
1661
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1662
|
+
// Hotspot selected - show both list and properties
|
|
1663
|
+
if (hotspotAll) hotspotAll.style.display = "block";
|
|
1664
|
+
if (hotspotProperties) hotspotProperties.style.display = "block";
|
|
1635
1665
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
const colorText = document.getElementById('hotspotColorText');
|
|
1643
|
-
if (colorText) {
|
|
1644
|
-
colorText.value = hotspot.color || '#00ff00';
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
// Update position inputs
|
|
1648
|
-
const pos = hotspot.position || { x: 0, y: 0, z: 0 };
|
|
1649
|
-
document.getElementById('hotspotPosX').value = pos.x;
|
|
1650
|
-
document.getElementById('hotspotPosY').value = pos.y;
|
|
1651
|
-
document.getElementById('hotspotPosZ').value = pos.z;
|
|
1666
|
+
document.getElementById("hotspotTitle").value = hotspot.title || "";
|
|
1667
|
+
document.getElementById("hotspotDescription").value =
|
|
1668
|
+
hotspot.description || "";
|
|
1669
|
+
document.getElementById("hotspotTarget").value =
|
|
1670
|
+
hotspot.targetSceneId || "";
|
|
1671
|
+
document.getElementById("hotspotColor").value = hotspot.color || "#00ff00";
|
|
1652
1672
|
|
|
1653
|
-
|
|
1654
|
-
|
|
1673
|
+
// Update color text input if it exists
|
|
1674
|
+
const colorText = document.getElementById("hotspotColorText");
|
|
1675
|
+
if (colorText) {
|
|
1676
|
+
colorText.value = hotspot.color || "#00ff00";
|
|
1655
1677
|
}
|
|
1656
1678
|
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
document.getElementById('sceneId').value = '';
|
|
1663
|
-
document.getElementById('sceneName').value = '';
|
|
1664
|
-
document.getElementById('sceneImageUrl').value = '';
|
|
1665
|
-
return;
|
|
1666
|
-
}
|
|
1679
|
+
// Update position inputs
|
|
1680
|
+
const pos = hotspot.position || { x: 0, y: 0, z: 0 };
|
|
1681
|
+
document.getElementById("hotspotPosX").value = pos.x;
|
|
1682
|
+
document.getElementById("hotspotPosY").value = pos.y;
|
|
1683
|
+
document.getElementById("hotspotPosZ").value = pos.z;
|
|
1667
1684
|
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1685
|
+
// Update target dropdown
|
|
1686
|
+
this.updateTargetSceneOptions();
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Update properties panel for scene
|
|
1691
|
+
*/
|
|
1692
|
+
updateSceneProperties(scene) {
|
|
1693
|
+
if (!scene) {
|
|
1694
|
+
document.getElementById("sceneId").value = "";
|
|
1695
|
+
document.getElementById("sceneName").value = "";
|
|
1696
|
+
document.getElementById("sceneImageUrl").value = "";
|
|
1697
|
+
return;
|
|
1671
1698
|
}
|
|
1672
1699
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1700
|
+
document.getElementById("sceneId").value = scene.id || "";
|
|
1701
|
+
document.getElementById("sceneName").value = scene.name || "";
|
|
1702
|
+
document.getElementById("sceneImageUrl").value = scene.imageUrl || "";
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* Update properties panel for tour
|
|
1707
|
+
*/
|
|
1708
|
+
updateTourProperties(config) {
|
|
1709
|
+
document.getElementById("tourTitle").value = config.title || "";
|
|
1710
|
+
document.getElementById("tourDescription").value = config.description || "";
|
|
1711
|
+
document.getElementById("tourInitialScene").value =
|
|
1712
|
+
config.initialSceneId || "";
|
|
1713
|
+
document.getElementById("tourAutoRotate").checked =
|
|
1714
|
+
config.autoRotate || false;
|
|
1715
|
+
document.getElementById("tourShowCompass").checked =
|
|
1716
|
+
config.showCompass || false;
|
|
1717
|
+
|
|
1718
|
+
// Also update project name in header if it exists
|
|
1719
|
+
const projectName = document.getElementById("project-name");
|
|
1720
|
+
if (projectName) {
|
|
1721
|
+
projectName.value = config.title || "";
|
|
1688
1722
|
}
|
|
1723
|
+
}
|
|
1689
1724
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1725
|
+
/**
|
|
1726
|
+
* Update target scene options in hotspot properties
|
|
1727
|
+
*/
|
|
1728
|
+
updateTargetSceneOptions() {
|
|
1729
|
+
const select = document.getElementById("hotspotTarget");
|
|
1730
|
+
if (!select) return;
|
|
1696
1731
|
|
|
1697
|
-
|
|
1698
|
-
|
|
1732
|
+
const scenes = this.editor.sceneManager.getAllScenes();
|
|
1733
|
+
const currentValue = select.value;
|
|
1699
1734
|
|
|
1700
|
-
|
|
1735
|
+
select.innerHTML = '<option value="">Select target scene...</option>';
|
|
1701
1736
|
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1737
|
+
scenes.forEach((scene) => {
|
|
1738
|
+
const option = document.createElement("option");
|
|
1739
|
+
option.value = scene.id;
|
|
1740
|
+
option.textContent = scene.name;
|
|
1741
|
+
select.appendChild(option);
|
|
1742
|
+
});
|
|
1708
1743
|
|
|
1709
|
-
|
|
1710
|
-
|
|
1744
|
+
select.value = currentValue;
|
|
1745
|
+
}
|
|
1711
1746
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1747
|
+
/**
|
|
1748
|
+
* Update initial scene options in tour properties
|
|
1749
|
+
*/
|
|
1750
|
+
updateInitialSceneOptions() {
|
|
1751
|
+
const select = document.getElementById("tourInitialScene");
|
|
1752
|
+
if (!select) return;
|
|
1718
1753
|
|
|
1719
|
-
|
|
1720
|
-
|
|
1754
|
+
const scenes = this.editor.sceneManager.getAllScenes();
|
|
1755
|
+
const currentValue = select.value;
|
|
1721
1756
|
|
|
1722
|
-
|
|
1757
|
+
select.innerHTML = '<option value="">Select initial scene...</option>';
|
|
1723
1758
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1759
|
+
scenes.forEach((scene) => {
|
|
1760
|
+
const option = document.createElement("option");
|
|
1761
|
+
option.value = scene.id;
|
|
1762
|
+
option.textContent = scene.name;
|
|
1763
|
+
select.appendChild(option);
|
|
1764
|
+
});
|
|
1730
1765
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1766
|
+
select.value = currentValue;
|
|
1767
|
+
}
|
|
1733
1768
|
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Show/hide loading indicator
|
|
1771
|
+
*/
|
|
1772
|
+
setLoading(isLoading) {
|
|
1773
|
+
const indicator = document.querySelector(".loading-indicator");
|
|
1774
|
+
if (indicator) {
|
|
1775
|
+
indicator.style.display = isLoading ? "block" : "none";
|
|
1742
1776
|
}
|
|
1777
|
+
}
|
|
1743
1778
|
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1779
|
+
/**
|
|
1780
|
+
* Switch properties tab
|
|
1781
|
+
*/
|
|
1782
|
+
switchTab(tabName) {
|
|
1783
|
+
// Update tab buttons
|
|
1784
|
+
document.querySelectorAll(".tab-btn").forEach((btn) => {
|
|
1785
|
+
btn.classList.toggle("active", btn.dataset.tab === tabName);
|
|
1786
|
+
});
|
|
1752
1787
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1788
|
+
// Update tab content
|
|
1789
|
+
document.querySelectorAll(".tab-content").forEach((content) => {
|
|
1790
|
+
content.classList.toggle("active", content.id === tabName + "Tab");
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1758
1793
|
};
|
|
1759
1794
|
|
|
1760
1795
|
// Export Manager - Handles JSON generation for SWT library
|
|
1761
1796
|
|
|
1762
1797
|
let ExportManager$1 = class ExportManager {
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1798
|
+
constructor(editor) {
|
|
1799
|
+
this.editor = editor;
|
|
1800
|
+
}
|
|
1766
1801
|
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1802
|
+
/**
|
|
1803
|
+
* Generate JSON compatible with SWT library
|
|
1804
|
+
* Follows the tourConfig structure from example-simple.html
|
|
1805
|
+
*/
|
|
1806
|
+
generateJSON() {
|
|
1807
|
+
const scenes = this.editor.sceneManager.getAllScenes();
|
|
1808
|
+
const config = this.editor.config;
|
|
1809
|
+
|
|
1810
|
+
// Build scenes object (keyed by scene ID)
|
|
1811
|
+
const scenesData = {};
|
|
1812
|
+
scenes.forEach((scene) => {
|
|
1813
|
+
scenesData[scene.id] = {
|
|
1814
|
+
name: scene.name,
|
|
1815
|
+
panorama: scene.imageUrl,
|
|
1816
|
+
hotspots: scene.hotspots.map((hotspot) => {
|
|
1817
|
+
const hotspotData = {
|
|
1818
|
+
position: hotspot.position,
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1821
|
+
// Add action based on hotspot type
|
|
1822
|
+
if (hotspot.type === "navigation" && hotspot.targetSceneId) {
|
|
1823
|
+
hotspotData.action = {
|
|
1824
|
+
type: "navigateTo",
|
|
1825
|
+
target: hotspot.targetSceneId,
|
|
1826
|
+
};
|
|
1827
|
+
} else if (hotspot.type === "info") {
|
|
1828
|
+
hotspotData.action = {
|
|
1829
|
+
type: "showInfo",
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1794
1832
|
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1833
|
+
// Add appearance
|
|
1834
|
+
hotspotData.appearance = {
|
|
1835
|
+
color: hotspot.color || "#FF6B6B",
|
|
1836
|
+
scale: "2 2 2",
|
|
1837
|
+
};
|
|
1838
|
+
|
|
1839
|
+
// Add tooltip if title exists
|
|
1840
|
+
if (hotspot.title) {
|
|
1841
|
+
hotspotData.tooltip = {
|
|
1842
|
+
text: hotspot.title,
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
return hotspotData;
|
|
1847
|
+
}),
|
|
1848
|
+
};
|
|
1849
|
+
});
|
|
1806
1850
|
|
|
1807
|
-
|
|
1851
|
+
// Determine initial scene
|
|
1852
|
+
let initialScene = config.initialSceneId;
|
|
1853
|
+
if (!initialScene && scenes.length > 0) {
|
|
1854
|
+
initialScene = scenes[0].id;
|
|
1808
1855
|
}
|
|
1809
1856
|
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1857
|
+
// Build final JSON matching SWT tourConfig format
|
|
1858
|
+
const jsonData = {
|
|
1859
|
+
initialScene: initialScene,
|
|
1860
|
+
scenes: scenesData,
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
return jsonData;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
/**
|
|
1867
|
+
* Export as JSON file
|
|
1868
|
+
*/
|
|
1869
|
+
exportJSON() {
|
|
1870
|
+
try {
|
|
1871
|
+
const jsonData = this.generateJSON();
|
|
1872
|
+
const json = JSON.stringify(jsonData, null, 2);
|
|
1873
|
+
|
|
1874
|
+
const title = this.editor.config.title || "tour";
|
|
1875
|
+
const filename = sanitizeId(title) + ".json";
|
|
1876
|
+
downloadTextAsFile(json, filename);
|
|
1877
|
+
|
|
1878
|
+
showToast("Tour exported successfully", "success");
|
|
1879
|
+
return true;
|
|
1880
|
+
} catch (error) {
|
|
1881
|
+
console.error("Export failed:", error);
|
|
1882
|
+
showToast("Export failed", "error");
|
|
1883
|
+
return false;
|
|
1828
1884
|
}
|
|
1885
|
+
}
|
|
1829
1886
|
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Copy JSON to clipboard
|
|
1889
|
+
*/
|
|
1890
|
+
async copyJSON() {
|
|
1891
|
+
try {
|
|
1892
|
+
const jsonData = this.generateJSON();
|
|
1893
|
+
const json = JSON.stringify(jsonData, null, 2);
|
|
1894
|
+
|
|
1895
|
+
const success = await copyToClipboard(json);
|
|
1896
|
+
if (success) {
|
|
1897
|
+
showToast("JSON copied to clipboard", "success");
|
|
1898
|
+
} else {
|
|
1899
|
+
showToast("Failed to copy to clipboard", "error");
|
|
1900
|
+
}
|
|
1901
|
+
return success;
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
console.error("Copy failed:", error);
|
|
1904
|
+
showToast("Copy failed", "error");
|
|
1905
|
+
return false;
|
|
1850
1906
|
}
|
|
1907
|
+
}
|
|
1851
1908
|
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1909
|
+
/**
|
|
1910
|
+
* Generate HTML viewer code
|
|
1911
|
+
*/
|
|
1912
|
+
generateViewerHTML() {
|
|
1913
|
+
const jsonData = this.generateJSON();
|
|
1914
|
+
const title = this.editor.config.title || "Virtual Tour";
|
|
1915
|
+
|
|
1916
|
+
return `<!DOCTYPE html>
|
|
1859
1917
|
<html lang="en">
|
|
1860
|
-
<head>
|
|
1861
|
-
<meta charset="UTF-8"
|
|
1862
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"
|
|
1863
|
-
<title>${
|
|
1864
|
-
<script src="https://
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
#loading {
|
|
1874
|
-
position: fixed;
|
|
1875
|
-
top: 0;
|
|
1876
|
-
left: 0;
|
|
1877
|
-
width: 100%;
|
|
1878
|
-
height: 100%;
|
|
1879
|
-
background: #000;
|
|
1880
|
-
display: flex;
|
|
1881
|
-
align-items: center;
|
|
1882
|
-
justify-content: center;
|
|
1883
|
-
color: #fff;
|
|
1884
|
-
z-index: 1000;
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
#loading.hidden {
|
|
1888
|
-
display: none;
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
.spinner {
|
|
1892
|
-
border: 4px solid rgba(255,255,255,0.3);
|
|
1893
|
-
border-top: 4px solid #fff;
|
|
1894
|
-
border-radius: 50%;
|
|
1895
|
-
width: 40px;
|
|
1896
|
-
height: 40px;
|
|
1897
|
-
animation: spin 1s linear infinite;
|
|
1898
|
-
margin-right: 15px;
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
@keyframes spin {
|
|
1902
|
-
0% { transform: rotate(0deg); }
|
|
1903
|
-
100% { transform: rotate(360deg); }
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
#ui {
|
|
1907
|
-
position: fixed;
|
|
1908
|
-
bottom: 20px;
|
|
1909
|
-
left: 50%;
|
|
1910
|
-
transform: translateX(-50%);
|
|
1911
|
-
z-index: 100;
|
|
1912
|
-
display: flex;
|
|
1913
|
-
gap: 10px;
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
.btn {
|
|
1917
|
-
background: rgba(0,0,0,0.7);
|
|
1918
|
-
color: #fff;
|
|
1919
|
-
border: none;
|
|
1920
|
-
padding: 10px 20px;
|
|
1921
|
-
border-radius: 5px;
|
|
1922
|
-
cursor: pointer;
|
|
1923
|
-
font-size: 14px;
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
.btn:hover {
|
|
1927
|
-
background: rgba(0,0,0,0.9);
|
|
1928
|
-
}
|
|
1929
|
-
</style>
|
|
1930
|
-
</head>
|
|
1931
|
-
<body>
|
|
1932
|
-
<div id="loading">
|
|
1933
|
-
<div class="spinner"></div>
|
|
1934
|
-
<span>Loading Tour...</span>
|
|
1935
|
-
</div>
|
|
1936
|
-
|
|
1937
|
-
<div id="tour-container"></div>
|
|
1938
|
-
|
|
1939
|
-
<div id="ui" style="display: none;">
|
|
1940
|
-
<button class="btn" id="resetBtn">Reset View</button>
|
|
1941
|
-
<span class="btn" id="sceneInfo"></span>
|
|
1942
|
-
</div>
|
|
1918
|
+
<head>
|
|
1919
|
+
<meta charset="UTF-8" />
|
|
1920
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1921
|
+
<title>${title}</title>
|
|
1922
|
+
<script src="https://unpkg.com/senangwebs-tour@latest/dist/swt.min.js"></script>
|
|
1923
|
+
</head>
|
|
1924
|
+
<body>
|
|
1925
|
+
<a-scene id="tour-container">
|
|
1926
|
+
<a-camera>
|
|
1927
|
+
<a-cursor></a-cursor>
|
|
1928
|
+
</a-camera>
|
|
1929
|
+
</a-scene>
|
|
1943
1930
|
|
|
1944
1931
|
<script>
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
tour.on('sceneChanged', (sceneId) => {
|
|
1958
|
-
updateSceneInfo();
|
|
1959
|
-
});
|
|
1960
|
-
|
|
1961
|
-
tour.on('ready', () => {
|
|
1962
|
-
document.getElementById('loading').classList.add('hidden');
|
|
1963
|
-
document.getElementById('ui').style.display = 'flex';
|
|
1964
|
-
updateSceneInfo();
|
|
1965
|
-
});
|
|
1966
|
-
|
|
1967
|
-
tour.on('error', (error) => {
|
|
1968
|
-
console.error('Tour error:', error);
|
|
1969
|
-
alert('Failed to load tour: ' + error.message);
|
|
1970
|
-
});
|
|
1971
|
-
|
|
1972
|
-
// Start tour
|
|
1973
|
-
await tour.start();
|
|
1974
|
-
|
|
1975
|
-
// Setup UI
|
|
1976
|
-
document.getElementById('resetBtn').addEventListener('click', () => {
|
|
1977
|
-
const camera = document.querySelector('[camera]');
|
|
1978
|
-
if (camera) {
|
|
1979
|
-
camera.setAttribute('rotation', '0 0 0');
|
|
1980
|
-
}
|
|
1981
|
-
});
|
|
1982
|
-
|
|
1983
|
-
} catch (error) {
|
|
1984
|
-
console.error('Failed to initialize tour:', error);
|
|
1985
|
-
alert('Failed to initialize tour: ' + error.message);
|
|
1986
|
-
}
|
|
1987
|
-
});
|
|
1988
|
-
|
|
1989
|
-
function updateSceneInfo() {
|
|
1990
|
-
const sceneId = tour.getCurrentSceneId();
|
|
1991
|
-
const scene = tourConfig.scenes.find(s => s.id === sceneId);
|
|
1992
|
-
if (scene) {
|
|
1993
|
-
document.getElementById('sceneInfo').textContent = scene.name;
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1932
|
+
// Tour configuration
|
|
1933
|
+
const tourConfig = ${JSON.stringify(jsonData, null, 8)
|
|
1934
|
+
.split("\n")
|
|
1935
|
+
.map((line, i) => (i === 0 ? line : " " + line))
|
|
1936
|
+
.join("\n")};
|
|
1937
|
+
|
|
1938
|
+
// Initialize tour when scene is loaded
|
|
1939
|
+
const sceneEl = document.querySelector("#tour-container");
|
|
1940
|
+
sceneEl.addEventListener("loaded", () => {
|
|
1941
|
+
const tour = new SWT.Tour(sceneEl, tourConfig);
|
|
1942
|
+
tour.start();
|
|
1943
|
+
});
|
|
1996
1944
|
</script>
|
|
1997
|
-
</body>
|
|
1945
|
+
</body>
|
|
1998
1946
|
</html>`;
|
|
1999
|
-
|
|
1947
|
+
}
|
|
2000
1948
|
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Export as standalone HTML viewer
|
|
1951
|
+
*/
|
|
1952
|
+
exportViewerHTML() {
|
|
1953
|
+
try {
|
|
1954
|
+
const html = this.generateViewerHTML();
|
|
1955
|
+
const title = this.editor.config.title || "tour";
|
|
1956
|
+
const filename = sanitizeId(title) + "-viewer.html";
|
|
1957
|
+
|
|
1958
|
+
downloadTextAsFile(html, filename);
|
|
1959
|
+
|
|
1960
|
+
showToast("Viewer HTML exported successfully", "success");
|
|
1961
|
+
return true;
|
|
1962
|
+
} catch (error) {
|
|
1963
|
+
console.error("Export viewer failed:", error);
|
|
1964
|
+
showToast("Export viewer failed", "error");
|
|
1965
|
+
return false;
|
|
2019
1966
|
}
|
|
1967
|
+
}
|
|
2020
1968
|
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Show export preview in modal
|
|
1971
|
+
*/
|
|
1972
|
+
showExportPreview() {
|
|
1973
|
+
try {
|
|
1974
|
+
const jsonData = this.generateJSON();
|
|
1975
|
+
const json = JSON.stringify(jsonData, null, 2);
|
|
1976
|
+
|
|
1977
|
+
const preview = document.getElementById("exportPreview");
|
|
1978
|
+
if (preview) {
|
|
1979
|
+
preview.textContent = json;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
showModal("exportModal");
|
|
1983
|
+
return true;
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
console.error("Failed to show export preview:", error);
|
|
1986
|
+
showToast("Failed to generate preview", "error");
|
|
1987
|
+
return false;
|
|
2041
1988
|
}
|
|
1989
|
+
}
|
|
2042
1990
|
};
|
|
2043
1991
|
|
|
2044
1992
|
// Main Editor Controller
|