@blueharford/scrypted-spatial-awareness 0.6.0 → 0.6.2
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 +134 -21
- package/out/main.nodejs.js.map +1 -1
- package/out/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/main.ts +90 -21
- package/src/ui/editor-html.ts +61 -1
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
|
|
@@ -40276,28 +40290,67 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40276
40290
|
const camera = sourceCameras[0]
|
|
40277
40291
|
? topology.cameras.find(c => c.deviceId === sourceCameras[0] || c.name === sourceCameras[0])
|
|
40278
40292
|
: null;
|
|
40279
|
-
// Create
|
|
40280
|
-
let
|
|
40281
|
-
|
|
40293
|
+
// Create zone polygon WITHIN the camera's field of view
|
|
40294
|
+
let polygon = [];
|
|
40295
|
+
const timestamp = Date.now();
|
|
40282
40296
|
if (camera?.floorPlanPosition) {
|
|
40283
|
-
|
|
40284
|
-
|
|
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}°`);
|
|
40334
|
+
}
|
|
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 = [
|
|
40341
|
+
{ x: centerX - size / 2, y: centerY - size / 2 },
|
|
40342
|
+
{ x: centerX + size / 2, y: centerY - size / 2 },
|
|
40343
|
+
{ x: centerX + size / 2, y: centerY + size / 2 },
|
|
40344
|
+
{ x: centerX - size / 2, y: centerY + size / 2 },
|
|
40345
|
+
];
|
|
40285
40346
|
}
|
|
40286
|
-
// Create a rectangular zone (user can edit later)
|
|
40287
|
-
const size = 100;
|
|
40288
|
-
const timestamp = Date.now();
|
|
40289
40347
|
// 1. Create DrawnZone (visual on floor plan)
|
|
40290
40348
|
const drawnZone = {
|
|
40291
40349
|
id: `zone_${timestamp}`,
|
|
40292
40350
|
name: zone.name,
|
|
40293
40351
|
type: (zone.type || 'custom'),
|
|
40294
40352
|
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
|
-
],
|
|
40353
|
+
polygon: polygon,
|
|
40301
40354
|
linkedCameras: sourceCameras,
|
|
40302
40355
|
};
|
|
40303
40356
|
if (!topology.drawnZones) {
|
|
@@ -41480,6 +41533,19 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
41480
41533
|
<p>Topology Editor</p>
|
|
41481
41534
|
</div>
|
|
41482
41535
|
<div class="sidebar-content">
|
|
41536
|
+
<div class="section" style="background: #1a3a5c; margin: -10px -15px 10px -15px; padding: 15px;">
|
|
41537
|
+
<div class="section-title" style="margin-bottom: 10px;">
|
|
41538
|
+
<span>Floor Plan Scale</span>
|
|
41539
|
+
</div>
|
|
41540
|
+
<div style="display: flex; gap: 10px; align-items: center;">
|
|
41541
|
+
<input type="number" id="scale-input" value="5" min="1" max="50" style="width: 60px; padding: 6px; background: #0f3460; border: 1px solid #1a4a7a; border-radius: 4px; color: #fff;" onchange="updateScale(this.value)">
|
|
41542
|
+
<span style="font-size: 12px; color: #888;">pixels per foot</span>
|
|
41543
|
+
<button class="btn btn-small" onclick="openScaleHelper()" style="margin-left: auto;">Help</button>
|
|
41544
|
+
</div>
|
|
41545
|
+
<div style="font-size: 11px; color: #666; margin-top: 8px;">
|
|
41546
|
+
Tip: If your floor plan is 800px wide and represents 80ft, scale = 10 px/ft
|
|
41547
|
+
</div>
|
|
41548
|
+
</div>
|
|
41483
41549
|
<div class="section">
|
|
41484
41550
|
<div class="section-title">
|
|
41485
41551
|
<span>Cameras</span>
|
|
@@ -41788,6 +41854,14 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
41788
41854
|
let currentDrawing = null;
|
|
41789
41855
|
let blankCanvasMode = false;
|
|
41790
41856
|
|
|
41857
|
+
// Floor plan scale: pixels per foot (default assumes ~5 pixels per foot for a typical floor plan)
|
|
41858
|
+
// User can adjust this by setting the scale
|
|
41859
|
+
let floorPlanScale = 5; // pixels per foot
|
|
41860
|
+
|
|
41861
|
+
// Helper functions for scale conversion
|
|
41862
|
+
function feetToPixels(feet) { return feet * floorPlanScale; }
|
|
41863
|
+
function pixelsToFeet(pixels) { return pixels / floorPlanScale; }
|
|
41864
|
+
|
|
41791
41865
|
// Zone drawing state
|
|
41792
41866
|
let zoneDrawingMode = false;
|
|
41793
41867
|
let currentZonePoints = [];
|
|
@@ -41843,6 +41917,12 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
41843
41917
|
if (response.ok) {
|
|
41844
41918
|
topology = await response.json();
|
|
41845
41919
|
if (!topology.drawings) topology.drawings = [];
|
|
41920
|
+
// Load floor plan scale if saved
|
|
41921
|
+
if (topology.floorPlanScale) {
|
|
41922
|
+
floorPlanScale = topology.floorPlanScale;
|
|
41923
|
+
const scaleInput = document.getElementById('scale-input');
|
|
41924
|
+
if (scaleInput) scaleInput.value = floorPlanScale;
|
|
41925
|
+
}
|
|
41846
41926
|
// Load floor plan from separate storage (handles legacy imageData in topology too)
|
|
41847
41927
|
if (topology.floorPlan?.imageData) {
|
|
41848
41928
|
// Legacy: imageData was stored in topology
|
|
@@ -43012,6 +43092,8 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43012
43092
|
function showCameraProperties(camera) {
|
|
43013
43093
|
const panel = document.getElementById('properties-panel');
|
|
43014
43094
|
const fov = camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 };
|
|
43095
|
+
// Convert stored pixel range to feet for display
|
|
43096
|
+
const rangeInFeet = Math.round(pixelsToFeet(fov.range || 80));
|
|
43015
43097
|
panel.innerHTML = '<h3>Camera Properties</h3>' +
|
|
43016
43098
|
'<div class="form-group"><label>Name</label><input type="text" value="' + camera.name + '" onchange="updateCameraName(\\'' + camera.deviceId + '\\', this.value)"></div>' +
|
|
43017
43099
|
'<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isEntryPoint ? 'checked' : '') + ' onchange="updateCameraEntry(\\'' + camera.deviceId + '\\', this.checked)">Entry Point</label></div>' +
|
|
@@ -43019,7 +43101,8 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43019
43101
|
'<h4 style="margin-top: 15px; margin-bottom: 10px; color: #888;">Field of View</h4>' +
|
|
43020
43102
|
'<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
43103
|
'<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 (
|
|
43104
|
+
'<div class="form-group"><label>Range (feet)</label><input type="number" value="' + rangeInFeet + '" min="5" max="200" onchange="updateCameraFovRange(\\'' + camera.deviceId + '\\', this.value)"></div>' +
|
|
43105
|
+
'<div style="font-size: 11px; color: #666; margin-top: -10px; margin-bottom: 15px;">~' + (fov.range || 80) + ' pixels at current scale</div>' +
|
|
43023
43106
|
'<div class="form-group"><button class="btn" style="width: 100%; background: #f44336;" onclick="deleteCamera(\\'' + camera.deviceId + '\\')">Delete Camera</button></div>';
|
|
43024
43107
|
}
|
|
43025
43108
|
|
|
@@ -43038,6 +43121,36 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43038
43121
|
camera.fov[field] = parseFloat(value);
|
|
43039
43122
|
render();
|
|
43040
43123
|
}
|
|
43124
|
+
function updateCameraFovRange(id, feetValue) {
|
|
43125
|
+
// Convert feet to pixels and store
|
|
43126
|
+
const camera = topology.cameras.find(c => c.deviceId === id);
|
|
43127
|
+
if (!camera) return;
|
|
43128
|
+
if (!camera.fov) camera.fov = { mode: 'simple', angle: 90, direction: 0, range: 80 };
|
|
43129
|
+
camera.fov.range = feetToPixels(parseFloat(feetValue));
|
|
43130
|
+
render();
|
|
43131
|
+
// Update the pixel display
|
|
43132
|
+
showCameraProperties(camera);
|
|
43133
|
+
}
|
|
43134
|
+
function updateScale(value) {
|
|
43135
|
+
floorPlanScale = parseFloat(value) || 5;
|
|
43136
|
+
// Store in topology for persistence
|
|
43137
|
+
topology.floorPlanScale = floorPlanScale;
|
|
43138
|
+
render();
|
|
43139
|
+
setStatus('Scale updated: ' + floorPlanScale + ' pixels per foot', 'success');
|
|
43140
|
+
}
|
|
43141
|
+
function openScaleHelper() {
|
|
43142
|
+
alert('How to determine your floor plan scale:\\n\\n' +
|
|
43143
|
+
'1. Measure a known distance on your floor plan in pixels\\n' +
|
|
43144
|
+
' (e.g., measure a room that you know is 20 feet wide)\\n\\n' +
|
|
43145
|
+
'2. Divide the pixel width by the real width in feet\\n' +
|
|
43146
|
+
' Example: 200 pixels / 20 feet = 10 pixels per foot\\n\\n' +
|
|
43147
|
+
'3. Enter that value in the scale field\\n\\n' +
|
|
43148
|
+
'Common scales:\\n' +
|
|
43149
|
+
'- Small floor plan (fits on screen): 3-5 px/ft\\n' +
|
|
43150
|
+
'- Medium floor plan: 5-10 px/ft\\n' +
|
|
43151
|
+
'- Large/detailed floor plan: 10-20 px/ft\\n\\n' +
|
|
43152
|
+
'Tip: Most outdoor cameras see 30-50 feet, indoor 15-30 feet');
|
|
43153
|
+
}
|
|
43041
43154
|
function updateConnectionName(id, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.name = value; updateUI(); }
|
|
43042
43155
|
function updateTransitTime(id, field, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.transitTime[field] = parseInt(value) * 1000; }
|
|
43043
43156
|
function updateConnectionBidi(id, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.bidirectional = value; render(); }
|