@blueharford/scrypted-spatial-awareness 0.6.16 → 0.6.19
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 +141 -76
- 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 +56 -7
- package/src/main.ts +49 -67
- package/src/models/topology.ts +3 -1
- package/src/ui/editor-html.ts +46 -8
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/out/main.nodejs.js
CHANGED
|
@@ -36236,7 +36236,10 @@ For EACH landmark, estimate its DISTANCE from the camera:
|
|
|
36236
36236
|
- Street/road
|
|
36237
36237
|
- Neighbor's yard
|
|
36238
36238
|
|
|
36239
|
-
For each zone,
|
|
36239
|
+
For each zone, provide:
|
|
36240
|
+
- coverage: percentage of the image it covers (0.0 to 1.0)
|
|
36241
|
+
- distance: how far the CENTER of the zone is from camera ("close", "near", "medium", "far", "distant")
|
|
36242
|
+
- boundingBox: [x, y, width, height] in normalized coordinates (0-1) where the zone appears in the image
|
|
36240
36243
|
|
|
36241
36244
|
## 3. EDGES - What's at each edge of the frame:
|
|
36242
36245
|
This helps understand what's just out of view.
|
|
@@ -36254,8 +36257,9 @@ Respond with ONLY valid JSON:
|
|
|
36254
36257
|
{"name": "Large Oak Tree", "type": "feature", "distance": "far", "confidence": 0.85, "description": "Mature oak tree near property line, roughly 80 feet from camera"}
|
|
36255
36258
|
],
|
|
36256
36259
|
"zones": [
|
|
36257
|
-
{"name": "Front Yard", "type": "yard", "coverage": 0.5, "description": "Grass lawn with some bare patches"},
|
|
36258
|
-
{"name": "Driveway", "type": "driveway", "coverage": 0.25, "description": "Concrete driveway leading to garage"}
|
|
36260
|
+
{"name": "Front Yard", "type": "yard", "coverage": 0.5, "distance": "medium", "boundingBox": [0.2, 0.4, 0.6, 0.4], "description": "Grass lawn with some bare patches"},
|
|
36261
|
+
{"name": "Driveway", "type": "driveway", "coverage": 0.25, "distance": "near", "boundingBox": [0.6, 0.5, 0.3, 0.4], "description": "Concrete driveway leading to garage"},
|
|
36262
|
+
{"name": "Street", "type": "street", "coverage": 0.1, "distance": "distant", "boundingBox": [0.0, 0.1, 1.0, 0.15], "description": "Public road beyond property line"}
|
|
36259
36263
|
],
|
|
36260
36264
|
"edges": {
|
|
36261
36265
|
"top": "sky, tree canopy",
|
|
@@ -36481,13 +36485,26 @@ class TopologyDiscoveryEngine {
|
|
|
36481
36485
|
for (const formatType of formatsToTry) {
|
|
36482
36486
|
try {
|
|
36483
36487
|
this.console.log(`[Discovery] Trying ${formatType} image format for ${cameraName}...`);
|
|
36488
|
+
// Build prompt with camera context (height)
|
|
36489
|
+
const cameraNode = this.topology ? (0, topology_1.findCamera)(this.topology, cameraId) : null;
|
|
36490
|
+
const mountHeight = cameraNode?.context?.mountHeight || 8;
|
|
36491
|
+
const cameraRange = cameraNode?.fov?.range || 80;
|
|
36492
|
+
// Add camera-specific context to the prompt
|
|
36493
|
+
const contextPrefix = `CAMERA INFORMATION:
|
|
36494
|
+
- Camera Name: ${cameraName}
|
|
36495
|
+
- Mount Height: ${mountHeight} feet above ground
|
|
36496
|
+
- Approximate viewing range: ${cameraRange} feet
|
|
36497
|
+
|
|
36498
|
+
Use the mount height to help estimate distances - objects at ground level will appear at different angles depending on distance from a camera mounted at ${mountHeight} feet.
|
|
36499
|
+
|
|
36500
|
+
`;
|
|
36484
36501
|
// Build multimodal message with provider-specific image format
|
|
36485
36502
|
const result = await llm.getChatCompletion({
|
|
36486
36503
|
messages: [
|
|
36487
36504
|
{
|
|
36488
36505
|
role: 'user',
|
|
36489
36506
|
content: [
|
|
36490
|
-
{ type: 'text', text: SCENE_ANALYSIS_PROMPT },
|
|
36507
|
+
{ type: 'text', text: contextPrefix + SCENE_ANALYSIS_PROMPT },
|
|
36491
36508
|
(0, spatial_reasoning_1.buildImageContent)(imageData, formatType),
|
|
36492
36509
|
],
|
|
36493
36510
|
},
|
|
@@ -36523,6 +36540,7 @@ class TopologyDiscoveryEngine {
|
|
|
36523
36540
|
coverage: typeof z.coverage === 'number' ? z.coverage : 0.5,
|
|
36524
36541
|
description: z.description || '',
|
|
36525
36542
|
boundingBox: z.boundingBox,
|
|
36543
|
+
distance: this.mapDistance(z.distance), // Parse distance for zones too
|
|
36526
36544
|
}));
|
|
36527
36545
|
}
|
|
36528
36546
|
if (parsed.edges && typeof parsed.edges === 'object') {
|
|
@@ -36644,6 +36662,26 @@ class TopologyDiscoveryEngine {
|
|
|
36644
36662
|
return 'distant';
|
|
36645
36663
|
return 'medium'; // Default to medium if not specified
|
|
36646
36664
|
}
|
|
36665
|
+
/** Get default distance in feet based on zone type */
|
|
36666
|
+
getDefaultZoneDistance(zoneType) {
|
|
36667
|
+
switch (zoneType) {
|
|
36668
|
+
case 'patio':
|
|
36669
|
+
case 'walkway':
|
|
36670
|
+
return 10; // Close zones
|
|
36671
|
+
case 'driveway':
|
|
36672
|
+
case 'parking':
|
|
36673
|
+
return 25; // Near zones
|
|
36674
|
+
case 'yard':
|
|
36675
|
+
case 'garden':
|
|
36676
|
+
case 'pool':
|
|
36677
|
+
return 40; // Medium zones
|
|
36678
|
+
case 'street':
|
|
36679
|
+
return 100; // Far zones
|
|
36680
|
+
case 'unknown':
|
|
36681
|
+
default:
|
|
36682
|
+
return 50; // Default to medium distance
|
|
36683
|
+
}
|
|
36684
|
+
}
|
|
36647
36685
|
/** Try to parse JSON with recovery for truncated responses */
|
|
36648
36686
|
parseJsonWithRecovery(jsonStr, context) {
|
|
36649
36687
|
// First, try direct parse
|
|
@@ -36937,6 +36975,9 @@ class TopologyDiscoveryEngine {
|
|
|
36937
36975
|
// Generate zone suggestions (even for smaller coverage - 10% is enough)
|
|
36938
36976
|
for (const zone of analysis.zones) {
|
|
36939
36977
|
if (zone.coverage >= 0.1) {
|
|
36978
|
+
// Calculate distance in feet from distance estimate (for zones with distance info)
|
|
36979
|
+
const zoneWithDist = zone;
|
|
36980
|
+
const distanceFeet = zoneWithDist.distance ? (0, discovery_1.distanceToFeet)(zoneWithDist.distance) : this.getDefaultZoneDistance(zone.type);
|
|
36940
36981
|
const suggestion = {
|
|
36941
36982
|
id: `zone_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
36942
36983
|
type: 'zone',
|
|
@@ -36944,10 +36985,14 @@ class TopologyDiscoveryEngine {
|
|
|
36944
36985
|
sourceCameras: [analysis.cameraId],
|
|
36945
36986
|
confidence: Math.min(0.9, 0.5 + zone.coverage), // Higher coverage = higher confidence
|
|
36946
36987
|
status: 'pending',
|
|
36947
|
-
zone:
|
|
36988
|
+
zone: {
|
|
36989
|
+
...zone,
|
|
36990
|
+
// Include distance metadata for positioning
|
|
36991
|
+
distanceFeet: distanceFeet,
|
|
36992
|
+
},
|
|
36948
36993
|
};
|
|
36949
36994
|
this.suggestions.set(suggestion.id, suggestion);
|
|
36950
|
-
this.console.log(`[Discovery] Zone suggestion: ${zone.name} (${zone.type}, ${Math.round(zone.coverage * 100)}% coverage)`);
|
|
36995
|
+
this.console.log(`[Discovery] Zone suggestion: ${zone.name} (${zone.type}, ${Math.round(zone.coverage * 100)}% coverage, ~${distanceFeet}ft)`);
|
|
36951
36996
|
}
|
|
36952
36997
|
}
|
|
36953
36998
|
}
|
|
@@ -40771,61 +40816,38 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40771
40816
|
// Convert direction to radians (0 = up/north, 90 = right/east)
|
|
40772
40817
|
const dirRad = (direction - 90) * Math.PI / 180;
|
|
40773
40818
|
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
40774
|
-
//
|
|
40775
|
-
|
|
40776
|
-
|
|
40777
|
-
|
|
40778
|
-
const
|
|
40779
|
-
|
|
40780
|
-
|
|
40781
|
-
|
|
40819
|
+
// Get floor plan scale (pixels per foot) - default 5
|
|
40820
|
+
const floorPlanScale = topology.floorPlanScale || 5;
|
|
40821
|
+
// Use the ACTUAL distance from LLM analysis (distanceFeet) - this is the key fix!
|
|
40822
|
+
// The LLM estimates distance based on object size, perspective, and camera context
|
|
40823
|
+
const landmarkData = suggestion.landmark;
|
|
40824
|
+
const distanceFeet = landmarkData.distanceFeet || 50; // Default 50ft if not set
|
|
40825
|
+
const distanceInPixels = distanceFeet * floorPlanScale;
|
|
40826
|
+
// Use bounding box for horizontal positioning within the FOV
|
|
40827
|
+
const bbox = landmarkData.boundingBox;
|
|
40782
40828
|
let angleOffset;
|
|
40783
|
-
let distanceMultiplier;
|
|
40784
40829
|
if (bbox && bbox.length >= 2) {
|
|
40785
40830
|
// Use bounding box X for horizontal position in FOV
|
|
40786
40831
|
const bboxCenterX = bbox[0] + (bbox[2] || 0) / 2; // 0 = left edge, 1 = right edge
|
|
40787
40832
|
// Map X position to angle within FOV
|
|
40788
40833
|
angleOffset = (bboxCenterX - 0.5) * 2 * halfFov;
|
|
40789
|
-
|
|
40790
|
-
if (isFarType) {
|
|
40791
|
-
// Neighbors, boundaries, streets are BEYOND the camera's normal range
|
|
40792
|
-
// Place at 150-200% of range to indicate they're distant background features
|
|
40793
|
-
distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
|
|
40794
|
-
this.console.log(`[Discovery] Far-type landmark "${landmarkType}" placed BEYOND FOV (${(distanceMultiplier * 100).toFixed(0)}% of range)`);
|
|
40795
|
-
}
|
|
40796
|
-
else {
|
|
40797
|
-
// For other types, use bbox Y as a hint (but cap minimum distance)
|
|
40798
|
-
const bboxCenterY = bbox[1] + (bbox[3] || 0) / 2;
|
|
40799
|
-
// Map Y to distance: 0 (top) = far, 1 (bottom) = closer (but not too close)
|
|
40800
|
-
distanceMultiplier = Math.max(0.4, 0.9 - (bboxCenterY * 0.5));
|
|
40801
|
-
}
|
|
40802
|
-
this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal position`);
|
|
40834
|
+
this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal angle offset`);
|
|
40803
40835
|
}
|
|
40804
40836
|
else {
|
|
40805
|
-
// No bounding box -
|
|
40837
|
+
// No bounding box - spread horizontally based on existing landmarks
|
|
40806
40838
|
const cameraDeviceId = camera.deviceId;
|
|
40807
40839
|
const existingFromCamera = (topology.landmarks || []).filter(l => l.visibleFromCameras?.includes(cameraDeviceId) ||
|
|
40808
40840
|
l.visibleFromCameras?.includes(camera.name)).length;
|
|
40809
40841
|
// Spread horizontally across FOV
|
|
40810
40842
|
angleOffset = (existingFromCamera % 3 - 1) * halfFov * 0.6;
|
|
40811
|
-
|
|
40812
|
-
if (isFarType) {
|
|
40813
|
-
// Neighbors, boundaries, streets are BEYOND the camera's normal range
|
|
40814
|
-
distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
|
|
40815
|
-
this.console.log(`[Discovery] Far-type landmark "${landmarkType}" (no bbox) placed BEYOND FOV`);
|
|
40816
|
-
}
|
|
40817
|
-
else {
|
|
40818
|
-
distanceMultiplier = 0.5 + (existingFromCamera % 2) * 0.3;
|
|
40819
|
-
this.console.log(`[Discovery] No bounding box, using fallback spread (existing: ${existingFromCamera})`);
|
|
40820
|
-
}
|
|
40843
|
+
this.console.log(`[Discovery] No bounding box, spreading horizontally (existing: ${existingFromCamera})`);
|
|
40821
40844
|
}
|
|
40822
40845
|
const finalAngle = dirRad + angleOffset;
|
|
40823
|
-
const distance = range * distanceMultiplier;
|
|
40824
40846
|
position = {
|
|
40825
|
-
x: camera.floorPlanPosition.x + Math.cos(finalAngle) *
|
|
40826
|
-
y: camera.floorPlanPosition.y + Math.sin(finalAngle) *
|
|
40847
|
+
x: camera.floorPlanPosition.x + Math.cos(finalAngle) * distanceInPixels,
|
|
40848
|
+
y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distanceInPixels,
|
|
40827
40849
|
};
|
|
40828
|
-
this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}"
|
|
40850
|
+
this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}" at ${distanceFeet}ft (${distanceInPixels}px) from ${camera.name}: dir=${direction}°, angle=${(angleOffset * 180 / Math.PI).toFixed(1)}°`);
|
|
40829
40851
|
}
|
|
40830
40852
|
else {
|
|
40831
40853
|
// Position in a grid pattern starting from center
|
|
@@ -40878,48 +40900,53 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40878
40900
|
let polygon = [];
|
|
40879
40901
|
const timestamp = Date.now();
|
|
40880
40902
|
if (camera?.floorPlanPosition) {
|
|
40881
|
-
// Get camera's FOV direction
|
|
40903
|
+
// Get camera's FOV direction (cast to any for flexible access)
|
|
40882
40904
|
const fov = (camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 });
|
|
40883
40905
|
const direction = fov.direction || 0;
|
|
40884
|
-
const range = fov.range || 80;
|
|
40885
40906
|
const fovAngle = fov.angle || 90;
|
|
40907
|
+
// Get floor plan scale (pixels per foot)
|
|
40908
|
+
const floorPlanScale = topology.floorPlanScale || 5;
|
|
40886
40909
|
// Convert direction to radians (0 = up/north, 90 = right/east)
|
|
40887
40910
|
const dirRad = (direction - 90) * Math.PI / 180;
|
|
40888
40911
|
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
40889
40912
|
const camX = camera.floorPlanPosition.x;
|
|
40890
40913
|
const camY = camera.floorPlanPosition.y;
|
|
40891
|
-
// Use
|
|
40914
|
+
// Use distanceFeet from the zone metadata for accurate positioning
|
|
40915
|
+
const zoneData = zone;
|
|
40916
|
+
const distanceFeet = zoneData.distanceFeet || 40; // Default 40ft if not set
|
|
40917
|
+
const distanceInPixels = distanceFeet * floorPlanScale;
|
|
40918
|
+
// Zone size based on coverage (larger coverage = wider zone)
|
|
40919
|
+
const zoneWidthFeet = Math.sqrt(zone.coverage) * 30; // e.g., 50% coverage = ~21ft wide
|
|
40920
|
+
const zoneWidthPixels = zoneWidthFeet * floorPlanScale;
|
|
40921
|
+
const zoneDepthPixels = (zone.coverage * 20) * floorPlanScale; // Depth based on coverage
|
|
40922
|
+
// Use bounding box for horizontal positioning if available
|
|
40892
40923
|
const bbox = zone.boundingBox; // [x, y, width, height] normalized 0-1
|
|
40893
|
-
let innerRadius;
|
|
40894
|
-
let outerRadius;
|
|
40895
40924
|
let angleStart;
|
|
40896
40925
|
let angleEnd;
|
|
40926
|
+
let innerRadius;
|
|
40927
|
+
let outerRadius;
|
|
40897
40928
|
if (bbox && bbox.length >= 4) {
|
|
40898
|
-
// Map bounding box to
|
|
40929
|
+
// Map bounding box X to angle within FOV
|
|
40899
40930
|
const bboxLeft = bbox[0];
|
|
40900
40931
|
const bboxRight = bbox[0] + bbox[2];
|
|
40901
|
-
const bboxTop = bbox[1];
|
|
40902
|
-
const bboxBottom = bbox[1] + bbox[3];
|
|
40903
|
-
// Map X to angle within FOV (0 = left edge, 1 = right edge)
|
|
40904
40932
|
angleStart = dirRad + (bboxLeft - 0.5) * 2 * halfFov;
|
|
40905
40933
|
angleEnd = dirRad + (bboxRight - 0.5) * 2 * halfFov;
|
|
40906
|
-
//
|
|
40907
|
-
innerRadius =
|
|
40908
|
-
outerRadius =
|
|
40909
|
-
|
|
40910
|
-
if (outerRadius - innerRadius < 20) {
|
|
40911
|
-
outerRadius = innerRadius + 20;
|
|
40912
|
-
}
|
|
40913
|
-
this.console.log(`[Discovery] Zone "${zone.name}" using bbox [${bbox.join(',')}] → angles ${(angleStart * 180 / Math.PI).toFixed(1)}° to ${(angleEnd * 180 / Math.PI).toFixed(1)}°`);
|
|
40934
|
+
// Use distanceFeet for depth, with a spread based on coverage
|
|
40935
|
+
innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
|
|
40936
|
+
outerRadius = distanceInPixels + zoneDepthPixels / 2;
|
|
40937
|
+
this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using bbox [${bbox.join(',')}]`);
|
|
40914
40938
|
}
|
|
40915
40939
|
else {
|
|
40916
|
-
// Fallback: wedge-shaped zone
|
|
40940
|
+
// Fallback: wedge-shaped zone covering portion of FOV
|
|
40917
40941
|
const existingFromCamera = (topology.drawnZones || []).filter((z) => z.linkedCameras?.includes(sourceCameras[0])).length;
|
|
40918
|
-
|
|
40919
|
-
|
|
40920
|
-
angleStart = dirRad - halfFov * 0.
|
|
40921
|
-
angleEnd = dirRad + halfFov * 0.
|
|
40922
|
-
|
|
40942
|
+
// Spread horizontally based on existing zones
|
|
40943
|
+
const offset = (existingFromCamera % 3 - 1) * halfFov * 0.5;
|
|
40944
|
+
angleStart = dirRad + offset - halfFov * 0.3;
|
|
40945
|
+
angleEnd = dirRad + offset + halfFov * 0.3;
|
|
40946
|
+
// Use distanceFeet for depth
|
|
40947
|
+
innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
|
|
40948
|
+
outerRadius = distanceInPixels + zoneDepthPixels / 2;
|
|
40949
|
+
this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using fallback spread`);
|
|
40923
40950
|
}
|
|
40924
40951
|
// Create arc polygon
|
|
40925
40952
|
const steps = 8;
|
|
@@ -40939,7 +40966,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40939
40966
|
y: camY + Math.sin(angle) * outerRadius,
|
|
40940
40967
|
});
|
|
40941
40968
|
}
|
|
40942
|
-
this.console.log(`[Discovery] Creating zone "${zone.name}"
|
|
40969
|
+
this.console.log(`[Discovery] Creating zone "${zone.name}" at ${distanceFeet}ft (${distanceInPixels}px) from ${camera.name}`);
|
|
40943
40970
|
}
|
|
40944
40971
|
else {
|
|
40945
40972
|
// Fallback: rectangular zone at default location
|
|
@@ -42541,6 +42568,7 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42541
42568
|
const response = await fetch('../api/topology');
|
|
42542
42569
|
if (response.ok) {
|
|
42543
42570
|
topology = await response.json();
|
|
42571
|
+
console.log('[Load] Loaded topology with', (topology.drawnZones || []).length, 'zones');
|
|
42544
42572
|
if (!topology.drawings) topology.drawings = [];
|
|
42545
42573
|
// Load floor plan scale if saved
|
|
42546
42574
|
if (topology.floorPlanScale) {
|
|
@@ -43058,14 +43086,24 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43058
43086
|
async function saveTopology() {
|
|
43059
43087
|
try {
|
|
43060
43088
|
setStatus('Saving...', 'warning');
|
|
43089
|
+
console.log('[Save] Saving topology with', (topology.drawnZones || []).length, 'zones');
|
|
43061
43090
|
const response = await fetch('../api/topology', {
|
|
43062
43091
|
method: 'PUT',
|
|
43063
43092
|
headers: { 'Content-Type': 'application/json' },
|
|
43064
43093
|
body: JSON.stringify(topology)
|
|
43065
43094
|
});
|
|
43066
|
-
if (response.ok) {
|
|
43067
|
-
|
|
43068
|
-
|
|
43095
|
+
if (response.ok) {
|
|
43096
|
+
console.log('[Save] Save successful');
|
|
43097
|
+
setStatus('Saved successfully', 'success');
|
|
43098
|
+
} else {
|
|
43099
|
+
const errorText = await response.text();
|
|
43100
|
+
console.error('[Save] Save failed:', response.status, errorText);
|
|
43101
|
+
setStatus('Failed to save: ' + errorText, 'error');
|
|
43102
|
+
}
|
|
43103
|
+
} catch (e) {
|
|
43104
|
+
console.error('Failed to save topology:', e);
|
|
43105
|
+
setStatus('Failed to save', 'error');
|
|
43106
|
+
}
|
|
43069
43107
|
}
|
|
43070
43108
|
|
|
43071
43109
|
function resizeCanvas() {
|
|
@@ -43719,8 +43757,12 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43719
43757
|
const fov = camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 };
|
|
43720
43758
|
// Convert stored pixel range to feet for display
|
|
43721
43759
|
const rangeInFeet = Math.round(pixelsToFeet(fov.range || 80));
|
|
43760
|
+
// Get mount height from context or default to 8
|
|
43761
|
+
const mountHeight = camera.context?.mountHeight || 8;
|
|
43722
43762
|
panel.innerHTML = '<h3>Camera Properties</h3>' +
|
|
43723
43763
|
'<div class="form-group"><label>Name</label><input type="text" value="' + camera.name + '" onchange="updateCameraName(\\'' + camera.deviceId + '\\', this.value)"></div>' +
|
|
43764
|
+
'<div class="form-group"><label>Mount Height (feet)</label><input type="number" value="' + mountHeight + '" min="1" max="40" step="0.5" onchange="updateCameraMountHeight(\\'' + camera.deviceId + '\\', this.value)"></div>' +
|
|
43765
|
+
'<div style="font-size: 11px; color: #666; margin-top: -10px; margin-bottom: 10px;">Height affects distance estimation in discovery</div>' +
|
|
43724
43766
|
'<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isEntryPoint ? 'checked' : '') + ' onchange="updateCameraEntry(\\'' + camera.deviceId + '\\', this.checked)">Entry Point</label></div>' +
|
|
43725
43767
|
'<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isExitPoint ? 'checked' : '') + ' onchange="updateCameraExit(\\'' + camera.deviceId + '\\', this.checked)">Exit Point</label></div>' +
|
|
43726
43768
|
'<h4 style="margin-top: 15px; margin-bottom: 10px; color: #888;">Field of View</h4>' +
|
|
@@ -43739,6 +43781,12 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43739
43781
|
function updateCameraName(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.name = value; updateUI(); }
|
|
43740
43782
|
function updateCameraEntry(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isEntryPoint = value; }
|
|
43741
43783
|
function updateCameraExit(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isExitPoint = value; }
|
|
43784
|
+
function updateCameraMountHeight(id, value) {
|
|
43785
|
+
const camera = topology.cameras.find(c => c.deviceId === id);
|
|
43786
|
+
if (!camera) return;
|
|
43787
|
+
if (!camera.context) camera.context = {};
|
|
43788
|
+
camera.context.mountHeight = parseFloat(value) || 8;
|
|
43789
|
+
}
|
|
43742
43790
|
function updateCameraFov(id, field, value) {
|
|
43743
43791
|
const camera = topology.cameras.find(c => c.deviceId === id);
|
|
43744
43792
|
if (!camera) return;
|
|
@@ -43855,9 +43903,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43855
43903
|
polygon: currentZonePoints.map(pt => ({ x: pt.x, y: pt.y })),
|
|
43856
43904
|
};
|
|
43857
43905
|
|
|
43906
|
+
console.log('[Zone] Creating zone:', zone.name, 'with', zone.polygon.length, 'points');
|
|
43907
|
+
|
|
43858
43908
|
if (!topology.drawnZones) topology.drawnZones = [];
|
|
43859
43909
|
topology.drawnZones.push(zone);
|
|
43860
43910
|
|
|
43911
|
+
console.log('[Zone] Total zones now:', topology.drawnZones.length);
|
|
43912
|
+
|
|
43861
43913
|
// Reset state
|
|
43862
43914
|
zoneDrawingMode = false;
|
|
43863
43915
|
currentZonePoints = [];
|
|
@@ -43866,7 +43918,19 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43866
43918
|
setTool('select');
|
|
43867
43919
|
updateUI();
|
|
43868
43920
|
render();
|
|
43869
|
-
|
|
43921
|
+
|
|
43922
|
+
console.log('[Zone] After updateUI/render, zones in topology:', (topology.drawnZones || []).length);
|
|
43923
|
+
|
|
43924
|
+
setStatus('Zone "' + zone.name + '" created - saving...', 'success');
|
|
43925
|
+
|
|
43926
|
+
// Auto-save after creating a zone
|
|
43927
|
+
saveTopology().then(() => {
|
|
43928
|
+
console.log('[Zone] Save completed, zones in topology:', (topology.drawnZones || []).length);
|
|
43929
|
+
setStatus('Zone "' + zone.name + '" saved', 'success');
|
|
43930
|
+
}).catch(err => {
|
|
43931
|
+
console.error('Failed to auto-save zone:', err);
|
|
43932
|
+
setStatus('Zone created but auto-save failed - click Save', 'warning');
|
|
43933
|
+
});
|
|
43870
43934
|
}
|
|
43871
43935
|
|
|
43872
43936
|
function selectZone(id) {
|
|
@@ -43891,9 +43955,9 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43891
43955
|
'<button class="btn btn-primary" onclick="deleteZone(\\'' + id + '\\')" style="background: #dc2626; width: 100%;">Delete Zone</button>';
|
|
43892
43956
|
}
|
|
43893
43957
|
|
|
43894
|
-
function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); } }
|
|
43895
|
-
function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); } }
|
|
43896
|
-
function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; } }
|
|
43958
|
+
function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); saveTopology(); } }
|
|
43959
|
+
function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); saveTopology(); } }
|
|
43960
|
+
function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; saveTopology(); } }
|
|
43897
43961
|
function deleteZone(id) {
|
|
43898
43962
|
if (!confirm('Delete this zone?')) return;
|
|
43899
43963
|
topology.drawnZones = (topology.drawnZones || []).filter(z => z.id !== id);
|
|
@@ -43901,7 +43965,8 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43901
43965
|
document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select an item to edit.</p>';
|
|
43902
43966
|
updateUI();
|
|
43903
43967
|
render();
|
|
43904
|
-
setStatus('Zone deleted', 'success');
|
|
43968
|
+
setStatus('Zone deleted - saving...', 'success');
|
|
43969
|
+
saveTopology().then(() => setStatus('Zone deleted', 'success'));
|
|
43905
43970
|
}
|
|
43906
43971
|
|
|
43907
43972
|
function useBlankCanvas() {
|