@blueharford/scrypted-spatial-awareness 0.5.8 → 0.6.0
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/dist/main.nodejs.js +1 -1
- package/dist/main.nodejs.js.map +1 -1
- package/dist/plugin.zip +0 -0
- package/out/main.nodejs.js +282 -10
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/core/topology-discovery.ts +2 -2
- package/src/main.ts +167 -5
- package/src/ui/editor-html.ts +139 -3
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/out/main.nodejs.js
CHANGED
|
@@ -36275,7 +36275,7 @@ class TopologyDiscoveryEngine {
|
|
|
36275
36275
|
],
|
|
36276
36276
|
},
|
|
36277
36277
|
],
|
|
36278
|
-
max_tokens:
|
|
36278
|
+
max_tokens: 1500,
|
|
36279
36279
|
temperature: 0.3,
|
|
36280
36280
|
});
|
|
36281
36281
|
const content = result?.choices?.[0]?.message?.content;
|
|
@@ -36484,7 +36484,7 @@ class TopologyDiscoveryEngine {
|
|
|
36484
36484
|
const prompt = CORRELATION_PROMPT.replace('{scenes}', scenesText);
|
|
36485
36485
|
const result = await llm.getChatCompletion({
|
|
36486
36486
|
messages: [{ role: 'user', content: prompt }],
|
|
36487
|
-
max_tokens:
|
|
36487
|
+
max_tokens: 2000,
|
|
36488
36488
|
temperature: 0.4,
|
|
36489
36489
|
});
|
|
36490
36490
|
const content = result?.choices?.[0]?.message?.content;
|
|
@@ -40224,12 +40224,38 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40224
40224
|
const topology = this.trackingEngine.getTopology();
|
|
40225
40225
|
let updated = false;
|
|
40226
40226
|
if (suggestion.type === 'landmark' && suggestion.landmark) {
|
|
40227
|
+
// Calculate a reasonable position for the landmark
|
|
40228
|
+
// Use the first visible camera's position as a starting point, or canvas center
|
|
40229
|
+
let position = suggestion.landmark.position;
|
|
40230
|
+
if (!position || (position.x === 0 && position.y === 0)) {
|
|
40231
|
+
// Find a camera that can see this landmark
|
|
40232
|
+
const visibleCameraId = suggestion.landmark.visibleFromCameras?.[0];
|
|
40233
|
+
const camera = visibleCameraId ? topology.cameras.find(c => c.deviceId === visibleCameraId) : null;
|
|
40234
|
+
if (camera?.floorPlanPosition) {
|
|
40235
|
+
// Position near the camera with some offset
|
|
40236
|
+
const offset = (topology.landmarks?.length || 0) * 30;
|
|
40237
|
+
position = {
|
|
40238
|
+
x: camera.floorPlanPosition.x + 50 + (offset % 100),
|
|
40239
|
+
y: camera.floorPlanPosition.y + 50 + Math.floor(offset / 100) * 30,
|
|
40240
|
+
};
|
|
40241
|
+
}
|
|
40242
|
+
else {
|
|
40243
|
+
// Position in a grid pattern starting from center
|
|
40244
|
+
const landmarkCount = topology.landmarks?.length || 0;
|
|
40245
|
+
const gridSize = 80;
|
|
40246
|
+
const cols = 5;
|
|
40247
|
+
position = {
|
|
40248
|
+
x: 200 + (landmarkCount % cols) * gridSize,
|
|
40249
|
+
y: 100 + Math.floor(landmarkCount / cols) * gridSize,
|
|
40250
|
+
};
|
|
40251
|
+
}
|
|
40252
|
+
}
|
|
40227
40253
|
// Add new landmark to topology
|
|
40228
40254
|
const landmark = {
|
|
40229
40255
|
id: `landmark_${Date.now()}`,
|
|
40230
40256
|
name: suggestion.landmark.name,
|
|
40231
40257
|
type: suggestion.landmark.type,
|
|
40232
|
-
position
|
|
40258
|
+
position,
|
|
40233
40259
|
description: suggestion.landmark.description,
|
|
40234
40260
|
visibleFromCameras: suggestion.landmark.visibleFromCameras,
|
|
40235
40261
|
aiSuggested: true,
|
|
@@ -40240,15 +40266,107 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40240
40266
|
}
|
|
40241
40267
|
topology.landmarks.push(landmark);
|
|
40242
40268
|
updated = true;
|
|
40243
|
-
this.console.log(`[Discovery] Added landmark: ${landmark.name}`);
|
|
40269
|
+
this.console.log(`[Discovery] Added landmark: ${landmark.name} at (${position.x}, ${position.y})`);
|
|
40270
|
+
}
|
|
40271
|
+
if (suggestion.type === 'zone' && suggestion.zone) {
|
|
40272
|
+
// Create a drawn zone from the discovery zone
|
|
40273
|
+
const zone = suggestion.zone;
|
|
40274
|
+
// Find cameras that see this zone
|
|
40275
|
+
const sourceCameras = suggestion.sourceCameras || [];
|
|
40276
|
+
const camera = sourceCameras[0]
|
|
40277
|
+
? topology.cameras.find(c => c.deviceId === sourceCameras[0] || c.name === sourceCameras[0])
|
|
40278
|
+
: null;
|
|
40279
|
+
// Create a default polygon near the camera or at a default location
|
|
40280
|
+
let centerX = 300;
|
|
40281
|
+
let centerY = 200;
|
|
40282
|
+
if (camera?.floorPlanPosition) {
|
|
40283
|
+
centerX = camera.floorPlanPosition.x;
|
|
40284
|
+
centerY = camera.floorPlanPosition.y + 80;
|
|
40285
|
+
}
|
|
40286
|
+
// Create a rectangular zone (user can edit later)
|
|
40287
|
+
const size = 100;
|
|
40288
|
+
const timestamp = Date.now();
|
|
40289
|
+
// 1. Create DrawnZone (visual on floor plan)
|
|
40290
|
+
const drawnZone = {
|
|
40291
|
+
id: `zone_${timestamp}`,
|
|
40292
|
+
name: zone.name,
|
|
40293
|
+
type: (zone.type || 'custom'),
|
|
40294
|
+
description: zone.description,
|
|
40295
|
+
polygon: [
|
|
40296
|
+
{ x: centerX - size / 2, y: centerY - size / 2 },
|
|
40297
|
+
{ x: centerX + size / 2, y: centerY - size / 2 },
|
|
40298
|
+
{ x: centerX + size / 2, y: centerY + size / 2 },
|
|
40299
|
+
{ x: centerX - size / 2, y: centerY + size / 2 },
|
|
40300
|
+
],
|
|
40301
|
+
linkedCameras: sourceCameras,
|
|
40302
|
+
};
|
|
40303
|
+
if (!topology.drawnZones) {
|
|
40304
|
+
topology.drawnZones = [];
|
|
40305
|
+
}
|
|
40306
|
+
topology.drawnZones.push(drawnZone);
|
|
40307
|
+
// 2. Create GlobalZone (for tracking/alerting)
|
|
40308
|
+
const globalZoneType = this.mapZoneTypeToGlobalType(zone.type);
|
|
40309
|
+
const cameraZones = sourceCameras
|
|
40310
|
+
.map(camRef => {
|
|
40311
|
+
const cam = topology.cameras.find(c => c.deviceId === camRef || c.name === camRef);
|
|
40312
|
+
if (!cam)
|
|
40313
|
+
return null;
|
|
40314
|
+
return {
|
|
40315
|
+
cameraId: cam.deviceId,
|
|
40316
|
+
zone: [[0, 0], [100, 0], [100, 100], [0, 100]], // Full frame default
|
|
40317
|
+
};
|
|
40318
|
+
})
|
|
40319
|
+
.filter((z) => z !== null);
|
|
40320
|
+
if (cameraZones.length > 0) {
|
|
40321
|
+
const globalZone = {
|
|
40322
|
+
id: `global_${timestamp}`,
|
|
40323
|
+
name: zone.name,
|
|
40324
|
+
type: globalZoneType,
|
|
40325
|
+
cameraZones,
|
|
40326
|
+
};
|
|
40327
|
+
if (!topology.globalZones) {
|
|
40328
|
+
topology.globalZones = [];
|
|
40329
|
+
}
|
|
40330
|
+
topology.globalZones.push(globalZone);
|
|
40331
|
+
this.console.log(`[Discovery] Added zone: ${zone.name} (${zone.type}) - DrawnZone and GlobalZone created`);
|
|
40332
|
+
}
|
|
40333
|
+
else {
|
|
40334
|
+
this.console.log(`[Discovery] Added zone: ${zone.name} (${zone.type}) - DrawnZone only (no cameras matched)`);
|
|
40335
|
+
}
|
|
40336
|
+
updated = true;
|
|
40244
40337
|
}
|
|
40245
40338
|
if (suggestion.type === 'connection' && suggestion.connection) {
|
|
40246
40339
|
// Add new connection to topology
|
|
40247
40340
|
const conn = suggestion.connection;
|
|
40341
|
+
// Resolve camera references - LLM may return names instead of deviceIds
|
|
40342
|
+
// Use case-insensitive matching for names
|
|
40343
|
+
const fromCamera = topology.cameras.find(c => c.deviceId === conn.fromCameraId ||
|
|
40344
|
+
c.name === conn.fromCameraId ||
|
|
40345
|
+
c.name.toLowerCase() === conn.fromCameraId.toLowerCase());
|
|
40346
|
+
const toCamera = topology.cameras.find(c => c.deviceId === conn.toCameraId ||
|
|
40347
|
+
c.name === conn.toCameraId ||
|
|
40348
|
+
c.name.toLowerCase() === conn.toCameraId.toLowerCase());
|
|
40349
|
+
// Don't create connection if cameras not found
|
|
40350
|
+
if (!fromCamera || !toCamera) {
|
|
40351
|
+
this.console.warn(`[Discovery] Cannot create connection: camera not found. from="${conn.fromCameraId}" to="${conn.toCameraId}"`);
|
|
40352
|
+
this.console.warn(`[Discovery] Available cameras: ${topology.cameras.map(c => `${c.name} (${c.deviceId})`).join(', ')}`);
|
|
40353
|
+
return;
|
|
40354
|
+
}
|
|
40355
|
+
// Check if connection already exists
|
|
40356
|
+
const existingConn = topology.connections.find(c => (c.fromCameraId === fromCamera.deviceId && c.toCameraId === toCamera.deviceId) ||
|
|
40357
|
+
(c.bidirectional && c.fromCameraId === toCamera.deviceId && c.toCameraId === fromCamera.deviceId));
|
|
40358
|
+
if (existingConn) {
|
|
40359
|
+
this.console.log(`[Discovery] Connection already exists between ${fromCamera.name} and ${toCamera.name}`);
|
|
40360
|
+
return;
|
|
40361
|
+
}
|
|
40362
|
+
// Warn if cameras don't have positions yet
|
|
40363
|
+
if (!fromCamera.floorPlanPosition || !toCamera.floorPlanPosition) {
|
|
40364
|
+
this.console.warn(`[Discovery] Note: One or both cameras not positioned on floor plan yet`);
|
|
40365
|
+
}
|
|
40248
40366
|
const newConnection = {
|
|
40249
40367
|
id: `conn_${Date.now()}`,
|
|
40250
|
-
fromCameraId:
|
|
40251
|
-
toCameraId:
|
|
40368
|
+
fromCameraId: fromCamera.deviceId,
|
|
40369
|
+
toCameraId: toCamera.deviceId,
|
|
40252
40370
|
bidirectional: conn.bidirectional,
|
|
40253
40371
|
// Default exit/entry zones covering full frame
|
|
40254
40372
|
exitZone: [[0, 0], [100, 0], [100, 100], [0, 100]],
|
|
@@ -40262,7 +40380,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40262
40380
|
};
|
|
40263
40381
|
topology.connections.push(newConnection);
|
|
40264
40382
|
updated = true;
|
|
40265
|
-
this.console.log(`[Discovery] Added connection: ${
|
|
40383
|
+
this.console.log(`[Discovery] Added connection: ${fromCamera.name} (${fromCamera.deviceId}) -> ${toCamera.name} (${toCamera.deviceId})`);
|
|
40266
40384
|
}
|
|
40267
40385
|
if (updated) {
|
|
40268
40386
|
// Save updated topology
|
|
@@ -40270,6 +40388,24 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40270
40388
|
this.trackingEngine.updateTopology(topology);
|
|
40271
40389
|
}
|
|
40272
40390
|
}
|
|
40391
|
+
/** Map discovered zone types to GlobalZone types for tracking/alerting */
|
|
40392
|
+
mapZoneTypeToGlobalType(type) {
|
|
40393
|
+
const mapping = {
|
|
40394
|
+
'yard': 'dwell',
|
|
40395
|
+
'driveway': 'entry',
|
|
40396
|
+
'street': 'entry',
|
|
40397
|
+
'patio': 'dwell',
|
|
40398
|
+
'walkway': 'entry',
|
|
40399
|
+
'parking': 'dwell',
|
|
40400
|
+
'garden': 'dwell',
|
|
40401
|
+
'pool': 'restricted',
|
|
40402
|
+
'entrance': 'entry',
|
|
40403
|
+
'garage': 'entry',
|
|
40404
|
+
'deck': 'dwell',
|
|
40405
|
+
'custom': 'dwell',
|
|
40406
|
+
};
|
|
40407
|
+
return mapping[type] || 'dwell';
|
|
40408
|
+
}
|
|
40273
40409
|
serveEditorUI(response) {
|
|
40274
40410
|
response.send(editor_html_1.EDITOR_HTML, {
|
|
40275
40411
|
headers: { 'Content-Type': 'text/html' },
|
|
@@ -41400,7 +41536,7 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
41400
41536
|
<button class="btn btn-small btn-primary" id="scan-now-btn" onclick="runDiscoveryScan()">Scan Now</button>
|
|
41401
41537
|
</div>
|
|
41402
41538
|
<div id="discovery-status" style="font-size: 11px; color: #888; margin-bottom: 8px;">
|
|
41403
|
-
<span id="discovery-status-text">
|
|
41539
|
+
<span id="discovery-status-text">Position cameras first, then scan</span>
|
|
41404
41540
|
</div>
|
|
41405
41541
|
<div id="discovery-suggestions-list"></div>
|
|
41406
41542
|
</div>
|
|
@@ -41433,6 +41569,7 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
41433
41569
|
</div>
|
|
41434
41570
|
<div class="toolbar-group">
|
|
41435
41571
|
<button class="btn" onclick="clearDrawings()">Clear Drawings</button>
|
|
41572
|
+
<button class="btn" onclick="clearAllTopology()" style="background: #dc2626;">Delete All</button>
|
|
41436
41573
|
</div>
|
|
41437
41574
|
<div class="toolbar-group">
|
|
41438
41575
|
<button class="btn btn-primary" onclick="saveTopology()">Save</button>
|
|
@@ -41935,6 +42072,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
41935
42072
|
let scanPollingInterval = null;
|
|
41936
42073
|
|
|
41937
42074
|
async function runDiscoveryScan() {
|
|
42075
|
+
// Check if cameras are positioned on the floor plan
|
|
42076
|
+
const positionedCameras = topology.cameras.filter(c => c.floorPlanPosition);
|
|
42077
|
+
if (positionedCameras.length === 0) {
|
|
42078
|
+
alert('Please position at least one camera on the floor plan before running discovery.\\n\\nSteps:\\n1. Click "Place Camera" in the toolbar\\n2. Click on the floor plan where the camera is located\\n3. Select the camera from the dropdown\\n4. Drag the rotation handle to set its direction\\n5. Then run discovery to detect zones and connections');
|
|
42079
|
+
return;
|
|
42080
|
+
}
|
|
42081
|
+
|
|
41938
42082
|
const scanBtn = document.getElementById('scan-now-btn');
|
|
41939
42083
|
const statusText = document.getElementById('discovery-status-text');
|
|
41940
42084
|
scanBtn.disabled = true;
|
|
@@ -42311,6 +42455,12 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42311
42455
|
for (let i = 1; i < currentZonePoints.length; i++) {
|
|
42312
42456
|
ctx.lineTo(currentZonePoints[i].x, currentZonePoints[i].y);
|
|
42313
42457
|
}
|
|
42458
|
+
// Close the polygon if we have 3+ points
|
|
42459
|
+
if (currentZonePoints.length >= 3) {
|
|
42460
|
+
ctx.closePath();
|
|
42461
|
+
ctx.fillStyle = color;
|
|
42462
|
+
ctx.fill();
|
|
42463
|
+
}
|
|
42314
42464
|
ctx.strokeStyle = strokeColor;
|
|
42315
42465
|
ctx.lineWidth = 2;
|
|
42316
42466
|
ctx.stroke();
|
|
@@ -42536,6 +42686,29 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42536
42686
|
function drawCamera(camera) {
|
|
42537
42687
|
const pos = camera.floorPlanPosition;
|
|
42538
42688
|
const isSelected = selectedItem?.type === 'camera' && selectedItem?.id === camera.deviceId;
|
|
42689
|
+
|
|
42690
|
+
// Get FOV settings or defaults
|
|
42691
|
+
const fov = camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 };
|
|
42692
|
+
const direction = (fov.mode === 'simple' || !fov.mode) ? (fov.direction || 0) : 0;
|
|
42693
|
+
const fovAngle = (fov.mode === 'simple' || !fov.mode) ? (fov.angle || 90) : 90;
|
|
42694
|
+
const range = (fov.mode === 'simple' || !fov.mode) ? (fov.range || 80) : 80;
|
|
42695
|
+
|
|
42696
|
+
// Convert direction to radians (0 = up/north, 90 = right/east)
|
|
42697
|
+
const dirRad = (direction - 90) * Math.PI / 180;
|
|
42698
|
+
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
42699
|
+
|
|
42700
|
+
// Draw FOV cone
|
|
42701
|
+
ctx.beginPath();
|
|
42702
|
+
ctx.moveTo(pos.x, pos.y);
|
|
42703
|
+
ctx.arc(pos.x, pos.y, range, dirRad - halfFov, dirRad + halfFov);
|
|
42704
|
+
ctx.closePath();
|
|
42705
|
+
ctx.fillStyle = isSelected ? 'rgba(233, 69, 96, 0.15)' : 'rgba(76, 175, 80, 0.15)';
|
|
42706
|
+
ctx.fill();
|
|
42707
|
+
ctx.strokeStyle = isSelected ? 'rgba(233, 69, 96, 0.5)' : 'rgba(76, 175, 80, 0.5)';
|
|
42708
|
+
ctx.lineWidth = 1;
|
|
42709
|
+
ctx.stroke();
|
|
42710
|
+
|
|
42711
|
+
// Draw camera circle
|
|
42539
42712
|
ctx.beginPath();
|
|
42540
42713
|
ctx.arc(pos.x, pos.y, 20, 0, Math.PI * 2);
|
|
42541
42714
|
ctx.fillStyle = isSelected ? '#e94560' : '#0f3460';
|
|
@@ -42543,12 +42716,41 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42543
42716
|
ctx.strokeStyle = '#fff';
|
|
42544
42717
|
ctx.lineWidth = 2;
|
|
42545
42718
|
ctx.stroke();
|
|
42719
|
+
|
|
42720
|
+
// Draw camera icon/text
|
|
42546
42721
|
ctx.fillStyle = '#fff';
|
|
42547
42722
|
ctx.font = '12px sans-serif';
|
|
42548
42723
|
ctx.textAlign = 'center';
|
|
42549
42724
|
ctx.textBaseline = 'middle';
|
|
42550
42725
|
ctx.fillText('CAM', pos.x, pos.y);
|
|
42551
42726
|
ctx.fillText(camera.name, pos.x, pos.y + 35);
|
|
42727
|
+
|
|
42728
|
+
// Draw direction handle (when selected) for rotation
|
|
42729
|
+
if (isSelected) {
|
|
42730
|
+
const handleLength = 45;
|
|
42731
|
+
const handleX = pos.x + Math.cos(dirRad) * handleLength;
|
|
42732
|
+
const handleY = pos.y + Math.sin(dirRad) * handleLength;
|
|
42733
|
+
|
|
42734
|
+
// Handle line
|
|
42735
|
+
ctx.beginPath();
|
|
42736
|
+
ctx.moveTo(pos.x + Math.cos(dirRad) * 20, pos.y + Math.sin(dirRad) * 20);
|
|
42737
|
+
ctx.lineTo(handleX, handleY);
|
|
42738
|
+
ctx.strokeStyle = '#ff6b6b';
|
|
42739
|
+
ctx.lineWidth = 3;
|
|
42740
|
+
ctx.stroke();
|
|
42741
|
+
|
|
42742
|
+
// Handle grip (circle at end)
|
|
42743
|
+
ctx.beginPath();
|
|
42744
|
+
ctx.arc(handleX, handleY, 8, 0, Math.PI * 2);
|
|
42745
|
+
ctx.fillStyle = '#ff6b6b';
|
|
42746
|
+
ctx.fill();
|
|
42747
|
+
ctx.strokeStyle = '#fff';
|
|
42748
|
+
ctx.lineWidth = 2;
|
|
42749
|
+
ctx.stroke();
|
|
42750
|
+
|
|
42751
|
+
// Store handle position for hit detection
|
|
42752
|
+
camera._handlePos = { x: handleX, y: handleY };
|
|
42753
|
+
}
|
|
42552
42754
|
}
|
|
42553
42755
|
|
|
42554
42756
|
function drawConnection(from, to, conn) {
|
|
@@ -42809,7 +43011,16 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42809
43011
|
|
|
42810
43012
|
function showCameraProperties(camera) {
|
|
42811
43013
|
const panel = document.getElementById('properties-panel');
|
|
42812
|
-
|
|
43014
|
+
const fov = camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 };
|
|
43015
|
+
panel.innerHTML = '<h3>Camera Properties</h3>' +
|
|
43016
|
+
'<div class="form-group"><label>Name</label><input type="text" value="' + camera.name + '" onchange="updateCameraName(\\'' + camera.deviceId + '\\', this.value)"></div>' +
|
|
43017
|
+
'<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isEntryPoint ? 'checked' : '') + ' onchange="updateCameraEntry(\\'' + camera.deviceId + '\\', this.checked)">Entry Point</label></div>' +
|
|
43018
|
+
'<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isExitPoint ? 'checked' : '') + ' onchange="updateCameraExit(\\'' + camera.deviceId + '\\', this.checked)">Exit Point</label></div>' +
|
|
43019
|
+
'<h4 style="margin-top: 15px; margin-bottom: 10px; color: #888;">Field of View</h4>' +
|
|
43020
|
+
'<div class="form-group"><label>Direction (0=up, 90=right)</label><input type="number" value="' + Math.round(fov.direction || 0) + '" min="0" max="359" onchange="updateCameraFov(\\'' + camera.deviceId + '\\', \\'direction\\', this.value)"></div>' +
|
|
43021
|
+
'<div class="form-group"><label>FOV Angle (degrees)</label><input type="number" value="' + (fov.angle || 90) + '" min="30" max="180" onchange="updateCameraFov(\\'' + camera.deviceId + '\\', \\'angle\\', this.value)"></div>' +
|
|
43022
|
+
'<div class="form-group"><label>Range (pixels)</label><input type="number" value="' + (fov.range || 80) + '" min="20" max="300" onchange="updateCameraFov(\\'' + camera.deviceId + '\\', \\'range\\', this.value)"></div>' +
|
|
43023
|
+
'<div class="form-group"><button class="btn" style="width: 100%; background: #f44336;" onclick="deleteCamera(\\'' + camera.deviceId + '\\')">Delete Camera</button></div>';
|
|
42813
43024
|
}
|
|
42814
43025
|
|
|
42815
43026
|
function showConnectionProperties(connection) {
|
|
@@ -42820,6 +43031,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42820
43031
|
function updateCameraName(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.name = value; updateUI(); }
|
|
42821
43032
|
function updateCameraEntry(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isEntryPoint = value; }
|
|
42822
43033
|
function updateCameraExit(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isExitPoint = value; }
|
|
43034
|
+
function updateCameraFov(id, field, value) {
|
|
43035
|
+
const camera = topology.cameras.find(c => c.deviceId === id);
|
|
43036
|
+
if (!camera) return;
|
|
43037
|
+
if (!camera.fov) camera.fov = { mode: 'simple', angle: 90, direction: 0, range: 80 };
|
|
43038
|
+
camera.fov[field] = parseFloat(value);
|
|
43039
|
+
render();
|
|
43040
|
+
}
|
|
42823
43041
|
function updateConnectionName(id, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.name = value; updateUI(); }
|
|
42824
43042
|
function updateTransitTime(id, field, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.transitTime[field] = parseInt(value) * 1000; }
|
|
42825
43043
|
function updateConnectionBidi(id, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.bidirectional = value; render(); }
|
|
@@ -42963,10 +43181,30 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42963
43181
|
setStatus('Drawings cleared', 'success');
|
|
42964
43182
|
}
|
|
42965
43183
|
|
|
43184
|
+
function clearAllTopology() {
|
|
43185
|
+
if (!confirm('DELETE ALL TOPOLOGY DATA?\\n\\nThis will remove:\\n- All cameras\\n- All connections\\n- All landmarks\\n- All zones\\n- All drawings\\n\\nThis cannot be undone.')) return;
|
|
43186
|
+
|
|
43187
|
+
topology.cameras = [];
|
|
43188
|
+
topology.connections = [];
|
|
43189
|
+
topology.landmarks = [];
|
|
43190
|
+
topology.globalZones = [];
|
|
43191
|
+
topology.drawnZones = [];
|
|
43192
|
+
topology.drawings = [];
|
|
43193
|
+
topology.relationships = [];
|
|
43194
|
+
|
|
43195
|
+
selectedItem = null;
|
|
43196
|
+
document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select an item to edit.</p>';
|
|
43197
|
+
updateCameraSelects();
|
|
43198
|
+
updateUI();
|
|
43199
|
+
render();
|
|
43200
|
+
setStatus('All topology data cleared', 'warning');
|
|
43201
|
+
}
|
|
43202
|
+
|
|
42966
43203
|
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
|
|
42967
43204
|
function setStatus(text, type) { document.getElementById('status-text').textContent = text; const dot = document.getElementById('status-dot'); dot.className = 'status-dot'; if (type === 'warning') dot.classList.add('warning'); if (type === 'error') dot.classList.add('error'); }
|
|
42968
43205
|
|
|
42969
43206
|
let dragging = null;
|
|
43207
|
+
let rotatingCamera = null;
|
|
42970
43208
|
|
|
42971
43209
|
canvas.addEventListener('mousedown', (e) => {
|
|
42972
43210
|
const rect = canvas.getBoundingClientRect();
|
|
@@ -42982,7 +43220,20 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42982
43220
|
}
|
|
42983
43221
|
|
|
42984
43222
|
if (currentTool === 'select') {
|
|
42985
|
-
// Check
|
|
43223
|
+
// Check for rotation handle on selected camera first
|
|
43224
|
+
if (selectedItem?.type === 'camera') {
|
|
43225
|
+
const camera = topology.cameras.find(c => c.deviceId === selectedItem.id);
|
|
43226
|
+
if (camera?._handlePos) {
|
|
43227
|
+
const dist = Math.hypot(x - camera._handlePos.x, y - camera._handlePos.y);
|
|
43228
|
+
if (dist < 15) {
|
|
43229
|
+
rotatingCamera = camera;
|
|
43230
|
+
setStatus('Drag to rotate camera direction', 'warning');
|
|
43231
|
+
return;
|
|
43232
|
+
}
|
|
43233
|
+
}
|
|
43234
|
+
}
|
|
43235
|
+
|
|
43236
|
+
// Check cameras
|
|
42986
43237
|
for (const camera of topology.cameras) {
|
|
42987
43238
|
if (camera.floorPlanPosition) {
|
|
42988
43239
|
const dist = Math.hypot(x - camera.floorPlanPosition.x, y - camera.floorPlanPosition.y);
|
|
@@ -43046,6 +43297,20 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43046
43297
|
const x = e.clientX - rect.left;
|
|
43047
43298
|
const y = e.clientY - rect.top;
|
|
43048
43299
|
|
|
43300
|
+
// Handle camera rotation
|
|
43301
|
+
if (rotatingCamera) {
|
|
43302
|
+
const pos = rotatingCamera.floorPlanPosition;
|
|
43303
|
+
const angle = Math.atan2(y - pos.y, x - pos.x);
|
|
43304
|
+
// Convert to our direction system (0 = up/north, 90 = right/east)
|
|
43305
|
+
const direction = (angle * 180 / Math.PI) + 90;
|
|
43306
|
+
if (!rotatingCamera.fov) {
|
|
43307
|
+
rotatingCamera.fov = { mode: 'simple', angle: 90, direction: 0, range: 80 };
|
|
43308
|
+
}
|
|
43309
|
+
rotatingCamera.fov.direction = ((direction % 360) + 360) % 360; // Normalize 0-360
|
|
43310
|
+
render();
|
|
43311
|
+
return;
|
|
43312
|
+
}
|
|
43313
|
+
|
|
43049
43314
|
if (dragging) {
|
|
43050
43315
|
if (dragging.type === 'camera') {
|
|
43051
43316
|
dragging.item.floorPlanPosition.x = x;
|
|
@@ -43068,6 +43333,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43068
43333
|
});
|
|
43069
43334
|
|
|
43070
43335
|
canvas.addEventListener('mouseup', (e) => {
|
|
43336
|
+
// Clear camera rotation
|
|
43337
|
+
if (rotatingCamera) {
|
|
43338
|
+
setStatus('Camera direction updated', 'success');
|
|
43339
|
+
rotatingCamera = null;
|
|
43340
|
+
return;
|
|
43341
|
+
}
|
|
43342
|
+
|
|
43071
43343
|
if (isDrawing && currentDrawing) {
|
|
43072
43344
|
if (!topology.drawings) topology.drawings = [];
|
|
43073
43345
|
// Normalize room coordinates if drawn backwards
|