@blueharford/scrypted-spatial-awareness 0.4.8-beta.1 → 0.5.0-beta

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/src/main.ts CHANGED
@@ -36,6 +36,8 @@ import { MqttPublisher, MqttConfig } from './integrations/mqtt-publisher';
36
36
  import { EDITOR_HTML } from './ui/editor-html';
37
37
  import { TRAINING_HTML } from './ui/training-html';
38
38
  import { TrainingConfig, TrainingLandmark } from './models/training';
39
+ import { TopologyDiscoveryEngine } from './core/topology-discovery';
40
+ import { DiscoveryConfig, DiscoverySuggestion } from './models/discovery';
39
41
 
40
42
  const { deviceManager, systemManager, mediaManager } = sdk;
41
43
 
@@ -49,6 +51,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
49
51
  private trackingState: TrackingState;
50
52
  private alertManager: AlertManager;
51
53
  private mqttPublisher: MqttPublisher | null = null;
54
+ private discoveryEngine: TopologyDiscoveryEngine | null = null;
52
55
  private devices: Map<string, any> = new Map();
53
56
 
54
57
  storageSettings = new StorageSettings(this, {
@@ -169,6 +172,36 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
169
172
  group: 'AI & Spatial Reasoning',
170
173
  },
171
174
 
175
+ // Auto-Topology Discovery Settings
176
+ discoveryIntervalHours: {
177
+ title: 'Auto-Discovery Interval (hours)',
178
+ type: 'number',
179
+ defaultValue: 0,
180
+ description: 'Automatically scan cameras to discover landmarks and connections. Set to 0 to disable. Uses vision LLM to analyze camera views.',
181
+ group: 'Auto-Topology Discovery',
182
+ },
183
+ minLandmarkConfidence: {
184
+ title: 'Min Landmark Confidence',
185
+ type: 'number',
186
+ defaultValue: 0.6,
187
+ description: 'Minimum confidence (0-1) for discovered landmarks',
188
+ group: 'Auto-Topology Discovery',
189
+ },
190
+ minConnectionConfidence: {
191
+ title: 'Min Connection Confidence',
192
+ type: 'number',
193
+ defaultValue: 0.5,
194
+ description: 'Minimum confidence (0-1) for suggested camera connections',
195
+ group: 'Auto-Topology Discovery',
196
+ },
197
+ autoAcceptThreshold: {
198
+ title: 'Auto-Accept Threshold',
199
+ type: 'number',
200
+ defaultValue: 0.85,
201
+ description: 'Suggestions above this confidence are automatically accepted (0-1)',
202
+ group: 'Auto-Topology Discovery',
203
+ },
204
+
172
205
  // MQTT Settings
173
206
  enableMqtt: {
174
207
  title: 'Enable MQTT',
@@ -355,6 +388,33 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
355
388
 
356
389
  await this.trackingEngine.startTracking();
357
390
  this.console.log('Tracking engine started');
391
+
392
+ // Initialize or update discovery engine
393
+ await this.initializeDiscoveryEngine(topology);
394
+ }
395
+
396
+ private async initializeDiscoveryEngine(topology: CameraTopology): Promise<void> {
397
+ const discoveryConfig: DiscoveryConfig = {
398
+ discoveryIntervalHours: this.storageSettings.values.discoveryIntervalHours as number ?? 0,
399
+ autoAcceptThreshold: this.storageSettings.values.autoAcceptThreshold as number ?? 0.85,
400
+ minLandmarkConfidence: this.storageSettings.values.minLandmarkConfidence as number ?? 0.6,
401
+ minConnectionConfidence: this.storageSettings.values.minConnectionConfidence as number ?? 0.5,
402
+ };
403
+
404
+ if (this.discoveryEngine) {
405
+ // Update existing engine
406
+ this.discoveryEngine.updateConfig(discoveryConfig);
407
+ this.discoveryEngine.updateTopology(topology);
408
+ } else {
409
+ // Create new engine
410
+ this.discoveryEngine = new TopologyDiscoveryEngine(discoveryConfig, this.console);
411
+ this.discoveryEngine.updateTopology(topology);
412
+
413
+ // Start periodic discovery if enabled
414
+ if (discoveryConfig.discoveryIntervalHours > 0) {
415
+ this.discoveryEngine.startPeriodicDiscovery();
416
+ }
417
+ }
358
418
  }
359
419
 
360
420
  // ==================== DeviceProvider Implementation ====================
@@ -874,6 +934,27 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
874
934
  return this.handleTrainingApplyRequest(response);
875
935
  }
