@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/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",
|
|
@@ -36536,6 +36540,7 @@ Use the mount height to help estimate distances - objects at ground level will a
|
|
|
36536
36540
|
coverage: typeof z.coverage === 'number' ? z.coverage : 0.5,
|
|
36537
36541
|
description: z.description || '',
|
|
36538
36542
|
boundingBox: z.boundingBox,
|
|
36543
|
+
distance: this.mapDistance(z.distance), // Parse distance for zones too
|
|
36539
36544
|
}));
|
|
36540
36545
|
}
|
|
36541
36546
|
if (parsed.edges && typeof parsed.edges === 'object') {
|
|
@@ -36657,6 +36662,26 @@ Use the mount height to help estimate distances - objects at ground level will a
|
|
|
36657
36662
|
return 'distant';
|
|
36658
36663
|
return 'medium'; // Default to medium if not specified
|
|
36659
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
|
+
}
|
|
36660
36685
|
/** Try to parse JSON with recovery for truncated responses */
|
|
36661
36686
|
parseJsonWithRecovery(jsonStr, context) {
|
|
36662
36687
|
// First, try direct parse
|
|
@@ -36950,6 +36975,9 @@ Use the mount height to help estimate distances - objects at ground level will a
|
|
|
36950
36975
|
// Generate zone suggestions (even for smaller coverage - 10% is enough)
|
|
36951
36976
|
for (const zone of analysis.zones) {
|
|
36952
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);
|
|
36953
36981
|
const suggestion = {
|
|
36954
36982
|
id: `zone_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
36955
36983
|
type: 'zone',
|
|
@@ -36957,10 +36985,14 @@ Use the mount height to help estimate distances - objects at ground level will a
|
|
|
36957
36985
|
sourceCameras: [analysis.cameraId],
|
|
36958
36986
|
confidence: Math.min(0.9, 0.5 + zone.coverage), // Higher coverage = higher confidence
|
|
36959
36987
|
status: 'pending',
|
|
36960
|
-
zone:
|
|
36988
|
+
zone: {
|
|
36989
|
+
...zone,
|
|
36990
|
+
// Include distance metadata for positioning
|
|
36991
|
+
distanceFeet: distanceFeet,
|
|
36992
|
+
},
|
|
36961
36993
|
};
|
|
36962
36994
|
this.suggestions.set(suggestion.id, suggestion);
|
|
36963
|
-
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)`);
|
|
36964
36996
|
}
|
|
36965
36997
|
}
|
|
36966
36998
|
}
|
|
@@ -39995,6 +40027,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
39995
40027
|
if (request.method === 'GET') {
|
|
39996
40028
|
const topologyJson = this.storage.getItem('topology');
|
|
39997
40029
|
const topology = topologyJson ? JSON.parse(topologyJson) : (0, topology_1.createEmptyTopology)();
|
|
40030
|
+
this.console.log(`[Topology API] GET - drawnZones: ${topology.drawnZones?.length || 0}`);
|
|
39998
40031
|
response.send(JSON.stringify(topology), {
|
|
39999
40032
|
headers: { 'Content-Type': 'application/json' },
|
|
40000
40033
|
});
|
|
@@ -40002,6 +40035,10 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40002
40035
|
else if (request.method === 'PUT' || request.method === 'POST') {
|
|
40003
40036
|
try {
|
|
40004
40037
|
const topology = JSON.parse(request.body);
|
|
40038
|
+
this.console.log(`[Topology API] PUT received - drawnZones: ${topology.drawnZones?.length || 0}`);
|
|
40039
|
+
if (topology.drawnZones?.length) {
|
|
40040
|
+
this.console.log(`[Topology API] Zone names: ${topology.drawnZones.map(z => z.name).join(', ')}`);
|
|
40041
|
+
}
|
|
40005
40042
|
this.storage.setItem('topology', JSON.stringify(topology));
|
|
40006
40043
|
await this.startTrackingEngine(topology);
|
|
40007
40044
|
response.send(JSON.stringify({ success: true }), {
|
|
@@ -40009,6 +40046,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40009
40046
|
});
|
|
40010
40047
|
}
|
|
40011
40048
|
catch (e) {
|
|
40049
|
+
this.console.error(`[Topology API] PUT error:`, e);
|
|
40012
40050
|
response.send(JSON.stringify({ error: 'Invalid topology JSON' }), {
|
|
40013
40051
|
code: 400,
|
|
40014
40052
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -40784,61 +40822,38 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40784
40822
|
// Convert direction to radians (0 = up/north, 90 = right/east)
|
|
40785
40823
|
const dirRad = (direction - 90) * Math.PI / 180;
|
|
40786
40824
|
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
40787
|
-
//
|
|
40788
|
-
|
|
40789
|
-
|
|
40790
|
-
|
|
40791
|
-
const
|
|
40792
|
-
|
|
40793
|
-
|
|
40794
|
-
|
|
40825
|
+
// Get floor plan scale (pixels per foot) - default 5
|
|
40826
|
+
const floorPlanScale = topology.floorPlanScale || 5;
|
|
40827
|
+
// Use the ACTUAL distance from LLM analysis (distanceFeet) - this is the key fix!
|
|
40828
|
+
// The LLM estimates distance based on object size, perspective, and camera context
|
|
40829
|
+
const landmarkData = suggestion.landmark;
|
|
40830
|
+
const distanceFeet = landmarkData.distanceFeet || 50; // Default 50ft if not set
|
|
40831
|
+
const distanceInPixels = distanceFeet * floorPlanScale;
|
|
40832
|
+
// Use bounding box for horizontal positioning within the FOV
|
|
40833
|
+
const bbox = landmarkData.boundingBox;
|
|
40795
40834
|
let angleOffset;
|
|
40796
|
-
let distanceMultiplier;
|
|
40797
40835
|
if (bbox && bbox.length >= 2) {
|
|
40798
40836
|
// Use bounding box X for horizontal position in FOV
|
|
40799
40837
|
const bboxCenterX = bbox[0] + (bbox[2] || 0) / 2; // 0 = left edge, 1 = right edge
|
|
40800
40838
|
// Map X position to angle within FOV
|
|
40801
40839
|
angleOffset = (bboxCenterX - 0.5) * 2 * halfFov;
|
|
40802
|
-
|
|
40803
|
-
if (isFarType) {
|
|
40804
|
-
// Neighbors, boundaries, streets are BEYOND the camera's normal range
|
|
40805
|
-
// Place at 150-200% of range to indicate they're distant background features
|
|
40806
|
-
distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
|
|
40807
|
-
this.console.log(`[Discovery] Far-type landmark "${landmarkType}" placed BEYOND FOV (${(distanceMultiplier * 100).toFixed(0)}% of range)`);
|
|
40808
|
-
}
|
|
40809
|
-
else {
|
|
40810
|
-
// For other types, use bbox Y as a hint (but cap minimum distance)
|
|
40811
|
-
const bboxCenterY = bbox[1] + (bbox[3] || 0) / 2;
|
|
40812
|
-
// Map Y to distance: 0 (top) = far, 1 (bottom) = closer (but not too close)
|
|
40813
|
-
distanceMultiplier = Math.max(0.4, 0.9 - (bboxCenterY * 0.5));
|
|
40814
|
-
}
|
|
40815
|
-
this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal position`);
|
|
40840
|
+
this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal angle offset`);
|
|
40816
40841
|
}
|
|
40817
40842
|
else {
|
|
40818
|
-
// No bounding box -
|
|
40843
|
+
// No bounding box - spread horizontally based on existing landmarks
|
|
40819
40844
|
const cameraDeviceId = camera.deviceId;
|
|
40820
40845
|
const existingFromCamera = (topology.landmarks || []).filter(l => l.visibleFromCameras?.includes(cameraDeviceId) ||
|
|
40821
40846
|
l.visibleFromCameras?.includes(camera.name)).length;
|
|
40822
40847
|
// Spread horizontally across FOV
|
|
40823
40848
|
angleOffset = (existingFromCamera % 3 - 1) * halfFov * 0.6;
|
|
40824
|
-
|
|
40825
|
-
if (isFarType) {
|
|
40826
|
-
// Neighbors, boundaries, streets are BEYOND the camera's normal range
|
|
40827
|
-
distanceMultiplier = 1.5 + Math.random() * 0.5; // 150-200% of range
|
|
40828
|
-
this.console.log(`[Discovery] Far-type landmark "${landmarkType}" (no bbox) placed BEYOND FOV`);
|
|
40829
|
-
}
|
|
40830
|
-
else {
|
|
40831
|
-
distanceMultiplier = 0.5 + (existingFromCamera % 2) * 0.3;
|
|
40832
|
-
this.console.log(`[Discovery] No bounding box, using fallback spread (existing: ${existingFromCamera})`);
|
|
40833
|
-
}
|
|
40849
|
+
this.console.log(`[Discovery] No bounding box, spreading horizontally (existing: ${existingFromCamera})`);
|
|
40834
40850
|
}
|
|
40835
40851
|
const finalAngle = dirRad + angleOffset;
|
|
40836
|
-
const distance = range * distanceMultiplier;
|
|
40837
40852
|
position = {
|
|
40838
|
-
x: camera.floorPlanPosition.x + Math.cos(finalAngle) *
|
|
40839
|
-
y: camera.floorPlanPosition.y + Math.sin(finalAngle) *
|
|
40853
|
+
x: camera.floorPlanPosition.x + Math.cos(finalAngle) * distanceInPixels,
|
|
40854
|
+
y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distanceInPixels,
|
|
40840
40855
|
};
|
|
40841
|
-
this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}"
|
|
40856
|
+
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)}°`);
|
|
40842
40857
|
}
|
|
40843
40858
|
else {
|
|
40844
40859
|
// Position in a grid pattern starting from center
|
|
@@ -40891,48 +40906,53 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40891
40906
|
let polygon = [];
|
|
40892
40907
|
const timestamp = Date.now();
|
|
40893
40908
|
if (camera?.floorPlanPosition) {
|
|
40894
|
-
// Get camera's FOV direction
|
|
40909
|
+
// Get camera's FOV direction (cast to any for flexible access)
|
|
40895
40910
|
const fov = (camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 });
|
|
40896
40911
|
const direction = fov.direction || 0;
|
|
40897
|
-
const range = fov.range || 80;
|
|
40898
40912
|
const fovAngle = fov.angle || 90;
|
|
40913
|
+
// Get floor plan scale (pixels per foot)
|
|
40914
|
+
const floorPlanScale = topology.floorPlanScale || 5;
|
|
40899
40915
|
// Convert direction to radians (0 = up/north, 90 = right/east)
|
|
40900
40916
|
const dirRad = (direction - 90) * Math.PI / 180;
|
|
40901
40917
|
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
40902
40918
|
const camX = camera.floorPlanPosition.x;
|
|
40903
40919
|
const camY = camera.floorPlanPosition.y;
|
|
40904
|
-
// Use
|
|
40920
|
+
// Use distanceFeet from the zone metadata for accurate positioning
|
|
40921
|
+
const zoneData = zone;
|
|
40922
|
+
const distanceFeet = zoneData.distanceFeet || 40; // Default 40ft if not set
|
|
40923
|
+
const distanceInPixels = distanceFeet * floorPlanScale;
|
|
40924
|
+
// Zone size based on coverage (larger coverage = wider zone)
|
|
40925
|
+
const zoneWidthFeet = Math.sqrt(zone.coverage) * 30; // e.g., 50% coverage = ~21ft wide
|
|
40926
|
+
const zoneWidthPixels = zoneWidthFeet * floorPlanScale;
|
|
40927
|
+
const zoneDepthPixels = (zone.coverage * 20) * floorPlanScale; // Depth based on coverage
|
|
40928
|
+
// Use bounding box for horizontal positioning if available
|
|
40905
40929
|
const bbox = zone.boundingBox; // [x, y, width, height] normalized 0-1
|
|
40906
|
-
let innerRadius;
|
|
40907
|
-
let outerRadius;
|
|
40908
40930
|
let angleStart;
|
|
40909
40931
|
let angleEnd;
|
|
40932
|
+
let innerRadius;
|
|
40933
|
+
let outerRadius;
|
|
40910
40934
|
if (bbox && bbox.length >= 4) {
|
|
40911
|
-
// Map bounding box to
|
|
40935
|
+
// Map bounding box X to angle within FOV
|
|
40912
40936
|
const bboxLeft = bbox[0];
|
|
40913
40937
|
const bboxRight = bbox[0] + bbox[2];
|
|
40914
|
-
const bboxTop = bbox[1];
|
|
40915
|
-
const bboxBottom = bbox[1] + bbox[3];
|
|
40916
|
-
// Map X to angle within FOV (0 = left edge, 1 = right edge)
|
|
40917
40938
|
angleStart = dirRad + (bboxLeft - 0.5) * 2 * halfFov;
|
|
40918
40939
|
angleEnd = dirRad + (bboxRight - 0.5) * 2 * halfFov;
|
|
40919
|
-
//
|
|
40920
|
-
innerRadius =
|
|
40921
|
-
outerRadius =
|
|
40922
|
-
|
|
40923
|
-
if (outerRadius - innerRadius < 20) {
|
|
40924
|
-
outerRadius = innerRadius + 20;
|
|
40925
|
-
}
|
|
40926
|
-
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)}°`);
|
|
40940
|
+
// Use distanceFeet for depth, with a spread based on coverage
|
|
40941
|
+
innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
|
|
40942
|
+
outerRadius = distanceInPixels + zoneDepthPixels / 2;
|
|
40943
|
+
this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using bbox [${bbox.join(',')}]`);
|
|
40927
40944
|
}
|
|
40928
40945
|
else {
|
|
40929
|
-
// Fallback: wedge-shaped zone
|
|
40946
|
+
// Fallback: wedge-shaped zone covering portion of FOV
|
|
40930
40947
|
const existingFromCamera = (topology.drawnZones || []).filter((z) => z.linkedCameras?.includes(sourceCameras[0])).length;
|
|
40931
|
-
|
|
40932
|
-
|
|
40933
|
-
angleStart = dirRad - halfFov * 0.
|
|
40934
|
-
angleEnd = dirRad + halfFov * 0.
|
|
40935
|
-
|
|
40948
|
+
// Spread horizontally based on existing zones
|
|
40949
|
+
const offset = (existingFromCamera % 3 - 1) * halfFov * 0.5;
|
|
40950
|
+
angleStart = dirRad + offset - halfFov * 0.3;
|
|
40951
|
+
angleEnd = dirRad + offset + halfFov * 0.3;
|
|
40952
|
+
// Use distanceFeet for depth
|
|
40953
|
+
innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
|
|
40954
|
+
outerRadius = distanceInPixels + zoneDepthPixels / 2;
|
|
40955
|
+
this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using fallback spread`);
|
|
40936
40956
|
}
|
|
40937
40957
|
// Create arc polygon
|
|
40938
40958
|
const steps = 8;
|
|
@@ -40952,7 +40972,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40952
40972
|
y: camY + Math.sin(angle) * outerRadius,
|
|
40953
40973
|
});
|
|
40954
40974
|
}
|
|
40955
|
-
this.console.log(`[Discovery] Creating zone "${zone.name}"
|
|
40975
|
+
this.console.log(`[Discovery] Creating zone "${zone.name}" at ${distanceFeet}ft (${distanceInPixels}px) from ${camera.name}`);
|
|
40956
40976
|
}
|
|
40957
40977
|
else {
|
|
40958
40978
|
// Fallback: rectangular zone at default location
|
|
@@ -42554,6 +42574,7 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42554
42574
|
const response = await fetch('../api/topology');
|
|
42555
42575
|
if (response.ok) {
|
|
42556
42576
|
topology = await response.json();
|
|
42577
|
+
console.log('[Load] Loaded topology with', (topology.drawnZones || []).length, 'zones');
|
|
42557
42578
|
if (!topology.drawings) topology.drawings = [];
|
|
42558
42579
|
// Load floor plan scale if saved
|
|
42559
42580
|
if (topology.floorPlanScale) {
|
|
@@ -43071,14 +43092,24 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43071
43092
|
async function saveTopology() {
|
|
43072
43093
|
try {
|
|
43073
43094
|
setStatus('Saving...', 'warning');
|
|
43095
|
+
console.log('[Save] Saving topology with', (topology.drawnZones || []).length, 'zones');
|
|
43074
43096
|
const response = await fetch('../api/topology', {
|
|
43075
43097
|
method: 'PUT',
|
|
43076
43098
|
headers: { 'Content-Type': 'application/json' },
|
|
43077
43099
|
body: JSON.stringify(topology)
|
|
43078
43100
|
});
|
|
43079
|
-
if (response.ok) {
|
|
43080
|
-
|
|
43081
|
-
|
|
43101
|
+
if (response.ok) {
|
|
43102
|
+
console.log('[Save] Save successful');
|
|
43103
|
+
setStatus('Saved successfully', 'success');
|
|
43104
|
+
} else {
|
|
43105
|
+
const errorText = await response.text();
|
|
43106
|
+
console.error('[Save] Save failed:', response.status, errorText);
|
|
43107
|
+
setStatus('Failed to save: ' + errorText, 'error');
|
|
43108
|
+
}
|
|
43109
|
+
} catch (e) {
|
|
43110
|
+
console.error('Failed to save topology:', e);
|
|
43111
|
+
setStatus('Failed to save', 'error');
|
|
43112
|
+
}
|
|
43082
43113
|
}
|
|
43083
43114
|
|
|
43084
43115
|
function resizeCanvas() {
|
|
@@ -43698,9 +43729,11 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43698
43729
|
// Zone list
|
|
43699
43730
|
const zoneList = document.getElementById('zone-list');
|
|
43700
43731
|
const zones = topology.drawnZones || [];
|
|
43732
|
+
console.log('[updateUI] Zone list update - topology.drawnZones:', zones.length, 'zones');
|
|
43701
43733
|
if (zones.length === 0) {
|
|
43702
43734
|
zoneList.innerHTML = '<div class="zone-item" style="color: #666; text-align: center; cursor: default; padding: 8px;">No zones drawn</div>';
|
|
43703
43735
|
} else {
|
|
43736
|
+
console.log('[updateUI] Rendering', zones.length, 'zones:', zones.map(z => z.name).join(', '));
|
|
43704
43737
|
zoneList.innerHTML = zones.map(z => {
|
|
43705
43738
|
const color = ZONE_STROKE_COLORS[z.type] || ZONE_STROKE_COLORS.custom;
|
|
43706
43739
|
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>';
|
|
@@ -43878,9 +43911,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43878
43911
|
polygon: currentZonePoints.map(pt => ({ x: pt.x, y: pt.y })),
|
|
43879
43912
|
};
|
|
43880
43913
|
|
|
43914
|
+
console.log('[Zone] Creating zone:', zone.name, 'with', zone.polygon.length, 'points');
|
|
43915
|
+
|
|
43881
43916
|
if (!topology.drawnZones) topology.drawnZones = [];
|
|
43882
43917
|
topology.drawnZones.push(zone);
|
|
43883
43918
|
|
|
43919
|
+
console.log('[Zone] Total zones now:', topology.drawnZones.length);
|
|
43920
|
+
|
|
43884
43921
|
// Reset state
|
|
43885
43922
|
zoneDrawingMode = false;
|
|
43886
43923
|
currentZonePoints = [];
|
|
@@ -43889,7 +43926,19 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43889
43926
|
setTool('select');
|
|
43890
43927
|
updateUI();
|
|
43891
43928
|
render();
|
|
43892
|
-
|
|
43929
|
+
|
|
43930
|
+
console.log('[Zone] After updateUI/render, zones in topology:', (topology.drawnZones || []).length);
|
|
43931
|
+
|
|
43932
|
+
setStatus('Zone "' + zone.name + '" created - saving...', 'success');
|
|
43933
|
+
|
|
43934
|
+
// Auto-save after creating a zone
|
|
43935
|
+
saveTopology().then(() => {
|
|
43936
|
+
console.log('[Zone] Save completed, zones in topology:', (topology.drawnZones || []).length);
|
|
43937
|
+
setStatus('Zone "' + zone.name + '" saved', 'success');
|
|
43938
|
+
}).catch(err => {
|
|
43939
|
+
console.error('Failed to auto-save zone:', err);
|
|
43940
|
+
setStatus('Zone created but auto-save failed - click Save', 'warning');
|
|
43941
|
+
});
|
|
43893
43942
|
}
|
|
43894
43943
|
|
|
43895
43944
|
function selectZone(id) {
|
|
@@ -43914,9 +43963,9 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43914
43963
|
'<button class="btn btn-primary" onclick="deleteZone(\\'' + id + '\\')" style="background: #dc2626; width: 100%;">Delete Zone</button>';
|
|
43915
43964
|
}
|
|
43916
43965
|
|
|
43917
|
-
function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); } }
|
|
43918
|
-
function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); } }
|
|
43919
|
-
function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; } }
|
|
43966
|
+
function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); saveTopology(); } }
|
|
43967
|
+
function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); saveTopology(); } }
|
|
43968
|
+
function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; saveTopology(); } }
|
|
43920
43969
|
function deleteZone(id) {
|
|
43921
43970
|
if (!confirm('Delete this zone?')) return;
|
|
43922
43971
|
topology.drawnZones = (topology.drawnZones || []).filter(z => z.id !== id);
|
|
@@ -43924,7 +43973,8 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43924
43973
|
document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select an item to edit.</p>';
|
|
43925
43974
|
updateUI();
|
|
43926
43975
|
render();
|
|
43927
|
-
setStatus('Zone deleted', 'success');
|
|
43976
|
+
setStatus('Zone deleted - saving...', 'success');
|
|
43977
|
+
saveTopology().then(() => setStatus('Zone deleted', 'success'));
|
|
43928
43978
|
}
|
|
43929
43979
|
|
|
43930
43980
|
function useBlankCanvas() {
|