@blueharford/scrypted-spatial-awareness 0.5.9 → 0.6.1
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 +287 -48
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/main.ts +188 -48
- package/src/ui/editor-html.ts +133 -3
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/out/main.nodejs.js
CHANGED
|
@@ -40224,20 +40224,34 @@ 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
|
|
40228
|
-
// Use the first visible camera's position as a starting point, or canvas center
|
|
40227
|
+
// Calculate position for the landmark WITHIN the camera's field of view
|
|
40229
40228
|
let position = suggestion.landmark.position;
|
|
40230
40229
|
if (!position || (position.x === 0 && position.y === 0)) {
|
|
40231
40230
|
// Find a camera that can see this landmark
|
|
40232
40231
|
const visibleCameraId = suggestion.landmark.visibleFromCameras?.[0];
|
|
40233
40232
|
const camera = visibleCameraId ? topology.cameras.find(c => c.deviceId === visibleCameraId) : null;
|
|
40234
40233
|
if (camera?.floorPlanPosition) {
|
|
40235
|
-
//
|
|
40236
|
-
const
|
|
40234
|
+
// Get camera's FOV direction and range (cast to any for flexible access)
|
|
40235
|
+
const fov = (camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 });
|
|
40236
|
+
const direction = fov.direction || 0;
|
|
40237
|
+
const range = fov.range || 80;
|
|
40238
|
+
const fovAngle = fov.angle || 90;
|
|
40239
|
+
// Count existing landmarks from this camera to spread them out
|
|
40240
|
+
const existingFromCamera = (topology.landmarks || []).filter(l => l.visibleFromCameras?.includes(visibleCameraId)).length;
|
|
40241
|
+
// Calculate position in front of camera within its FOV
|
|
40242
|
+
// Convert direction to radians (0 = up/north, 90 = right/east)
|
|
40243
|
+
const dirRad = (direction - 90) * Math.PI / 180;
|
|
40244
|
+
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
40245
|
+
// Spread landmarks across the FOV cone at varying distances
|
|
40246
|
+
const angleOffset = (existingFromCamera % 3 - 1) * halfFov * 0.6; // -0.6, 0, +0.6 of half FOV
|
|
40247
|
+
const distanceMultiplier = 0.5 + (existingFromCamera % 2) * 0.3; // 50% or 80% of range
|
|
40248
|
+
const finalAngle = dirRad + angleOffset;
|
|
40249
|
+
const distance = range * distanceMultiplier;
|
|
40237
40250
|
position = {
|
|
40238
|
-
x: camera.floorPlanPosition.x +
|
|
40239
|
-
y: camera.floorPlanPosition.y +
|
|
40251
|
+
x: camera.floorPlanPosition.x + Math.cos(finalAngle) * distance,
|
|
40252
|
+
y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distance,
|
|
40240
40253
|
};
|
|
40254
|
+
this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}" in ${camera.name}'s FOV: dir=${direction}°, dist=${distance.toFixed(0)}px`);
|
|
40241
40255
|
}
|
|
40242
40256
|
else {
|
|
40243
40257
|
// Position in a grid pattern starting from center
|
|
@@ -40271,64 +40285,141 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40271
40285
|
if (suggestion.type === 'zone' && suggestion.zone) {
|
|
40272
40286
|
// Create a drawn zone from the discovery zone
|
|
40273
40287
|
const zone = suggestion.zone;
|
|
40274
|
-
// Find cameras that see this zone
|
|
40275
|
-
const
|
|
40276
|
-
const camera =
|
|
40277
|
-
|
|
40278
|
-
|
|
40279
|
-
|
|
40288
|
+
// Find cameras that see this zone
|
|
40289
|
+
const sourceCameras = suggestion.sourceCameras || [];
|
|
40290
|
+
const camera = sourceCameras[0]
|
|
40291
|
+
? topology.cameras.find(c => c.deviceId === sourceCameras[0] || c.name === sourceCameras[0])
|
|
40292
|
+
: null;
|
|
40293
|
+
// Create zone polygon WITHIN the camera's field of view
|
|
40294
|
+
let polygon = [];
|
|
40295
|
+
const timestamp = Date.now();
|
|
40280
40296
|
if (camera?.floorPlanPosition) {
|
|
40281
|
-
|
|
40282
|
-
|
|
40297
|
+
// Get camera's FOV direction and range (cast to any for flexible access)
|
|
40298
|
+
const fov = (camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 });
|
|
40299
|
+
const direction = fov.direction || 0;
|
|
40300
|
+
const range = fov.range || 80;
|
|
40301
|
+
const fovAngle = fov.angle || 90;
|
|
40302
|
+
// Convert direction to radians (0 = up/north, 90 = right/east)
|
|
40303
|
+
const dirRad = (direction - 90) * Math.PI / 180;
|
|
40304
|
+
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
40305
|
+
// Count existing zones from this camera to offset new ones
|
|
40306
|
+
const existingFromCamera = (topology.drawnZones || []).filter((z) => z.linkedCameras?.includes(sourceCameras[0])).length;
|
|
40307
|
+
// Create a wedge-shaped zone within the camera's FOV
|
|
40308
|
+
// Offset based on existing zones to avoid overlap
|
|
40309
|
+
const innerRadius = range * 0.3 + existingFromCamera * 20;
|
|
40310
|
+
const outerRadius = range * 0.8 + existingFromCamera * 20;
|
|
40311
|
+
// Use a portion of the FOV for each zone
|
|
40312
|
+
const zoneSpread = halfFov * 0.7; // 70% of half FOV
|
|
40313
|
+
const camX = camera.floorPlanPosition.x;
|
|
40314
|
+
const camY = camera.floorPlanPosition.y;
|
|
40315
|
+
// Create arc polygon (wedge shape)
|
|
40316
|
+
const steps = 8;
|
|
40317
|
+
// Inner arc (from left to right)
|
|
40318
|
+
for (let i = 0; i <= steps; i++) {
|
|
40319
|
+
const angle = dirRad - zoneSpread + (zoneSpread * 2 * i / steps);
|
|
40320
|
+
polygon.push({
|
|
40321
|
+
x: camX + Math.cos(angle) * innerRadius,
|
|
40322
|
+
y: camY + Math.sin(angle) * innerRadius,
|
|
40323
|
+
});
|
|
40324
|
+
}
|
|
40325
|
+
// Outer arc (from right to left)
|
|
40326
|
+
for (let i = steps; i >= 0; i--) {
|
|
40327
|
+
const angle = dirRad - zoneSpread + (zoneSpread * 2 * i / steps);
|
|
40328
|
+
polygon.push({
|
|
40329
|
+
x: camX + Math.cos(angle) * outerRadius,
|
|
40330
|
+
y: camY + Math.sin(angle) * outerRadius,
|
|
40331
|
+
});
|
|
40332
|
+
}
|
|
40333
|
+
this.console.log(`[Discovery] Creating zone "${zone.name}" in ${camera.name}'s FOV: dir=${direction}°`);
|
|
40283
40334
|
}
|
|
40284
|
-
|
|
40285
|
-
|
|
40286
|
-
|
|
40287
|
-
|
|
40288
|
-
|
|
40289
|
-
|
|
40290
|
-
description: zone.description,
|
|
40291
|
-
polygon: [
|
|
40335
|
+
else {
|
|
40336
|
+
// Fallback: rectangular zone at default location
|
|
40337
|
+
const centerX = 300 + (topology.drawnZones?.length || 0) * 120;
|
|
40338
|
+
const centerY = 200;
|
|
40339
|
+
const size = 100;
|
|
40340
|
+
polygon = [
|
|
40292
40341
|
{ x: centerX - size / 2, y: centerY - size / 2 },
|
|
40293
40342
|
{ x: centerX + size / 2, y: centerY - size / 2 },
|
|
40294
40343
|
{ x: centerX + size / 2, y: centerY + size / 2 },
|
|
40295
40344
|
{ x: centerX - size / 2, y: centerY + size / 2 },
|
|
40296
|
-
]
|
|
40345
|
+
];
|
|
40346
|
+
}
|
|
40347
|
+
// 1. Create DrawnZone (visual on floor plan)
|
|
40348
|
+
const drawnZone = {
|
|
40349
|
+
id: `zone_${timestamp}`,
|
|
40350
|
+
name: zone.name,
|
|
40351
|
+
type: (zone.type || 'custom'),
|
|
40352
|
+
description: zone.description,
|
|
40353
|
+
polygon: polygon,
|
|
40354
|
+
linkedCameras: sourceCameras,
|
|
40297
40355
|
};
|
|
40298
40356
|
if (!topology.drawnZones) {
|
|
40299
40357
|
topology.drawnZones = [];
|
|
40300
40358
|
}
|
|
40301
40359
|
topology.drawnZones.push(drawnZone);
|
|
40360
|
+
// 2. Create GlobalZone (for tracking/alerting)
|
|
40361
|
+
const globalZoneType = this.mapZoneTypeToGlobalType(zone.type);
|
|
40362
|
+
const cameraZones = sourceCameras
|
|
40363
|
+
.map(camRef => {
|
|
40364
|
+
const cam = topology.cameras.find(c => c.deviceId === camRef || c.name === camRef);
|
|
40365
|
+
if (!cam)
|
|
40366
|
+
return null;
|
|
40367
|
+
return {
|
|
40368
|
+
cameraId: cam.deviceId,
|
|
40369
|
+
zone: [[0, 0], [100, 0], [100, 100], [0, 100]], // Full frame default
|
|
40370
|
+
};
|
|
40371
|
+
})
|
|
40372
|
+
.filter((z) => z !== null);
|
|
40373
|
+
if (cameraZones.length > 0) {
|
|
40374
|
+
const globalZone = {
|
|
40375
|
+
id: `global_${timestamp}`,
|
|
40376
|
+
name: zone.name,
|
|
40377
|
+
type: globalZoneType,
|
|
40378
|
+
cameraZones,
|
|
40379
|
+
};
|
|
40380
|
+
if (!topology.globalZones) {
|
|
40381
|
+
topology.globalZones = [];
|
|
40382
|
+
}
|
|
40383
|
+
topology.globalZones.push(globalZone);
|
|
40384
|
+
this.console.log(`[Discovery] Added zone: ${zone.name} (${zone.type}) - DrawnZone and GlobalZone created`);
|
|
40385
|
+
}
|
|
40386
|
+
else {
|
|
40387
|
+
this.console.log(`[Discovery] Added zone: ${zone.name} (${zone.type}) - DrawnZone only (no cameras matched)`);
|
|
40388
|
+
}
|
|
40302
40389
|
updated = true;
|
|
40303
|
-
this.console.log(`[Discovery] Added zone: ${zone.name} (${zone.type})`);
|
|
40304
40390
|
}
|
|
40305
40391
|
if (suggestion.type === 'connection' && suggestion.connection) {
|
|
40306
40392
|
// Add new connection to topology
|
|
40307
40393
|
const conn = suggestion.connection;
|
|
40308
|
-
//
|
|
40309
|
-
|
|
40310
|
-
const
|
|
40311
|
-
|
|
40312
|
-
|
|
40313
|
-
|
|
40314
|
-
|
|
40315
|
-
|
|
40316
|
-
|
|
40317
|
-
|
|
40318
|
-
this.console.
|
|
40394
|
+
// Resolve camera references - LLM may return names instead of deviceIds
|
|
40395
|
+
// Use case-insensitive matching for names
|
|
40396
|
+
const fromCamera = topology.cameras.find(c => c.deviceId === conn.fromCameraId ||
|
|
40397
|
+
c.name === conn.fromCameraId ||
|
|
40398
|
+
c.name.toLowerCase() === conn.fromCameraId.toLowerCase());
|
|
40399
|
+
const toCamera = topology.cameras.find(c => c.deviceId === conn.toCameraId ||
|
|
40400
|
+
c.name === conn.toCameraId ||
|
|
40401
|
+
c.name.toLowerCase() === conn.toCameraId.toLowerCase());
|
|
40402
|
+
// Don't create connection if cameras not found
|
|
40403
|
+
if (!fromCamera || !toCamera) {
|
|
40404
|
+
this.console.warn(`[Discovery] Cannot create connection: camera not found. from="${conn.fromCameraId}" to="${conn.toCameraId}"`);
|
|
40405
|
+
this.console.warn(`[Discovery] Available cameras: ${topology.cameras.map(c => `${c.name} (${c.deviceId})`).join(', ')}`);
|
|
40406
|
+
return;
|
|
40319
40407
|
}
|
|
40320
|
-
if
|
|
40321
|
-
|
|
40322
|
-
toCamera.
|
|
40323
|
-
|
|
40324
|
-
|
|
40325
|
-
|
|
40326
|
-
|
|
40408
|
+
// Check if connection already exists
|
|
40409
|
+
const existingConn = topology.connections.find(c => (c.fromCameraId === fromCamera.deviceId && c.toCameraId === toCamera.deviceId) ||
|
|
40410
|
+
(c.bidirectional && c.fromCameraId === toCamera.deviceId && c.toCameraId === fromCamera.deviceId));
|
|
40411
|
+
if (existingConn) {
|
|
40412
|
+
this.console.log(`[Discovery] Connection already exists between ${fromCamera.name} and ${toCamera.name}`);
|
|
40413
|
+
return;
|
|
40414
|
+
}
|
|
40415
|
+
// Warn if cameras don't have positions yet
|
|
40416
|
+
if (!fromCamera.floorPlanPosition || !toCamera.floorPlanPosition) {
|
|
40417
|
+
this.console.warn(`[Discovery] Note: One or both cameras not positioned on floor plan yet`);
|
|
40327
40418
|
}
|
|
40328
40419
|
const newConnection = {
|
|
40329
40420
|
id: `conn_${Date.now()}`,
|
|
40330
|
-
fromCameraId: fromCamera
|
|
40331
|
-
toCameraId: toCamera
|
|
40421
|
+
fromCameraId: fromCamera.deviceId,
|
|
40422
|
+
toCameraId: toCamera.deviceId,
|
|
40332
40423
|
bidirectional: conn.bidirectional,
|
|
40333
40424
|
// Default exit/entry zones covering full frame
|
|
40334
40425
|
exitZone: [[0, 0], [100, 0], [100, 100], [0, 100]],
|
|
@@ -40342,7 +40433,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40342
40433
|
};
|
|
40343
40434
|
topology.connections.push(newConnection);
|
|
40344
40435
|
updated = true;
|
|
40345
|
-
this.console.log(`[Discovery] Added connection: ${fromCamera
|
|
40436
|
+
this.console.log(`[Discovery] Added connection: ${fromCamera.name} (${fromCamera.deviceId}) -> ${toCamera.name} (${toCamera.deviceId})`);
|
|
40346
40437
|
}
|
|
40347
40438
|
if (updated) {
|
|
40348
40439
|
// Save updated topology
|
|
@@ -40350,6 +40441,24 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40350
40441
|
this.trackingEngine.updateTopology(topology);
|
|
40351
40442
|
}
|
|
40352
40443
|
}
|
|
40444
|
+
/** Map discovered zone types to GlobalZone types for tracking/alerting */
|
|
40445
|
+
mapZoneTypeToGlobalType(type) {
|
|
40446
|
+
const mapping = {
|
|
40447
|
+
'yard': 'dwell',
|
|
40448
|
+
'driveway': 'entry',
|
|
40449
|
+
'street': 'entry',
|
|
40450
|
+
'patio': 'dwell',
|
|
40451
|
+
'walkway': 'entry',
|
|
40452
|
+
'parking': 'dwell',
|
|
40453
|
+
'garden': 'dwell',
|
|
40454
|
+
'pool': 'restricted',
|
|
40455
|
+
'entrance': 'entry',
|
|
40456
|
+
'garage': 'entry',
|
|
40457
|
+
'deck': 'dwell',
|
|
40458
|
+
'custom': 'dwell',
|
|
40459
|
+
};
|
|
40460
|
+
return mapping[type] || 'dwell';
|
|
40461
|
+
}
|
|
40353
40462
|
serveEditorUI(response) {
|
|
40354
40463
|
response.send(editor_html_1.EDITOR_HTML, {
|
|
40355
40464
|
headers: { 'Content-Type': 'text/html' },
|
|
@@ -41480,7 +41589,7 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
41480
41589
|
<button class="btn btn-small btn-primary" id="scan-now-btn" onclick="runDiscoveryScan()">Scan Now</button>
|
|
41481
41590
|
</div>
|
|
41482
41591
|
<div id="discovery-status" style="font-size: 11px; color: #888; margin-bottom: 8px;">
|
|
41483
|
-
<span id="discovery-status-text">
|
|
41592
|
+
<span id="discovery-status-text">Position cameras first, then scan</span>
|
|
41484
41593
|
</div>
|
|
41485
41594
|
<div id="discovery-suggestions-list"></div>
|
|
41486
41595
|
</div>
|
|
@@ -41513,6 +41622,7 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
41513
41622
|
</div>
|
|
41514
41623
|
<div class="toolbar-group">
|
|
41515
41624
|
<button class="btn" onclick="clearDrawings()">Clear Drawings</button>
|
|
41625
|
+
<button class="btn" onclick="clearAllTopology()" style="background: #dc2626;">Delete All</button>
|
|
41516
41626
|
</div>
|
|
41517
41627
|
<div class="toolbar-group">
|
|
41518
41628
|
<button class="btn btn-primary" onclick="saveTopology()">Save</button>
|
|
@@ -42015,6 +42125,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42015
42125
|
let scanPollingInterval = null;
|
|
42016
42126
|
|
|
42017
42127
|
async function runDiscoveryScan() {
|
|
42128
|
+
// Check if cameras are positioned on the floor plan
|
|
42129
|
+
const positionedCameras = topology.cameras.filter(c => c.floorPlanPosition);
|
|
42130
|
+
if (positionedCameras.length === 0) {
|
|
42131
|
+
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');
|
|
42132
|
+
return;
|
|
42133
|
+
}
|
|
42134
|
+
|
|
42018
42135
|
const scanBtn = document.getElementById('scan-now-btn');
|
|
42019
42136
|
const statusText = document.getElementById('discovery-status-text');
|
|
42020
42137
|
scanBtn.disabled = true;
|
|
@@ -42622,6 +42739,29 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42622
42739
|
function drawCamera(camera) {
|
|
42623
42740
|
const pos = camera.floorPlanPosition;
|
|
42624
42741
|
const isSelected = selectedItem?.type === 'camera' && selectedItem?.id === camera.deviceId;
|
|
42742
|
+
|
|
42743
|
+
// Get FOV settings or defaults
|
|
42744
|
+
const fov = camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 };
|
|
42745
|
+
const direction = (fov.mode === 'simple' || !fov.mode) ? (fov.direction || 0) : 0;
|
|
42746
|
+
const fovAngle = (fov.mode === 'simple' || !fov.mode) ? (fov.angle || 90) : 90;
|
|
42747
|
+
const range = (fov.mode === 'simple' || !fov.mode) ? (fov.range || 80) : 80;
|
|
42748
|
+
|
|
42749
|
+
// Convert direction to radians (0 = up/north, 90 = right/east)
|
|
42750
|
+
const dirRad = (direction - 90) * Math.PI / 180;
|
|
42751
|
+
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
42752
|
+
|
|
42753
|
+
// Draw FOV cone
|
|
42754
|
+
ctx.beginPath();
|
|
42755
|
+
ctx.moveTo(pos.x, pos.y);
|
|
42756
|
+
ctx.arc(pos.x, pos.y, range, dirRad - halfFov, dirRad + halfFov);
|
|
42757
|
+
ctx.closePath();
|
|
42758
|
+
ctx.fillStyle = isSelected ? 'rgba(233, 69, 96, 0.15)' : 'rgba(76, 175, 80, 0.15)';
|
|
42759
|
+
ctx.fill();
|
|
42760
|
+
ctx.strokeStyle = isSelected ? 'rgba(233, 69, 96, 0.5)' : 'rgba(76, 175, 80, 0.5)';
|
|
42761
|
+
ctx.lineWidth = 1;
|
|
42762
|
+
ctx.stroke();
|
|
42763
|
+
|
|
42764
|
+
// Draw camera circle
|
|
42625
42765
|
ctx.beginPath();
|
|
42626
42766
|
ctx.arc(pos.x, pos.y, 20, 0, Math.PI * 2);
|
|
42627
42767
|
ctx.fillStyle = isSelected ? '#e94560' : '#0f3460';
|
|
@@ -42629,12 +42769,41 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42629
42769
|
ctx.strokeStyle = '#fff';
|
|
42630
42770
|
ctx.lineWidth = 2;
|
|
42631
42771
|
ctx.stroke();
|
|
42772
|
+
|
|
42773
|
+
// Draw camera icon/text
|
|
42632
42774
|
ctx.fillStyle = '#fff';
|
|
42633
42775
|
ctx.font = '12px sans-serif';
|
|
42634
42776
|
ctx.textAlign = 'center';
|
|
42635
42777
|
ctx.textBaseline = 'middle';
|
|
42636
42778
|
ctx.fillText('CAM', pos.x, pos.y);
|
|
42637
42779
|
ctx.fillText(camera.name, pos.x, pos.y + 35);
|
|
42780
|
+
|
|
42781
|
+
// Draw direction handle (when selected) for rotation
|
|
42782
|
+
if (isSelected) {
|
|
42783
|
+
const handleLength = 45;
|
|
42784
|
+
const handleX = pos.x + Math.cos(dirRad) * handleLength;
|
|
42785
|
+
const handleY = pos.y + Math.sin(dirRad) * handleLength;
|
|
42786
|
+
|
|
42787
|
+
// Handle line
|
|
42788
|
+
ctx.beginPath();
|
|
42789
|
+
ctx.moveTo(pos.x + Math.cos(dirRad) * 20, pos.y + Math.sin(dirRad) * 20);
|
|
42790
|
+
ctx.lineTo(handleX, handleY);
|
|
42791
|
+
ctx.strokeStyle = '#ff6b6b';
|
|
42792
|
+
ctx.lineWidth = 3;
|
|
42793
|
+
ctx.stroke();
|
|
42794
|
+
|
|
42795
|
+
// Handle grip (circle at end)
|
|
42796
|
+
ctx.beginPath();
|
|
42797
|
+
ctx.arc(handleX, handleY, 8, 0, Math.PI * 2);
|
|
42798
|
+
ctx.fillStyle = '#ff6b6b';
|
|
42799
|
+
ctx.fill();
|
|
42800
|
+
ctx.strokeStyle = '#fff';
|
|
42801
|
+
ctx.lineWidth = 2;
|
|
42802
|
+
ctx.stroke();
|
|
42803
|
+
|
|
42804
|
+
// Store handle position for hit detection
|
|
42805
|
+
camera._handlePos = { x: handleX, y: handleY };
|
|
42806
|
+
}
|
|
42638
42807
|
}
|
|
42639
42808
|
|
|
42640
42809
|
function drawConnection(from, to, conn) {
|
|
@@ -42895,7 +43064,16 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42895
43064
|
|
|
42896
43065
|
function showCameraProperties(camera) {
|
|
42897
43066
|
const panel = document.getElementById('properties-panel');
|
|
42898
|
-
|
|
43067
|
+
const fov = camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 };
|
|
43068
|
+
panel.innerHTML = '<h3>Camera Properties</h3>' +
|
|
43069
|
+
'<div class="form-group"><label>Name</label><input type="text" value="' + camera.name + '" onchange="updateCameraName(\\'' + camera.deviceId + '\\', this.value)"></div>' +
|
|
43070
|
+
'<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isEntryPoint ? 'checked' : '') + ' onchange="updateCameraEntry(\\'' + camera.deviceId + '\\', this.checked)">Entry Point</label></div>' +
|
|
43071
|
+
'<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isExitPoint ? 'checked' : '') + ' onchange="updateCameraExit(\\'' + camera.deviceId + '\\', this.checked)">Exit Point</label></div>' +
|
|
43072
|
+
'<h4 style="margin-top: 15px; margin-bottom: 10px; color: #888;">Field of View</h4>' +
|
|
43073
|
+
'<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>' +
|
|
43074
|
+
'<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>' +
|
|
43075
|
+
'<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>' +
|
|
43076
|
+
'<div class="form-group"><button class="btn" style="width: 100%; background: #f44336;" onclick="deleteCamera(\\'' + camera.deviceId + '\\')">Delete Camera</button></div>';
|
|
42899
43077
|
}
|
|
42900
43078
|
|
|
42901
43079
|
function showConnectionProperties(connection) {
|
|
@@ -42906,6 +43084,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42906
43084
|
function updateCameraName(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.name = value; updateUI(); }
|
|
42907
43085
|
function updateCameraEntry(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isEntryPoint = value; }
|
|
42908
43086
|
function updateCameraExit(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isExitPoint = value; }
|
|
43087
|
+
function updateCameraFov(id, field, value) {
|
|
43088
|
+
const camera = topology.cameras.find(c => c.deviceId === id);
|
|
43089
|
+
if (!camera) return;
|
|
43090
|
+
if (!camera.fov) camera.fov = { mode: 'simple', angle: 90, direction: 0, range: 80 };
|
|
43091
|
+
camera.fov[field] = parseFloat(value);
|
|
43092
|
+
render();
|
|
43093
|
+
}
|
|
42909
43094
|
function updateConnectionName(id, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.name = value; updateUI(); }
|
|
42910
43095
|
function updateTransitTime(id, field, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.transitTime[field] = parseInt(value) * 1000; }
|
|
42911
43096
|
function updateConnectionBidi(id, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.bidirectional = value; render(); }
|
|
@@ -43049,10 +43234,30 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43049
43234
|
setStatus('Drawings cleared', 'success');
|
|
43050
43235
|
}
|
|
43051
43236
|
|
|
43237
|
+
function clearAllTopology() {
|
|
43238
|
+
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;
|
|
43239
|
+
|
|
43240
|
+
topology.cameras = [];
|
|
43241
|
+
topology.connections = [];
|
|
43242
|
+
topology.landmarks = [];
|
|
43243
|
+
topology.globalZones = [];
|
|
43244
|
+
topology.drawnZones = [];
|
|
43245
|
+
topology.drawings = [];
|
|
43246
|
+
topology.relationships = [];
|
|
43247
|
+
|
|
43248
|
+
selectedItem = null;
|
|
43249
|
+
document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select an item to edit.</p>';
|
|
43250
|
+
updateCameraSelects();
|
|
43251
|
+
updateUI();
|
|
43252
|
+
render();
|
|
43253
|
+
setStatus('All topology data cleared', 'warning');
|
|
43254
|
+
}
|
|
43255
|
+
|
|
43052
43256
|
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
|
|
43053
43257
|
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'); }
|
|
43054
43258
|
|
|
43055
43259
|
let dragging = null;
|
|
43260
|
+
let rotatingCamera = null;
|
|
43056
43261
|
|
|
43057
43262
|
canvas.addEventListener('mousedown', (e) => {
|
|
43058
43263
|
const rect = canvas.getBoundingClientRect();
|
|
@@ -43068,7 +43273,20 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43068
43273
|
}
|
|
43069
43274
|
|
|
43070
43275
|
if (currentTool === 'select') {
|
|
43071
|
-
// Check
|
|
43276
|
+
// Check for rotation handle on selected camera first
|
|
43277
|
+
if (selectedItem?.type === 'camera') {
|
|
43278
|
+
const camera = topology.cameras.find(c => c.deviceId === selectedItem.id);
|
|
43279
|
+
if (camera?._handlePos) {
|
|
43280
|
+
const dist = Math.hypot(x - camera._handlePos.x, y - camera._handlePos.y);
|
|
43281
|
+
if (dist < 15) {
|
|
43282
|
+
rotatingCamera = camera;
|
|
43283
|
+
setStatus('Drag to rotate camera direction', 'warning');
|
|
43284
|
+
return;
|
|
43285
|
+
}
|
|
43286
|
+
}
|
|
43287
|
+
}
|
|
43288
|
+
|
|
43289
|
+
// Check cameras
|
|
43072
43290
|
for (const camera of topology.cameras) {
|
|
43073
43291
|
if (camera.floorPlanPosition) {
|
|
43074
43292
|
const dist = Math.hypot(x - camera.floorPlanPosition.x, y - camera.floorPlanPosition.y);
|
|
@@ -43132,6 +43350,20 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43132
43350
|
const x = e.clientX - rect.left;
|
|
43133
43351
|
const y = e.clientY - rect.top;
|
|
43134
43352
|
|
|
43353
|
+
// Handle camera rotation
|
|
43354
|
+
if (rotatingCamera) {
|
|
43355
|
+
const pos = rotatingCamera.floorPlanPosition;
|
|
43356
|
+
const angle = Math.atan2(y - pos.y, x - pos.x);
|
|
43357
|
+
// Convert to our direction system (0 = up/north, 90 = right/east)
|
|
43358
|
+
const direction = (angle * 180 / Math.PI) + 90;
|
|
43359
|
+
if (!rotatingCamera.fov) {
|
|
43360
|
+
rotatingCamera.fov = { mode: 'simple', angle: 90, direction: 0, range: 80 };
|
|
43361
|
+
}
|
|
43362
|
+
rotatingCamera.fov.direction = ((direction % 360) + 360) % 360; // Normalize 0-360
|
|
43363
|
+
render();
|
|
43364
|
+
return;
|
|
43365
|
+
}
|
|
43366
|
+
|
|
43135
43367
|
if (dragging) {
|
|
43136
43368
|
if (dragging.type === 'camera') {
|
|
43137
43369
|
dragging.item.floorPlanPosition.x = x;
|
|
@@ -43154,6 +43386,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43154
43386
|
});
|
|
43155
43387
|
|
|
43156
43388
|
canvas.addEventListener('mouseup', (e) => {
|
|
43389
|
+
// Clear camera rotation
|
|
43390
|
+
if (rotatingCamera) {
|
|
43391
|
+
setStatus('Camera direction updated', 'success');
|
|
43392
|
+
rotatingCamera = null;
|
|
43393
|
+
return;
|
|
43394
|
+
}
|
|
43395
|
+
|
|
43157
43396
|
if (isDrawing && currentDrawing) {
|
|
43158
43397
|
if (!topology.drawings) topology.drawings = [];
|
|
43159
43398
|
// Normalize room coordinates if drawn backwards
|