876
936
 
937
+ // Discovery endpoints
938
+ if (path.endsWith('/api/discovery/scan')) {
939
+ return this.handleDiscoveryScanRequest(response);
940
+ }
941
+ if (path.endsWith('/api/discovery/status')) {
942
+ return this.handleDiscoveryStatusRequest(response);
943
+ }
944
+ if (path.endsWith('/api/discovery/suggestions')) {
945
+ return this.handleDiscoverySuggestionsRequest(response);
946
+ }
947
+ if (path.match(/\/api\/discovery\/suggestions\/[\w-]+\/(accept|reject)$/)) {
948
+ const parts = path.split('/');
949
+ const action = parts.pop()!;
950
+ const suggestionId = parts.pop()!;
951
+ return this.handleDiscoverySuggestionActionRequest(suggestionId, action, response);
952
+ }
953
+ if (path.match(/\/api\/discovery\/camera\/[\w-]+$/)) {
954
+ const cameraId = path.split('/').pop()!;
955
+ return this.handleDiscoveryCameraAnalysisRequest(cameraId, response);
956
+ }
957
+
877
958
  // UI Routes
878
959
  if (path.endsWith('/ui/editor') || path.endsWith('/ui/editor/')) {
879
960
  return this.serveEditorUI(response);
@@ -890,7 +971,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
890
971
  // Default: return info page
891
972
  response.send(JSON.stringify({
892
973
  name: 'Spatial Awareness Plugin',
893
- version: '0.4.0',
974
+ version: '0.5.0-beta',
894
975
  endpoints: {
895
976
  api: {
896
977
  trackedObjects: '/api/tracked-objects',
@@ -911,6 +992,12 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
911
992
  landmark: '/api/training/landmark',
912
993
  apply: '/api/training/apply',
913
994
  },
995
+ discovery: {
996
+ scan: '/api/discovery/scan',
997
+ status: '/api/discovery/status',
998
+ suggestions: '/api/discovery/suggestions',
999
+ camera: '/api/discovery/camera/{cameraId}',
1000
+ },
914
1001
  },
915
1002
  ui: {
916
1003
  editor: '/ui/editor',
@@ -1581,6 +1668,207 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
1581
1668
  });
1582
1669
  }
1583
1670
 
1671
+ // ==================== Discovery Handlers ====================
1672
+
1673
+ private async handleDiscoveryScanRequest(response: HttpResponse): Promise<void> {
1674
+ if (!this.discoveryEngine) {
1675
+ response.send(JSON.stringify({ error: 'Discovery engine not initialized. Configure topology first.' }), {
1676
+ code: 500,
1677
+ headers: { 'Content-Type': 'application/json' },
1678
+ });
1679
+ return;
1680
+ }
1681
+
1682
+ try {
1683
+ this.console.log('[Discovery] Manual scan triggered via API');
1684
+ const correlation = await this.discoveryEngine.runFullDiscovery();
1685
+ const status = this.discoveryEngine.getStatus();
1686
+ const suggestions = this.discoveryEngine.getPendingSuggestions();
1687
+
1688
+ response.send(JSON.stringify({
1689
+ success: true,
1690
+ status,
1691
+ correlation,
1692
+ suggestions,
1693
+ }), {
1694
+ headers: { 'Content-Type': 'application/json' },
1695
+ });
1696
+ } catch (e) {
1697
+ this.console.error('[Discovery] Scan failed:', e);
1698
+ response.send(JSON.stringify({ error: `Scan failed: ${(e as Error).message}` }), {
1699
+ code: 500,
1700
+ headers: { 'Content-Type': 'application/json' },
1701
+ });
1702
+ }
1703
+ }
1704
+
1705
+ private handleDiscoveryStatusRequest(response: HttpResponse): void {
1706
+ if (!this.discoveryEngine) {
1707
+ response.send(JSON.stringify({
1708
+ isRunning: false,
1709
+ isScanning: false,
1710
+ lastScanTime: null,
1711
+ nextScanTime: null,
1712
+ camerasAnalyzed: 0,
1713
+ pendingSuggestions: 0,
1714
+ }), {
1715
+ headers: { 'Content-Type': 'application/json' },
1716
+ });
1717
+ return;
1718
+ }
1719
+
1720
+ const status = this.discoveryEngine.getStatus();
1721
+ response.send(JSON.stringify(status), {
1722
+ headers: { 'Content-Type': 'application/json' },
1723
+ });
1724
+ }
1725
+
1726
+ private handleDiscoverySuggestionsRequest(response: HttpResponse): void {
1727
+ if (!this.discoveryEngine) {
1728
+ response.send(JSON.stringify({ suggestions: [] }), {
1729
+ headers: { 'Content-Type': 'application/json' },
1730
+ });
1731
+ return;
1732
+ }
1733
+
1734
+ const suggestions = this.discoveryEngine.getPendingSuggestions();
1735
+ response.send(JSON.stringify({ suggestions }), {
1736
+ headers: { 'Content-Type': 'application/json' },
1737
+ });
1738
+ }
1739
+
1740
+ private handleDiscoverySuggestionActionRequest(
1741
+ suggestionId: string,
1742
+ action: string,
1743
+ response: HttpResponse
1744
+ ): void {
1745
+ if (!this.discoveryEngine) {
1746
+ response.send(JSON.stringify({ error: 'Discovery engine not initialized' }), {
1747
+ code: 500,
1748
+ headers: { 'Content-Type': 'application/json' },
1749
+ });
1750
+ return;
1751
+ }
1752
+
1753
+ if (action === 'accept') {
1754
+ const suggestion = this.discoveryEngine.acceptSuggestion(suggestionId);
1755
+ if (suggestion) {
1756
+ // Apply accepted suggestion to topology
1757
+ this.applyDiscoverySuggestion(suggestion);
1758
+ response.send(JSON.stringify({ success: true, suggestion }), {
1759
+ headers: { 'Content-Type': 'application/json' },
1760
+ });
1761
+ } else {
1762
+ response.send(JSON.stringify({ error: 'Suggestion not found' }), {
1763
+ code: 404,
1764
+ headers: { 'Content-Type': 'application/json' },
1765
+ });
1766
+ }
1767
+ } else if (action === 'reject') {
1768
+ const success = this.discoveryEngine.rejectSuggestion(suggestionId);
1769
+ if (success) {
1770
+ response.send(JSON.stringify({ success: true }), {
1771
+ headers: { 'Content-Type': 'application/json' },
1772
+ });
1773
+ } else {
1774
+ response.send(JSON.stringify({ error: 'Suggestion not found' }), {
1775
+ code: 404,
1776
+ headers: { 'Content-Type': 'application/json' },
1777
+ });
1778
+ }
1779
+ } else {
1780
+ response.send(JSON.stringify({ error: 'Invalid action' }), {
1781
+ code: 400,
1782
+ headers: { 'Content-Type': 'application/json' },
1783
+ });
1784
+ }
1785
+ }
1786
+
1787
+ private async handleDiscoveryCameraAnalysisRequest(
1788
+ cameraId: string,
1789
+ response: HttpResponse
1790
+ ): Promise<void> {
1791
+ if (!this.discoveryEngine) {
1792
+ response.send(JSON.stringify({ error: 'Discovery engine not initialized' }), {
1793
+ code: 500,
1794
+ headers: { 'Content-Type': 'application/json' },
1795
+ });
1796
+ return;
1797
+ }
1798
+
1799
+ try {
1800
+ const analysis = await this.discoveryEngine.analyzeScene(cameraId);
1801
+ response.send(JSON.stringify(analysis), {
1802
+ headers: { 'Content-Type': 'application/json' },
1803
+ });
1804
+ } catch (e) {
1805
+ response.send(JSON.stringify({ error: `Analysis failed: ${(e as Error).message}` }), {
1806
+ code: 500,
1807
+ headers: { 'Content-Type': 'application/json' },
1808
+ });
1809
+ }
1810
+ }
1811
+
1812
+ private applyDiscoverySuggestion(suggestion: DiscoverySuggestion): void {
1813
+ if (!this.trackingEngine) return;
1814
+
1815
+ const topology = this.trackingEngine.getTopology();
1816
+ let updated = false;
1817
+
1818
+ if (suggestion.type === 'landmark' && suggestion.landmark) {
1819
+ // Add new landmark to topology
1820
+ const landmark: Landmark = {
1821
+ id: `landmark_${Date.now()}`,
1822
+ name: suggestion.landmark.name!,
1823
+ type: suggestion.landmark.type!,
1824
+ position: suggestion.landmark.position || { x: 0, y: 0 },
1825
+ description: suggestion.landmark.description,
1826
+ visibleFromCameras: suggestion.landmark.visibleFromCameras,
1827
+ aiSuggested: true,
1828
+ aiConfidence: suggestion.confidence,
1829
+ };
1830
+
1831
+ if (!topology.landmarks) {
1832
+ topology.landmarks = [];
1833
+ }
1834
+ topology.landmarks.push(landmark);
1835
+ updated = true;
1836
+
1837
+ this.console.log(`[Discovery] Added landmark: ${landmark.name}`);
1838
+ }
1839
+
1840
+ if (suggestion.type === 'connection' && suggestion.connection) {
1841
+ // Add new connection to topology
1842
+ const conn = suggestion.connection;
1843
+ const newConnection = {
1844
+ id: `conn_${Date.now()}`,
1845
+ fromCameraId: conn.fromCameraId,
1846
+ toCameraId: conn.toCameraId,
1847
+ bidirectional: conn.bidirectional,
1848
+ // Default exit/entry zones covering full frame
1849
+ exitZone: [[0, 0], [100, 0], [100, 100], [0, 100]] as [number, number][],
1850
+ entryZone: [[0, 0], [100, 0], [100, 100], [0, 100]] as [number, number][],
1851
+ transitTime: {
1852
+ typical: conn.transitSeconds * 1000,
1853
+ min: Math.max(1000, conn.transitSeconds * 500),
1854
+ max: conn.transitSeconds * 2000,
1855
+ },
1856
+ name: conn.via ? `Via ${conn.via}` : undefined,
1857
+ };
1858
+
1859
+ topology.connections.push(newConnection);
1860
+ updated = true;
1861
+
1862
+ this.console.log(`[Discovery] Added connection: ${conn.fromCameraId} -> ${conn.toCameraId}`);
1863
+ }
1864
+
1865
+ if (updated) {
1866
+ // Save updated topology
1867
+ this.storage.setItem('topology', JSON.stringify(topology));
1868
+ this.trackingEngine.updateTopology(topology);
1869
+ }
1870
+ }
1871
+
1584
1872
  private serveEditorUI(response: HttpResponse): void {
1585
1873
  response.send(EDITOR_HTML, {
1586
1874
  headers: { 'Content-Type': 'text/html' },
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Auto-Topology Discovery Models
3
+ * Types for scene analysis and topology discovery via vision LLM
4
+ */
5
+
6
+ import { LandmarkType, Landmark, CameraConnection } from './topology';
7
+
8
+ // ==================== Discovery Configuration ====================
9
+
10
+ /** Configuration for the topology discovery engine */
11
+ export interface DiscoveryConfig {
12
+ /** Hours between automatic discovery scans (0 = disabled) */
13
+ discoveryIntervalHours: number;
14
+ /** Minimum confidence threshold for auto-accepting suggestions */
15
+ autoAcceptThreshold: number;
16
+ /** Minimum confidence for landmark suggestions */
17
+ minLandmarkConfidence: number;
18
+ /** Minimum confidence for connection suggestions */
19
+ minConnectionConfidence: number;
20
+ }
21
+
22
+ /** Default discovery configuration */
23
+ export const DEFAULT_DISCOVERY_CONFIG: DiscoveryConfig = {
24
+ discoveryIntervalHours: 0, // Disabled by default
25
+ autoAcceptThreshold: 0.85,
26
+ minLandmarkConfidence: 0.6,
27
+ minConnectionConfidence: 0.5,
28
+ };
29
+
30
+ /** Rate limit warning thresholds (in hours) */
31
+ export const RATE_LIMIT_WARNING_THRESHOLD = 1; // Warn if interval is less than 1 hour
32
+
33
+ // ==================== Scene Analysis Types ====================
34
+
35
+ /** Zone types that can be discovered in camera views */
36
+ export type DiscoveredZoneType =
37
+ | 'yard' // Front yard, back yard, side yard
38
+ | 'driveway' // Driveway, parking area
39
+ | 'street' // Street, road, sidewalk
40
+ | 'patio' // Patio, deck
41
+ | 'walkway' // Walkways, paths
42
+ | 'parking' // Parking lot, parking space
43
+ | 'garden' // Garden, landscaped area
44
+ | 'pool' // Pool area
45
+ | 'unknown'; // Unidentified area
46
+
47
+ /** A zone discovered in a camera view */
48
+ export interface DiscoveredZone {
49
+ /** Name of the zone (e.g., "Front Yard", "Driveway") */
50
+ name: string;
51
+ /** Type classification */
52
+ type: DiscoveredZoneType;
53
+ /** Estimated percentage of frame this zone covers (0-1) */
54
+ coverage: number;
55
+ /** Description from LLM analysis */
56
+ description: string;
57
+ /** Bounding box in normalized coordinates [x, y, width, height] (0-1) */
58
+ boundingBox?: [number, number, number, number];
59
+ }
60
+
61
+ /** A landmark discovered in a camera view */
62
+ export interface DiscoveredLandmark {
63
+ /** Name of the landmark */
64
+ name: string;
65
+ /** Type classification */
66
+ type: LandmarkType;
67
+ /** Confidence score from LLM (0-1) */
68
+ confidence: number;
69
+ /** Bounding box in normalized coordinates [x, y, width, height] (0-1) */
70
+ boundingBox?: [number, number, number, number];
71
+ /** Description from LLM analysis */
72
+ description: string;
73
+ }
74
+
75
+ /** Edge analysis - what's visible at frame boundaries */
76
+ export interface EdgeAnalysis {
77
+ /** What's visible at the top edge */
78
+ top: string;
79
+ /** What's visible at the left edge */
80
+ left: string;
81
+ /** What's visible at the right edge */
82
+ right: string;
83
+ /** What's visible at the bottom edge */
84
+ bottom: string;
85
+ }
86
+
87
+ /** Complete scene analysis result for a single camera */
88
+ export interface SceneAnalysis {
89
+ /** Camera device ID */
90
+ cameraId: string;
91
+ /** Camera name for reference */
92
+ cameraName: string;
93
+ /** When this analysis was performed */
94
+ timestamp: number;
95
+ /** Landmarks discovered in the scene */
96
+ landmarks: DiscoveredLandmark[];
97
+ /** Zones discovered in the scene */
98
+ zones: DiscoveredZone[];
99
+ /** Edge analysis for camera correlation */
100
+ edges: EdgeAnalysis;
101
+ /** Estimated camera facing direction */
102
+ orientation: 'north' | 'south' | 'east' | 'west' | 'northeast' | 'northwest' | 'southeast' | 'southwest' | 'unknown';
103
+ /** Camera IDs that may have overlapping views */
104
+ potentialOverlaps: string[];
105
+ /** Whether this analysis is still valid (not stale) */
106
+ isValid: boolean;
107
+ /** Error message if analysis failed */
108
+ error?: string;
109
+ }
110
+
111
+ // ==================== Correlation Types ====================
112
+
113
+ /** A landmark that appears in multiple camera views */
114
+ export interface SharedLandmark {
115
+ /** Suggested name for this landmark */
116
+ name: string;
117
+ /** Suggested type */
118
+ type: LandmarkType;
119
+ /** Camera IDs where this landmark is visible */
120
+ seenByCameras: string[];
121
+ /** Confidence in this correlation */
122
+ confidence: number;
123
+ /** Description of the shared landmark */
124
+ description?: string;
125
+ }
126
+
127
+ /** A suggested connection between cameras */
128
+ export interface SuggestedConnection {
129
+ /** Source camera ID */
130
+ fromCameraId: string;
131
+ /** Destination camera ID */
132
+ toCameraId: string;
133
+ /** Estimated transit time in seconds */
134
+ transitSeconds: number;
135
+ /** Path description (e.g., "via driveway", "through front yard") */
136
+ via: string;
137
+ /** Confidence in this suggestion */
138
+ confidence: number;
139
+ /** Whether this is bidirectional */
140
+ bidirectional: boolean;
141
+ }
142
+
143
+ /** Result of correlating scenes across multiple cameras */
144
+ export interface TopologyCorrelation {
145
+ /** Landmarks that appear in multiple camera views */
146
+ sharedLandmarks: SharedLandmark[];
147
+ /** Suggested connections between cameras */
148
+ suggestedConnections: SuggestedConnection[];
149
+ /** Overall description of property layout from LLM */
150
+ layoutDescription: string;
151
+ /** When this correlation was performed */
152
+ timestamp: number;
153
+ }
154
+
155
+ // ==================== Discovery Suggestions ====================
156
+
157
+ /** Status of a discovery suggestion */
158
+ export type SuggestionStatus = 'pending' | 'accepted' | 'rejected' | 'merged';
159
+
160
+ /** A pending discovery suggestion for user review */
161
+ export interface DiscoverySuggestion {
162
+ /** Unique ID for this suggestion */
163
+ id: string;
164
+ /** Type of suggestion */
165
+ type: 'landmark' | 'zone' | 'connection';
166
+ /** When this was discovered */
167
+ timestamp: number;
168
+ /** Cameras that contributed to this discovery */
169
+ sourceCameras: string[];
170
+ /** Confidence score */
171
+ confidence: number;
172
+ /** Current status */
173
+ status: SuggestionStatus;
174
+ /** The suggested landmark (if type is 'landmark') */
175
+ landmark?: Partial<Landmark>;
176
+ /** The suggested zone (if type is 'zone') */
177
+ zone?: DiscoveredZone;
178
+ /** The suggested connection (if type is 'connection') */
179
+ connection?: SuggestedConnection;
180
+ }
181
+
182
+ // ==================== Discovery Status ====================
183
+
184
+ /** Current status of the discovery engine */
185
+ export interface DiscoveryStatus {
186
+ /** Whether discovery is currently running */
187
+ isRunning: boolean;
188
+ /** Whether a scan is in progress */
189
+ isScanning: boolean;
190
+ /** Last scan timestamp */
191
+ lastScanTime: number | null;
192
+ /** Next scheduled scan timestamp */
193
+ nextScanTime: number | null;
194
+ /** Number of cameras analyzed */
195
+ camerasAnalyzed: number;
196
+ /** Number of pending suggestions */
197
+ pendingSuggestions: number;
198
+ /** Any error from last scan */
199
+ lastError?: string;
200
+ }
201
+
202
+ /** Default discovery status */
203
+ export const DEFAULT_DISCOVERY_STATUS: DiscoveryStatus = {
204
+ isRunning: false,
205
+ isScanning: false,
206
+ lastScanTime: null,
207
+ nextScanTime: null,
208
+ camerasAnalyzed: 0,
209
+ pendingSuggestions: 0,
210
+ };
@@ -198,6 +198,57 @@ export interface CameraZoneMapping {
198
198
  zone: ClipPath;
199
199
  }
200
200
 
201
+ // ==================== Drawn Zones (Floor Plan) ====================
202
+
203
+ /** Zone type for floor plan zones */
204
+ export type DrawnZoneType =
205
+ | 'yard' // Front yard, back yard, side yard
206
+ | 'driveway' // Driveway, parking area
207
+ | 'street' // Street, sidewalk
208
+ | 'patio' // Patio, deck
209
+ | 'walkway' // Walkways, paths
210
+ | 'parking' // Parking lot, parking space
211
+ | 'garden' // Garden, landscaped area
212
+ | 'pool' // Pool area
213
+ | 'garage' // Garage area
214
+ | 'entrance' // Entry areas
215
+ | 'custom'; // Custom zone type
216
+
217
+ /** Zone colors by type */
218
+ export const ZONE_TYPE_COLORS: Record<DrawnZoneType, string> = {
219
+ yard: 'rgba(76, 175, 80, 0.3)', // Green
220
+ driveway: 'rgba(158, 158, 158, 0.3)', // Gray
221
+ street: 'rgba(96, 96, 96, 0.3)', // Dark gray
222
+ patio: 'rgba(255, 152, 0, 0.3)', // Orange
223
+ walkway: 'rgba(121, 85, 72, 0.3)', // Brown
224
+ parking: 'rgba(189, 189, 189, 0.3)', // Light gray
225
+ garden: 'rgba(139, 195, 74, 0.3)', // Light green
226
+ pool: 'rgba(33, 150, 243, 0.3)', // Blue
227
+ garage: 'rgba(117, 117, 117, 0.3)', // Medium gray
228
+ entrance: 'rgba(233, 30, 99, 0.3)', // Pink
229
+ custom: 'rgba(156, 39, 176, 0.3)', // Purple
230
+ };
231
+
232
+ /** A zone drawn on the floor plan */
233
+ export interface DrawnZone {
234
+ /** Unique identifier */
235
+ id: string;
236
+ /** Display name */
237
+ name: string;
238
+ /** Zone type */
239
+ type: DrawnZoneType;
240
+ /** Polygon points on floor plan (x, y coordinates) */
241
+ polygon: Point[];
242
+ /** Custom color override (optional) */
243
+ color?: string;
244
+ /** Linked camera IDs that can see this zone */
245
+ linkedCameras?: string[];
246
+ /** Linked landmark IDs within this zone */
247
+ linkedLandmarks?: string[];
248
+ /** Description for context */
249
+ description?: string;
250
+ }
251
+
201
252
  // ==================== Spatial Relationships ====================
202
253
 
203
254
  /** Types of spatial relationships between entities */
@@ -297,6 +348,8 @@ export interface CameraTopology {
297
348
  relationships: SpatialRelationship[];
298
349
  /** Named zones spanning multiple cameras */
299
350
  globalZones: GlobalZone[];
351
+ /** Zones drawn on the floor plan */
352
+ drawnZones?: DrawnZone[];
300
353
  /** Floor plan configuration (optional) */
301
354
  floorPlan?: FloorPlanConfig;
302
355
  /** Pending AI landmark suggestions */