@blueharford/scrypted-spatial-awareness 0.6.17 → 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 +117 -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 +49 -67
- package/src/models/topology.ts +3 -1
- package/src/ui/editor-html.ts +36 -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
|
@@ -1904,71 +1904,46 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
1904
1904
|
const dirRad = (direction - 90) * Math.PI / 180;
|
|
1905
1905
|
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
1906
1906
|
|
|
1907
|
-
//
|
|
1908
|
-
|
|
1909
|
-
const landmarkType = suggestion.landmark.type;
|
|
1910
|
-
const farTypes = ['neighbor', 'boundary', 'street']; // Always place at edge of FOV
|
|
1911
|
-
const isFarType = farTypes.includes(landmarkType || '');
|
|
1907
|
+
// Get floor plan scale (pixels per foot) - default 5
|
|
1908
|
+
const floorPlanScale = topology.floorPlanScale || 5;
|
|
1912
1909
|
|
|
1913
|
-
// Use
|
|
1914
|
-
//
|
|
1915
|
-
const
|
|
1910
|
+
// Use the ACTUAL distance from LLM analysis (distanceFeet) - this is the key fix!
|
|
1911
|
+
// The LLM estimates distance based on object size, perspective, and camera context
|
|
1912
|
+
const landmarkData = suggestion.landmark as any;
|
|
1913
|
+
const distanceFeet = landmarkData.distanceFeet || 50; // Default 50ft if not set
|
|
1914
|
+
const distanceInPixels = distanceFeet * floorPlanScale;
|
|
1915
|
+
|
|
1916
|
+
// Use bounding box for horizontal positioning within the FOV
|
|
1917
|
+
const bbox = landmarkData.boundingBox as [number, number, number, number] | undefined;
|
|
1916
1918
|
|
|
1917
1919
|
let angleOffset: number;
|
|
1918
|
-
let distanceMultiplier: number;
|
|
1919
1920
|
|
|
1920
1921
|
if (bbox && bbox.length >= 2) {
|
|
1921
1922
|
// Use bounding box X for horizontal position in FOV
|
|
1922
1923
|
const bboxCenterX = bbox[0] + (bbox[2] || 0) / 2; // 0 = left edge, 1 = right edge
|
|
1923
|
-
|
|
1924
1924
|
// Map X position to angle within FOV
|
|
1925
1925
|
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`);
|
|
1926
|
+
this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal angle offset`);
|
|
1941
1927
|
} else {
|
|
1942
|
-
// No bounding box -
|
|
1928
|
+
// No bounding box - spread horizontally based on existing landmarks
|
|
1943
1929
|
const cameraDeviceId = camera.deviceId;
|
|
1944
1930
|
const existingFromCamera = (topology.landmarks || []).filter(l =>
|
|
1945
1931
|
l.visibleFromCameras?.includes(cameraDeviceId) ||
|
|
1946
1932
|
l.visibleFromCameras?.includes(camera.name)
|
|
1947
1933
|
).length;
|
|
1948
|
-
|
|
1949
1934
|
// Spread horizontally across FOV
|
|
1950
1935
|
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
|
-
}
|
|
1936
|
+
this.console.log(`[Discovery] No bounding box, spreading horizontally (existing: ${existingFromCamera})`);
|
|
1961
1937
|
}
|
|
1962
1938
|
|
|
1963
1939
|
const finalAngle = dirRad + angleOffset;
|
|
1964
|
-
const distance = range * distanceMultiplier;
|
|
1965
1940
|
|
|
1966
1941
|
position = {
|
|
1967
|
-
x: camera.floorPlanPosition.x + Math.cos(finalAngle) *
|
|
1968
|
-
y: camera.floorPlanPosition.y + Math.sin(finalAngle) *
|
|
1942
|
+
x: camera.floorPlanPosition.x + Math.cos(finalAngle) * distanceInPixels,
|
|
1943
|
+
y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distanceInPixels,
|
|
1969
1944
|
};
|
|
1970
1945
|
|
|
1971
|
-
this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}"
|
|
1946
|
+
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
1947
|
} else {
|
|
1973
1948
|
// Position in a grid pattern starting from center
|
|
1974
1949
|
const landmarkCount = topology.landmarks?.length || 0;
|
|
@@ -2029,12 +2004,14 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
2029
2004
|
const timestamp = Date.now();
|
|
2030
2005
|
|
|
2031
2006
|
if (camera?.floorPlanPosition) {
|
|
2032
|
-
// Get camera's FOV direction
|
|
2007
|
+
// Get camera's FOV direction (cast to any for flexible access)
|
|
2033
2008
|
const fov = (camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 }) as any;
|
|
2034
2009
|
const direction = fov.direction || 0;
|
|
2035
|
-
const range = fov.range || 80;
|
|
2036
2010
|
const fovAngle = fov.angle || 90;
|
|
2037
2011
|
|
|
2012
|
+
// Get floor plan scale (pixels per foot)
|
|
2013
|
+
const floorPlanScale = topology.floorPlanScale || 5;
|
|
2014
|
+
|
|
2038
2015
|
// Convert direction to radians (0 = up/north, 90 = right/east)
|
|
2039
2016
|
const dirRad = (direction - 90) * Math.PI / 180;
|
|
2040
2017
|
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
@@ -2042,47 +2019,52 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
2042
2019
|
const camX = camera.floorPlanPosition.x;
|
|
2043
2020
|
const camY = camera.floorPlanPosition.y;
|
|
2044
2021
|
|
|
2045
|
-
// Use
|
|
2022
|
+
// Use distanceFeet from the zone metadata for accurate positioning
|
|
2023
|
+
const zoneData = zone as any;
|
|
2024
|
+
const distanceFeet = zoneData.distanceFeet || 40; // Default 40ft if not set
|
|
2025
|
+
const distanceInPixels = distanceFeet * floorPlanScale;
|
|
2026
|
+
|
|
2027
|
+
// Zone size based on coverage (larger coverage = wider zone)
|
|
2028
|
+
const zoneWidthFeet = Math.sqrt(zone.coverage) * 30; // e.g., 50% coverage = ~21ft wide
|
|
2029
|
+
const zoneWidthPixels = zoneWidthFeet * floorPlanScale;
|
|
2030
|
+
const zoneDepthPixels = (zone.coverage * 20) * floorPlanScale; // Depth based on coverage
|
|
2031
|
+
|
|
2032
|
+
// Use bounding box for horizontal positioning if available
|
|
2046
2033
|
const bbox = zone.boundingBox; // [x, y, width, height] normalized 0-1
|
|
2047
2034
|
|
|
2048
|
-
let innerRadius: number;
|
|
2049
|
-
let outerRadius: number;
|
|
2050
2035
|
let angleStart: number;
|
|
2051
2036
|
let angleEnd: number;
|
|
2037
|
+
let innerRadius: number;
|
|
2038
|
+
let outerRadius: number;
|
|
2052
2039
|
|
|
2053
2040
|
if (bbox && bbox.length >= 4) {
|
|
2054
|
-
// Map bounding box to
|
|
2041
|
+
// Map bounding box X to angle within FOV
|
|
2055
2042
|
const bboxLeft = bbox[0];
|
|
2056
2043
|
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
2044
|
angleStart = dirRad + (bboxLeft - 0.5) * 2 * halfFov;
|
|
2062
2045
|
angleEnd = dirRad + (bboxRight - 0.5) * 2 * halfFov;
|
|
2063
2046
|
|
|
2064
|
-
//
|
|
2065
|
-
innerRadius =
|
|
2066
|
-
outerRadius =
|
|
2067
|
-
|
|
2068
|
-
// Ensure min size
|
|
2069
|
-
if (outerRadius - innerRadius < 20) {
|
|
2070
|
-
outerRadius = innerRadius + 20;
|
|
2071
|
-
}
|
|
2047
|
+
// Use distanceFeet for depth, with a spread based on coverage
|
|
2048
|
+
innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
|
|
2049
|
+
outerRadius = distanceInPixels + zoneDepthPixels / 2;
|
|
2072
2050
|
|
|
2073
|
-
this.console.log(`[Discovery] Zone "${zone.name}" using bbox [${bbox.join(',')}]
|
|
2051
|
+
this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using bbox [${bbox.join(',')}]`);
|
|
2074
2052
|
} else {
|
|
2075
|
-
// Fallback: wedge-shaped zone
|
|
2053
|
+
// Fallback: wedge-shaped zone covering portion of FOV
|
|
2076
2054
|
const existingFromCamera = (topology.drawnZones || []).filter((z: any) =>
|
|
2077
2055
|
z.linkedCameras?.includes(sourceCameras[0])
|
|
2078
2056
|
).length;
|
|
2079
2057
|
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
angleStart = dirRad - halfFov * 0.
|
|
2083
|
-
angleEnd = dirRad + halfFov * 0.
|
|
2058
|
+
// Spread horizontally based on existing zones
|
|
2059
|
+
const offset = (existingFromCamera % 3 - 1) * halfFov * 0.5;
|
|
2060
|
+
angleStart = dirRad + offset - halfFov * 0.3;
|
|
2061
|
+
angleEnd = dirRad + offset + halfFov * 0.3;
|
|
2062
|
+
|
|
2063
|
+
// Use distanceFeet for depth
|
|
2064
|
+
innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
|
|
2065
|
+
outerRadius = distanceInPixels + zoneDepthPixels / 2;
|
|
2084
2066
|
|
|
2085
|
-
this.console.log(`[Discovery] Zone "${zone.name}" using fallback spread
|
|
2067
|
+
this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using fallback spread`);
|
|
2086
2068
|
}
|
|
2087
2069
|
|
|
2088
2070
|
// Create arc polygon
|
|
@@ -2104,7 +2086,7 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
2104
2086
|
});
|
|
2105
2087
|
}
|
|
2106
2088
|
|
|
2107
|
-
this.console.log(`[Discovery] Creating zone "${zone.name}"
|
|
2089
|
+
this.console.log(`[Discovery] Creating zone "${zone.name}" at ${distanceFeet}ft (${distanceInPixels}px) from ${camera.name}`);
|
|
2108
2090
|
} else {
|
|
2109
2091
|
// Fallback: rectangular zone at default location
|
|
2110
2092
|
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() {
|
|
@@ -1778,9 +1789,13 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
1778
1789
|
polygon: currentZonePoints.map(pt => ({ x: pt.x, y: pt.y })),
|
|
1779
1790
|
};
|
|
1780
1791
|
|
|
1792
|
+
console.log('[Zone] Creating zone:', zone.name, 'with', zone.polygon.length, 'points');
|
|
1793
|
+
|
|
1781
1794
|
if (!topology.drawnZones) topology.drawnZones = [];
|
|
1782
1795
|
topology.drawnZones.push(zone);
|
|
1783
1796
|
|
|
1797
|
+
console.log('[Zone] Total zones now:', topology.drawnZones.length);
|
|
1798
|
+
|
|
1784
1799
|
// Reset state
|
|
1785
1800
|
zoneDrawingMode = false;
|
|
1786
1801
|
currentZonePoints = [];
|
|
@@ -1789,7 +1804,19 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
1789
1804
|
setTool('select');
|
|
1790
1805
|
updateUI();
|
|
1791
1806
|
render();
|
|
1792
|
-
|
|
1807
|
+
|
|
1808
|
+
console.log('[Zone] After updateUI/render, zones in topology:', (topology.drawnZones || []).length);
|
|
1809
|
+
|
|
1810
|
+
setStatus('Zone "' + zone.name + '" created - saving...', 'success');
|
|
1811
|
+
|
|
1812
|
+
// Auto-save after creating a zone
|
|
1813
|
+
saveTopology().then(() => {
|
|
1814
|
+
console.log('[Zone] Save completed, zones in topology:', (topology.drawnZones || []).length);
|
|
1815
|
+
setStatus('Zone "' + zone.name + '" saved', 'success');
|
|
1816
|
+
}).catch(err => {
|
|
1817
|
+
console.error('Failed to auto-save zone:', err);
|
|
1818
|
+
setStatus('Zone created but auto-save failed - click Save', 'warning');
|
|
1819
|
+
});
|
|
1793
1820
|
}
|
|
1794
1821
|
|
|
1795
1822
|
function selectZone(id) {
|
|
@@ -1814,9 +1841,9 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
1814
1841
|
'<button class="btn btn-primary" onclick="deleteZone(\\'' + id + '\\')" style="background: #dc2626; width: 100%;">Delete Zone</button>';
|
|
1815
1842
|
}
|
|
1816
1843
|
|
|
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; } }
|
|
1844
|
+
function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); saveTopology(); } }
|
|
1845
|
+
function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); saveTopology(); } }
|
|
1846
|
+
function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; saveTopology(); } }
|
|
1820
1847
|
function deleteZone(id) {
|
|
1821
1848
|
if (!confirm('Delete this zone?')) return;
|
|
1822
1849
|
topology.drawnZones = (topology.drawnZones || []).filter(z => z.id !== id);
|
|
@@ -1824,7 +1851,8 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
1824
1851
|
document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select an item to edit.</p>';
|
|
1825
1852
|
updateUI();
|
|
1826
1853
|
render();
|
|
1827
|
-
setStatus('Zone deleted', 'success');
|
|
1854
|
+
setStatus('Zone deleted - saving...', 'success');
|
|
1855
|
+
saveTopology().then(() => setStatus('Zone deleted', 'success'));
|
|
1828
1856
|
}
|
|
1829
1857
|
|
|
1830
1858
|
function useBlankCanvas() {
|