@blueharford/scrypted-spatial-awareness 0.6.17 → 0.6.20
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 +125 -75
- 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 +40 -6
- package/src/main.ts +55 -67
- package/src/models/topology.ts +3 -1
- package/src/ui/editor-html.ts +38 -8
package/out/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -105,7 +105,10 @@ For EACH landmark, estimate its DISTANCE from the camera:
|
|
|
105
105
|
- Street/road
|
|
106
106
|
- Neighbor's yard
|
|
107
107
|
|
|
108
|
-
For each zone,
|
|
108
|
+
For each zone, provide:
|
|
109
|
+
- coverage: percentage of the image it covers (0.0 to 1.0)
|
|
110
|
+
- distance: how far the CENTER of the zone is from camera ("close", "near", "medium", "far", "distant")
|
|
111
|
+
- boundingBox: [x, y, width, height] in normalized coordinates (0-1) where the zone appears in the image
|
|
109
112
|
|
|
110
113
|
## 3. EDGES - What's at each edge of the frame:
|
|
111
114
|
This helps understand what's just out of view.
|
|
@@ -123,8 +126,9 @@ Respond with ONLY valid JSON:
|
|
|
123
126
|
{"name": "Large Oak Tree", "type": "feature", "distance": "far", "confidence": 0.85, "description": "Mature oak tree near property line, roughly 80 feet from camera"}
|
|
124
127
|
],
|
|
125
128
|
"zones": [
|
|
126
|
-
{"name": "Front Yard", "type": "yard", "coverage": 0.5, "description": "Grass lawn with some bare patches"},
|
|
127
|
-
{"name": "Driveway", "type": "driveway", "coverage": 0.25, "description": "Concrete driveway leading to garage"}
|
|
129
|
+
{"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"},
|
|
130
|
+
{"name": "Driveway", "type": "driveway", "coverage": 0.25, "distance": "near", "boundingBox": [0.6, 0.5, 0.3, 0.4], "description": "Concrete driveway leading to garage"},
|
|
131
|
+
{"name": "Street", "type": "street", "coverage": 0.1, "distance": "distant", "boundingBox": [0.0, 0.1, 1.0, 0.15], "description": "Public road beyond property line"}
|
|
128
132
|
],
|
|
129
133
|
"edges": {
|
|
130
134
|
"top": "sky, tree canopy",
|
|
@@ -440,7 +444,8 @@ Use the mount height to help estimate distances - objects at ground level will a
|
|
|
440
444
|
coverage: typeof z.coverage === 'number' ? z.coverage : 0.5,
|
|
441
445
|
description: z.description || '',
|
|
442
446
|
boundingBox: z.boundingBox,
|
|
443
|
-
|
|
447
|
+
distance: this.mapDistance(z.distance), // Parse distance for zones too
|
|
448
|
+
} as DiscoveredZone & { distance?: DistanceEstimate }));
|
|
444
449
|
}
|
|
445
450
|
|
|
446
451
|
if (parsed.edges && typeof parsed.edges === 'object') {
|
|
@@ -560,6 +565,27 @@ Use the mount height to help estimate distances - objects at ground level will a
|
|
|
560
565
|
return 'medium'; // Default to medium if not specified
|
|
561
566
|
}
|
|
562
567
|
|
|
568
|
+
/** Get default distance in feet based on zone type */
|
|
569
|
+
private getDefaultZoneDistance(zoneType: string): number {
|
|
570
|
+
switch (zoneType) {
|
|
571
|
+
case 'patio':
|
|
572
|
+
case 'walkway':
|
|
573
|
+
return 10; // Close zones
|
|
574
|
+
case 'driveway':
|
|
575
|
+
case 'parking':
|
|
576
|
+
return 25; // Near zones
|
|
577
|
+
case 'yard':
|
|
578
|
+
case 'garden':
|
|
579
|
+
case 'pool':
|
|
580
|
+
return 40; // Medium zones
|
|
581
|
+
case 'street':
|
|
582
|
+
return 100; // Far zones
|
|
583
|
+
case 'unknown':
|
|
584
|
+
default:
|
|
585
|
+
return 50; // Default to medium distance
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
563
589
|
/** Try to parse JSON with recovery for truncated responses */
|
|
564
590
|
private parseJsonWithRecovery(jsonStr: string, context: string): any {
|
|
565
591
|
// First, try direct parse
|
|
@@ -882,6 +908,10 @@ Use the mount height to help estimate distances - objects at ground level will a
|
|
|
882
908
|
// Generate zone suggestions (even for smaller coverage - 10% is enough)
|
|
883
909
|
for (const zone of analysis.zones) {
|
|
884
910
|
if (zone.coverage >= 0.1) {
|
|
911
|
+
// Calculate distance in feet from distance estimate (for zones with distance info)
|
|
912
|
+
const zoneWithDist = zone as DiscoveredZone & { distance?: DistanceEstimate };
|
|
913
|
+
const distanceFeet = zoneWithDist.distance ? distanceToFeet(zoneWithDist.distance) : this.getDefaultZoneDistance(zone.type);
|
|
914
|
+
|
|
885
915
|
const suggestion: DiscoverySuggestion = {
|
|
886
916
|
id: `zone_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
887
917
|
type: 'zone',
|
|
@@ -889,10 +919,14 @@ Use the mount height to help estimate distances - objects at ground level will a
|
|
|
889
919
|
sourceCameras: [analysis.cameraId],
|
|
890
920
|
confidence: Math.min(0.9, 0.5 + zone.coverage), // Higher coverage = higher confidence
|
|
891
921
|
status: 'pending',
|
|
892
|
-
zone:
|
|
922
|
+
zone: {
|
|
923
|
+
...zone,
|
|
924
|
+
// Include distance metadata for positioning
|
|
925
|
+
distanceFeet: distanceFeet,
|
|
926
|
+
} as any,
|
|
893
927
|
};
|
|
894
928
|
this.suggestions.set(suggestion.id, suggestion);
|
|
895
|
-
this.console.log(`[Discovery] Zone suggestion: ${zone.name} (${zone.type}, ${Math.round(zone.coverage * 100)}% coverage)`);
|
|
929
|
+
this.console.log(`[Discovery] Zone suggestion: ${zone.name} (${zone.type}, ${Math.round(zone.coverage * 100)}% coverage, ~${distanceFeet}ft)`);
|
|
896
930
|
}
|
|
897
931
|
}
|
|
898
932
|
}
|
package/src/main.ts
CHANGED
|
@@ -1062,18 +1062,24 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
1062
1062
|
if (request.method === 'GET') {
|
|
1063
1063
|
const topologyJson = this.storage.getItem('topology');
|
|
1064
1064
|
const topology = topologyJson ? JSON.parse(topologyJson) : createEmptyTopology();
|
|
1065
|
+
this.console.log(`[Topology API] GET - drawnZones: ${topology.drawnZones?.length || 0}`);
|
|
1065
1066
|
response.send(JSON.stringify(topology), {
|
|
1066
1067
|
headers: { 'Content-Type': 'application/json' },
|
|
1067
1068
|
});
|
|
1068
1069
|
} else if (request.method === 'PUT' || request.method === 'POST') {
|
|
1069
1070
|
try {
|
|
1070
1071
|
const topology = JSON.parse(request.body!) as CameraTopology;
|
|
1072
|
+
this.console.log(`[Topology API] PUT received - drawnZones: ${topology.drawnZones?.length || 0}`);
|
|
1073
|
+
if (topology.drawnZones?.length) {
|
|
1074
|
+
this.console.log(`[Topology API] Zone names: ${topology.drawnZones.map(z => z.name).join(', ')}`);
|
|
1075
|
+
}
|
|
1071
1076
|
this.storage.setItem('topology', JSON.stringify(topology));
|
|
1072
1077
|
await this.startTrackingEngine(topology);
|
|
1073
1078
|
response.send(JSON.stringify({ success: true }), {
|
|
1074
1079
|
headers: { 'Content-Type': 'application/json' },
|
|
1075
1080
|
});
|
|
1076
1081
|
} catch (e) {
|
|
1082
|
+
this.console.error(`[Topology API] PUT error:`, e);
|
|
1077
1083
|
response.send(JSON.stringify({ error: 'Invalid topology JSON' }), {
|
|
1078
1084
|
code: 400,
|
|
1079
1085
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1904,71 +1910,46 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
1904
1910
|
const dirRad = (direction - 90) * Math.PI / 180;
|
|
1905
1911
|
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
1906
1912
|
|
|
1907
|
-
//
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1913
|
+
// Get floor plan scale (pixels per foot) - default 5
|
|
1914
|
+
const floorPlanScale = topology.floorPlanScale || 5;
|
|
1915
|
+
|
|
1916
|
+
// Use the ACTUAL distance from LLM analysis (distanceFeet) - this is the key fix!
|
|
1917
|
+
// The LLM estimates distance based on object size, perspective, and camera context
|
|
1918
|
+
const landmarkData = suggestion.landmark as any;
|
|
1919
|
+
const distanceFeet = landmarkData.distanceFeet || 50; // Default 50ft if not set
|
|
1920
|
+
const distanceInPixels = distanceFeet * floorPlanScale;
|
|
1912
1921
|
|
|
1913
|
-
// Use bounding box
|
|
1914
|
-
|
|
1915
|
-
const bbox = (suggestion.landmark as any).boundingBox as [number, number, number, number] | undefined;
|
|
1922
|
+
// Use bounding box for horizontal positioning within the FOV
|
|
1923
|
+
const bbox = landmarkData.boundingBox as [number, number, number, number] | undefined;
|
|
1916
1924
|
|
|
1917
1925
|
let angleOffset: number;
|
|
1918
|
-
let distanceMultiplier: number;
|
|
1919
1926
|
|
|
1920
1927
|
if (bbox && bbox.length >= 2) {
|
|
1921
1928
|
// Use bounding box X for horizontal position in FOV
|
|
1922
1929
|
const bboxCenterX = bbox[0] + (bbox[2] || 0) / 2; // 0 = left edge, 1 = right edge
|
|
1923
|
-
|
|
1924
1930
|
// Map X position to angle within FOV
|
|
1925
1931
|
angleOffset = (bboxCenterX - 0.5) * 2 * halfFov;
|
|
1926
|
-
|
|
1927
|
-
// For distance: use TYPE first, then bbox Y as hint
|
|
1928
|
-
if (isFarType) {
|
|
1929
|
-
// Neighbors, boundaries, streets are BEYOND the camera's normal range
|
|
1930
|
-
// Place at 150-200% of range to indicate they're distant background features
|
|
1931
|
-
distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
|
|
1932
|
-
this.console.log(`[Discovery] Far-type landmark "${landmarkType}" placed BEYOND FOV (${(distanceMultiplier * 100).toFixed(0)}% of range)`);
|
|
1933
|
-
} else {
|
|
1934
|
-
// For other types, use bbox Y as a hint (but cap minimum distance)
|
|
1935
|
-
const bboxCenterY = bbox[1] + (bbox[3] || 0) / 2;
|
|
1936
|
-
// Map Y to distance: 0 (top) = far, 1 (bottom) = closer (but not too close)
|
|
1937
|
-
distanceMultiplier = Math.max(0.4, 0.9 - (bboxCenterY * 0.5));
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal position`);
|
|
1932
|
+
this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal angle offset`);
|
|
1941
1933
|
} else {
|
|
1942
|
-
// No bounding box -
|
|
1934
|
+
// No bounding box - spread horizontally based on existing landmarks
|
|
1943
1935
|
const cameraDeviceId = camera.deviceId;
|
|
1944
1936
|
const existingFromCamera = (topology.landmarks || []).filter(l =>
|
|
1945
1937
|
l.visibleFromCameras?.includes(cameraDeviceId) ||
|
|
1946
1938
|
l.visibleFromCameras?.includes(camera.name)
|
|
1947
1939
|
).length;
|
|
1948
|
-
|
|
1949
1940
|
// Spread horizontally across FOV
|
|
1950
1941
|
angleOffset = (existingFromCamera % 3 - 1) * halfFov * 0.6;
|
|
1951
|
-
|
|
1952
|
-
// Distance based on type
|
|
1953
|
-
if (isFarType) {
|
|
1954
|
-
// Neighbors, boundaries, streets are BEYOND the camera's normal range
|
|
1955
|
-
distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
|
|
1956
|
-
this.console.log(`[Discovery] Far-type landmark "${landmarkType}" (no bbox) placed BEYOND FOV`);
|
|
1957
|
-
} else {
|
|
1958
|
-
distanceMultiplier = 0.5 + (existingFromCamera % 2) * 0.3;
|
|
1959
|
-
this.console.log(`[Discovery] No bounding box, using fallback spread (existing: ${existingFromCamera})`);
|
|
1960
|
-
}
|
|
1942
|
+
this.console.log(`[Discovery] No bounding box, spreading horizontally (existing: ${existingFromCamera})`);
|
|
1961
1943
|
}
|
|
1962
1944
|
|
|
1963
1945
|
const finalAngle = dirRad + angleOffset;
|
|
1964
|
-
const distance = range * distanceMultiplier;
|
|
1965
1946
|
|
|
1966
1947
|
position = {
|
|
1967
|
-
x: camera.floorPlanPosition.x + Math.cos(finalAngle) *
|
|
1968
|
-
y: camera.floorPlanPosition.y + Math.sin(finalAngle) *
|
|
1948
|
+
x: camera.floorPlanPosition.x + Math.cos(finalAngle) * distanceInPixels,
|
|
1949
|
+
y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distanceInPixels,
|
|
1969
1950
|
};
|
|
1970
1951
|
|
|
1971
|
-
this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}"
|
|
1952
|
+
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)}°`);
|
|
1972
1953
|
} else {
|
|
1973
1954
|
// Position in a grid pattern starting from center
|
|
1974
1955
|
const landmarkCount = topology.landmarks?.length || 0;
|
|
@@ -2029,12 +2010,14 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
2029
2010
|
const timestamp = Date.now();
|
|
2030
2011
|
|
|
2031
2012
|
if (camera?.floorPlanPosition) {
|
|
2032
|
-
// Get camera's FOV direction
|
|
2013
|
+
// Get camera's FOV direction (cast to any for flexible access)
|
|
2033
2014
|
const fov = (camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 }) as any;
|
|
2034
2015
|
const direction = fov.direction || 0;
|
|
2035
|
-
const range = fov.range || 80;
|
|
2036
2016
|
const fovAngle = fov.angle || 90;
|
|
2037
2017
|
|
|
2018
|
+
// Get floor plan scale (pixels per foot)
|
|
2019
|
+
const floorPlanScale = topology.floorPlanScale || 5;
|
|
2020
|
+
|
|
2038
2021
|
// Convert direction to radians (0 = up/north, 90 = right/east)
|
|
2039
2022
|
const dirRad = (direction - 90) * Math.PI / 180;
|
|
2040
2023
|
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
@@ -2042,47 +2025,52 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
2042
2025
|
const camX = camera.floorPlanPosition.x;
|
|
2043
2026
|
const camY = camera.floorPlanPosition.y;
|
|
2044
2027
|
|
|
2045
|
-
// Use
|
|
2028
|
+
// Use distanceFeet from the zone metadata for accurate positioning
|
|
2029
|
+
const zoneData = zone as any;
|
|
2030
|
+
const distanceFeet = zoneData.distanceFeet || 40; // Default 40ft if not set
|
|
2031
|
+
const distanceInPixels = distanceFeet * floorPlanScale;
|
|
2032
|
+
|
|
2033
|
+
// Zone size based on coverage (larger coverage = wider zone)
|
|
2034
|
+
const zoneWidthFeet = Math.sqrt(zone.coverage) * 30; // e.g., 50% coverage = ~21ft wide
|
|
2035
|
+
const zoneWidthPixels = zoneWidthFeet * floorPlanScale;
|
|
2036
|
+
const zoneDepthPixels = (zone.coverage * 20) * floorPlanScale; // Depth based on coverage
|
|
2037
|
+
|
|
2038
|
+
// Use bounding box for horizontal positioning if available
|
|
2046
2039
|
const bbox = zone.boundingBox; // [x, y, width, height] normalized 0-1
|
|
2047
2040
|
|
|
2048
|
-
let innerRadius: number;
|
|
2049
|
-
let outerRadius: number;
|
|
2050
2041
|
let angleStart: number;
|
|
2051
2042
|
let angleEnd: number;
|
|
2043
|
+
let innerRadius: number;
|
|
2044
|
+
let outerRadius: number;
|
|
2052
2045
|
|
|
2053
2046
|
if (bbox && bbox.length >= 4) {
|
|
2054
|
-
// Map bounding box to
|
|
2047
|
+
// Map bounding box X to angle within FOV
|
|
2055
2048
|
const bboxLeft = bbox[0];
|
|
2056
2049
|
const bboxRight = bbox[0] + bbox[2];
|
|
2057
|
-
const bboxTop = bbox[1];
|
|
2058
|
-
const bboxBottom = bbox[1] + bbox[3];
|
|
2059
|
-
|
|
2060
|
-
// Map X to angle within FOV (0 = left edge, 1 = right edge)
|
|
2061
2050
|
angleStart = dirRad + (bboxLeft - 0.5) * 2 * halfFov;
|
|
2062
2051
|
angleEnd = dirRad + (bboxRight - 0.5) * 2 * halfFov;
|
|
2063
2052
|
|
|
2064
|
-
//
|
|
2065
|
-
innerRadius =
|
|
2066
|
-
outerRadius =
|
|
2053
|
+
// Use distanceFeet for depth, with a spread based on coverage
|
|
2054
|
+
innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
|
|
2055
|
+
outerRadius = distanceInPixels + zoneDepthPixels / 2;
|
|
2067
2056
|
|
|
2068
|
-
|
|
2069
|
-
if (outerRadius - innerRadius < 20) {
|
|
2070
|
-
outerRadius = innerRadius + 20;
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
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)}°`);
|
|
2057
|
+
this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using bbox [${bbox.join(',')}]`);
|
|
2074
2058
|
} else {
|
|
2075
|
-
// Fallback: wedge-shaped zone
|
|
2059
|
+
// Fallback: wedge-shaped zone covering portion of FOV
|
|
2076
2060
|
const existingFromCamera = (topology.drawnZones || []).filter((z: any) =>
|
|
2077
2061
|
z.linkedCameras?.includes(sourceCameras[0])
|
|
2078
2062
|
).length;
|
|
2079
2063
|
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
angleStart = dirRad - halfFov * 0.
|
|
2083
|
-
angleEnd = dirRad + halfFov * 0.
|
|
2064
|
+
// Spread horizontally based on existing zones
|
|
2065
|
+
const offset = (existingFromCamera % 3 - 1) * halfFov * 0.5;
|
|
2066
|
+
angleStart = dirRad + offset - halfFov * 0.3;
|
|
2067
|
+
angleEnd = dirRad + offset + halfFov * 0.3;
|
|
2068
|
+
|
|
2069
|
+
// Use distanceFeet for depth
|
|
2070
|
+
innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
|
|
2071
|
+
outerRadius = distanceInPixels + zoneDepthPixels / 2;
|
|
2084
2072
|
|
|
2085
|
-
this.console.log(`[Discovery] Zone "${zone.name}" using fallback spread
|
|
2073
|
+
this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using fallback spread`);
|
|
2086
2074
|
}
|
|
2087
2075
|
|
|
2088
2076
|
// Create arc polygon
|
|
@@ -2104,7 +2092,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
2104
2092
|
});
|
|
2105
2093
|
}
|
|
2106
2094
|
|
|
2107
|
-
this.console.log(`[Discovery] Creating zone "${zone.name}"
|
|
2095
|
+
this.console.log(`[Discovery] Creating zone "${zone.name}" at ${distanceFeet}ft (${distanceInPixels}px) from ${camera.name}`);
|
|
2108
2096
|
} else {
|
|
2109
2097
|
// Fallback: rectangular zone at default location
|
|
2110
2098
|
const centerX = 300 + (topology.drawnZones?.length || 0) * 120;
|
package/src/models/topology.ts
CHANGED
|
@@ -238,7 +238,7 @@ export interface DrawnZone {
|
|
|
238
238
|
/** Zone type */
|
|
239
239
|
type: DrawnZoneType;
|
|
240
240
|
/** Polygon points on floor plan (x, y coordinates) */
|
|
241
|
-
polygon:
|
|
241
|
+
polygon: FloorPlanPosition[];
|
|
242
242
|
/** Custom color override (optional) */
|
|
243
243
|
color?: string;
|
|
244
244
|
/** Linked camera IDs that can see this zone */
|
|
@@ -354,6 +354,8 @@ export interface CameraTopology {
|
|
|
354
354
|
floorPlan?: FloorPlanConfig;
|
|
355
355
|
/** Pending AI landmark suggestions */
|
|
356
356
|
pendingSuggestions?: LandmarkSuggestion[];
|
|
357
|
+
/** Floor plan scale in pixels per foot (for distance calculations) */
|
|
358
|
+
floorPlanScale?: number;
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
// ==================== Helper Functions ====================
|
package/src/ui/editor-html.ts
CHANGED
|
@@ -454,6 +454,7 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
454
454
|
const response = await fetch('../api/topology');
|
|
455
455
|
if (response.ok) {
|
|
456
456
|
topology = await response.json();
|
|
457
|
+
console.log('[Load] Loaded topology with', (topology.drawnZones || []).length, 'zones');
|
|
457
458
|
if (!topology.drawings) topology.drawings = [];
|
|
458
459
|
// Load floor plan scale if saved
|
|
459
460
|
if (topology.floorPlanScale) {
|
|
@@ -971,14 +972,24 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
971
972
|
async function saveTopology() {
|
|
972
973
|
try {
|
|
973
974
|
setStatus('Saving...', 'warning');
|
|
975
|
+
console.log('[Save] Saving topology with', (topology.drawnZones || []).length, 'zones');
|
|
974
976
|
const response = await fetch('../api/topology', {
|
|
975
977
|
method: 'PUT',
|
|
976
978
|
headers: { 'Content-Type': 'application/json' },
|
|
977
979
|
body: JSON.stringify(topology)
|
|
978
980
|
});
|
|
979
|
-
if (response.ok) {
|
|
980
|
-
|
|
981
|
-
|
|
981
|
+
if (response.ok) {
|
|
982
|
+
console.log('[Save] Save successful');
|
|
983
|
+
setStatus('Saved successfully', 'success');
|
|
984
|
+
} else {
|
|
985
|
+
const errorText = await response.text();
|
|
986
|
+
console.error('[Save] Save failed:', response.status, errorText);
|
|
987
|
+
setStatus('Failed to save: ' + errorText, 'error');
|
|
988
|
+
}
|
|
989
|
+
} catch (e) {
|
|
990
|
+
console.error('Failed to save topology:', e);
|
|
991
|
+
setStatus('Failed to save', 'error');
|
|
992
|
+
}
|
|
982
993
|
}
|
|
983
994
|
|
|
984
995
|
function resizeCanvas() {
|
|
@@ -1598,9 +1609,11 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
1598
1609
|
// Zone list
|
|
1599
1610
|
const zoneList = document.getElementById('zone-list');
|
|
1600
1611
|
const zones = topology.drawnZones || [];
|
|
1612
|
+
console.log('[updateUI] Zone list update - topology.drawnZones:', zones.length, 'zones');
|
|
1601
1613
|
if (zones.length === 0) {
|
|
1602
1614
|
zoneList.innerHTML = '<div class="zone-item" style="color: #666; text-align: center; cursor: default; padding: 8px;">No zones drawn</div>';
|
|
1603
1615
|
} else {
|
|
1616
|
+
console.log('[updateUI] Rendering', zones.length, 'zones:', zones.map(z => z.name).join(', '));
|
|
1604
1617
|
zoneList.innerHTML = zones.map(z => {
|
|
1605
1618
|
const color = ZONE_STROKE_COLORS[z.type] || ZONE_STROKE_COLORS.custom;
|
|
1606
1619
|
return '<div class="camera-item ' + (selectedItem?.type === 'zone' && selectedItem?.id === z.id ? 'selected' : '') + '" onclick="selectZone(\\'' + z.id + '\\')" style="border-left: 3px solid ' + color + ';"><div class="camera-name">' + z.name + '</div><div class="camera-info">' + z.type + ' | ' + z.polygon.length + ' points</div></div>';
|
|
@@ -1778,9 +1791,13 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
1778
1791
|
polygon: currentZonePoints.map(pt => ({ x: pt.x, y: pt.y })),
|
|
1779
1792
|
};
|
|
1780
1793
|
|
|
1794
|
+
console.log('[Zone] Creating zone:', zone.name, 'with', zone.polygon.length, 'points');
|
|
1795
|
+
|
|
1781
1796
|
if (!topology.drawnZones) topology.drawnZones = [];
|
|
1782
1797
|
topology.drawnZones.push(zone);
|
|
1783
1798
|
|
|
1799
|
+
console.log('[Zone] Total zones now:', topology.drawnZones.length);
|
|
1800
|
+
|
|
1784
1801
|
// Reset state
|
|
1785
1802
|
zoneDrawingMode = false;
|
|
1786
1803
|
currentZonePoints = [];
|
|
@@ -1789,7 +1806,19 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
1789
1806
|
setTool('select');
|
|
1790
1807
|
updateUI();
|
|
1791
1808
|
render();
|
|
1792
|
-
|
|
1809
|
+
|
|
1810
|
+
console.log('[Zone] After updateUI/render, zones in topology:', (topology.drawnZones || []).length);
|
|
1811
|
+
|
|
1812
|
+
setStatus('Zone "' + zone.name + '" created - saving...', 'success');
|
|
1813
|
+
|
|
1814
|
+
// Auto-save after creating a zone
|
|
1815
|
+
saveTopology().then(() => {
|
|
1816
|
+
console.log('[Zone] Save completed, zones in topology:', (topology.drawnZones || []).length);
|
|
1817
|
+
setStatus('Zone "' + zone.name + '" saved', 'success');
|
|
1818
|
+
}).catch(err => {
|
|
1819
|
+
console.error('Failed to auto-save zone:', err);
|
|
1820
|
+
setStatus('Zone created but auto-save failed - click Save', 'warning');
|
|
1821
|
+
});
|
|
1793
1822
|
}
|
|
1794
1823
|
|
|
1795
1824
|
function selectZone(id) {
|
|
@@ -1814,9 +1843,9 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
1814
1843
|
'<button class="btn btn-primary" onclick="deleteZone(\\'' + id + '\\')" style="background: #dc2626; width: 100%;">Delete Zone</button>';
|
|
1815
1844
|
}
|
|
1816
1845
|
|
|
1817
|
-
function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); } }
|
|
1818
|
-
function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); } }
|
|
1819
|
-
function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; } }
|
|
1846
|
+
function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); saveTopology(); } }
|
|
1847
|
+
function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); saveTopology(); } }
|
|
1848
|
+
function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; saveTopology(); } }
|
|
1820
1849
|
function deleteZone(id) {
|
|
1821
1850
|
if (!confirm('Delete this zone?')) return;
|
|
1822
1851
|
topology.drawnZones = (topology.drawnZones || []).filter(z => z.id !== id);
|
|
@@ -1824,7 +1853,8 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
1824
1853
|
document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select an item to edit.</p>';
|
|
1825
1854
|
updateUI();
|
|
1826
1855
|
render();
|
|
1827
|
-
setStatus('Zone deleted', 'success');
|
|
1856
|
+
setStatus('Zone deleted - saving...', 'success');
|
|
1857
|
+
saveTopology().then(() => setStatus('Zone deleted', 'success'));
|
|
1828
1858
|
}
|
|
1829
1859
|
|
|
1830
1860
|
function useBlankCanvas() {
|