@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/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
|
}
|
|
@@ -40784,61 +40816,38 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40784
40816
|
// Convert direction to radians (0 = up/north, 90 = right/east)
|
|
40785
40817
|
const dirRad = (direction - 90) * Math.PI / 180;
|
|
40786
40818
|
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
40787
|
-
//
|
|
40788
|
-
|
|
40789
|
-
|
|
40790
|
-
|
|
40791
|
-
const
|
|
40792
|
-
|
|
40793
|
-
|
|
40794
|
-
|
|
40819
|
+
// Get floor plan scale (pixels per foot) - default 5
|
|
40820
|
+
const floorPlanScale = topology.floorPlanScale || 5;
|
|
40821
|
+
// Use the ACTUAL distance from LLM analysis (distanceFeet) - this is the key fix!
|
|
40822
|
+
// The LLM estimates distance based on object size, perspective, and camera context
|
|
40823
|
+
const landmarkData = suggestion.landmark;
|
|
40824
|
+
const distanceFeet = landmarkData.distanceFeet || 50; // Default 50ft if not set
|
|
40825
|
+
const distanceInPixels = distanceFeet * floorPlanScale;
|
|
40826
|
+
// Use bounding box for horizontal positioning within the FOV
|
|
40827
|
+
const bbox = landmarkData.boundingBox;
|
|
40795
40828
|
let angleOffset;
|
|
40796
|
-
let distanceMultiplier;
|
|
40797
40829
|
if (bbox && bbox.length >= 2) {
|
|
40798
40830
|
// Use bounding box X for horizontal position in FOV
|
|
40799
40831
|
const bboxCenterX = bbox[0] + (bbox[2] || 0) / 2; // 0 = left edge, 1 = right edge
|
|
40800
40832
|
// Map X position to angle within FOV
|
|
40801
40833
|
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`);
|
|
40834
|
+
this.console.log(`[Discovery] Using bounding box [${bbox.join(',')}] for horizontal angle offset`);
|
|
40816
40835
|
}
|
|
40817
40836
|
else {
|
|
40818
|
-
// No bounding box -
|
|
40837
|
+
// No bounding box - spread horizontally based on existing landmarks
|
|
40819
40838
|
const cameraDeviceId = camera.deviceId;
|
|
40820
40839
|
const existingFromCamera = (topology.landmarks || []).filter(l => l.visibleFromCameras?.includes(cameraDeviceId) ||
|
|
40821
40840
|
l.visibleFromCameras?.includes(camera.name)).length;
|
|
40822
40841
|
// Spread horizontally across FOV
|
|
40823
40842
|
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
|
-
}
|
|
40843
|
+
this.console.log(`[Discovery] No bounding box, spreading horizontally (existing: ${existingFromCamera})`);
|
|
40834
40844
|
}
|
|
40835
40845
|
const finalAngle = dirRad + angleOffset;
|
|
40836
|
-
const distance = range * distanceMultiplier;
|
|
40837
40846
|
position = {
|
|
40838
|
-
x: camera.floorPlanPosition.x + Math.cos(finalAngle) *
|
|
40839
|
-
y: camera.floorPlanPosition.y + Math.sin(finalAngle) *
|
|
40847
|
+
x: camera.floorPlanPosition.x + Math.cos(finalAngle) * distanceInPixels,
|
|
40848
|
+
y: camera.floorPlanPosition.y + Math.sin(finalAngle) * distanceInPixels,
|
|
40840
40849
|
};
|
|
40841
|
-
this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}"
|
|
40850
|
+
this.console.log(`[Discovery] Placing landmark "${suggestion.landmark.name}" at ${distanceFeet}ft (${distanceInPixels}px) from ${camera.name}: dir=${direction}°, angle=${(angleOffset * 180 / Math.PI).toFixed(1)}°`);
|
|
40842
40851
|
}
|
|
40843
40852
|
else {
|
|
40844
40853
|
// Position in a grid pattern starting from center
|
|
@@ -40891,48 +40900,53 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40891
40900
|
let polygon = [];
|
|
40892
40901
|
const timestamp = Date.now();
|
|
40893
40902
|
if (camera?.floorPlanPosition) {
|
|
40894
|
-
// Get camera's FOV direction
|
|
40903
|
+
// Get camera's FOV direction (cast to any for flexible access)
|
|
40895
40904
|
const fov = (camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 });
|
|
40896
40905
|
const direction = fov.direction || 0;
|
|
40897
|
-
const range = fov.range || 80;
|
|
40898
40906
|
const fovAngle = fov.angle || 90;
|
|
40907
|
+
// Get floor plan scale (pixels per foot)
|
|
40908
|
+
const floorPlanScale = topology.floorPlanScale || 5;
|
|
40899
40909
|
// Convert direction to radians (0 = up/north, 90 = right/east)
|
|
40900
40910
|
const dirRad = (direction - 90) * Math.PI / 180;
|
|
40901
40911
|
const halfFov = (fovAngle / 2) * Math.PI / 180;
|
|
40902
40912
|
const camX = camera.floorPlanPosition.x;
|
|
40903
40913
|
const camY = camera.floorPlanPosition.y;
|
|
40904
|
-
// Use
|
|
40914
|
+
// Use distanceFeet from the zone metadata for accurate positioning
|
|
40915
|
+
const zoneData = zone;
|
|
40916
|
+
const distanceFeet = zoneData.distanceFeet || 40; // Default 40ft if not set
|
|
40917
|
+
const distanceInPixels = distanceFeet * floorPlanScale;
|
|
40918
|
+
// Zone size based on coverage (larger coverage = wider zone)
|
|
40919
|
+
const zoneWidthFeet = Math.sqrt(zone.coverage) * 30; // e.g., 50% coverage = ~21ft wide
|
|
40920
|
+
const zoneWidthPixels = zoneWidthFeet * floorPlanScale;
|
|
40921
|
+
const zoneDepthPixels = (zone.coverage * 20) * floorPlanScale; // Depth based on coverage
|
|
40922
|
+
// Use bounding box for horizontal positioning if available
|
|
40905
40923
|
const bbox = zone.boundingBox; // [x, y, width, height] normalized 0-1
|
|
40906
|
-
let innerRadius;
|
|
40907
|
-
let outerRadius;
|
|
40908
40924
|
let angleStart;
|
|
40909
40925
|
let angleEnd;
|
|
40926
|
+
let innerRadius;
|
|
40927
|
+
let outerRadius;
|
|
40910
40928
|
if (bbox && bbox.length >= 4) {
|
|
40911
|
-
// Map bounding box to
|
|
40929
|
+
// Map bounding box X to angle within FOV
|
|
40912
40930
|
const bboxLeft = bbox[0];
|
|
40913
40931
|
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
40932
|
angleStart = dirRad + (bboxLeft - 0.5) * 2 * halfFov;
|
|
40918
40933
|
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)}°`);
|
|
40934
|
+
// Use distanceFeet for depth, with a spread based on coverage
|
|
40935
|
+
innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
|
|
40936
|
+
outerRadius = distanceInPixels + zoneDepthPixels / 2;
|
|
40937
|
+
this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using bbox [${bbox.join(',')}]`);
|
|
40927
40938
|
}
|
|
40928
40939
|
else {
|
|
40929
|
-
// Fallback: wedge-shaped zone
|
|
40940
|
+
// Fallback: wedge-shaped zone covering portion of FOV
|
|
40930
40941
|
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
|
-
|
|
40942
|
+
// Spread horizontally based on existing zones
|
|
40943
|
+
const offset = (existingFromCamera % 3 - 1) * halfFov * 0.5;
|
|
40944
|
+
angleStart = dirRad + offset - halfFov * 0.3;
|
|
40945
|
+
angleEnd = dirRad + offset + halfFov * 0.3;
|
|
40946
|
+
// Use distanceFeet for depth
|
|
40947
|
+
innerRadius = Math.max(distanceInPixels - zoneDepthPixels / 2, 10);
|
|
40948
|
+
outerRadius = distanceInPixels + zoneDepthPixels / 2;
|
|
40949
|
+
this.console.log(`[Discovery] Zone "${zone.name}" at ${distanceFeet}ft using fallback spread`);
|
|
40936
40950
|
}
|
|
40937
40951
|
// Create arc polygon
|
|
40938
40952
|
const steps = 8;
|
|
@@ -40952,7 +40966,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
|
|
|
40952
40966
|
y: camY + Math.sin(angle) * outerRadius,
|
|
40953
40967
|
});
|
|
40954
40968
|
}
|
|
40955
|
-
this.console.log(`[Discovery] Creating zone "${zone.name}"
|
|
40969
|
+
this.console.log(`[Discovery] Creating zone "${zone.name}" at ${distanceFeet}ft (${distanceInPixels}px) from ${camera.name}`);
|
|
40956
40970
|
}
|
|
40957
40971
|
else {
|
|
40958
40972
|
// Fallback: rectangular zone at default location
|
|
@@ -42554,6 +42568,7 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
42554
42568
|
const response = await fetch('../api/topology');
|
|
42555
42569
|
if (response.ok) {
|
|
42556
42570
|
topology = await response.json();
|
|
42571
|
+
console.log('[Load] Loaded topology with', (topology.drawnZones || []).length, 'zones');
|
|
42557
42572
|
if (!topology.drawings) topology.drawings = [];
|
|
42558
42573
|
// Load floor plan scale if saved
|
|
42559
42574
|
if (topology.floorPlanScale) {
|
|
@@ -43071,14 +43086,24 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43071
43086
|
async function saveTopology() {
|
|
43072
43087
|
try {
|
|
43073
43088
|
setStatus('Saving...', 'warning');
|
|
43089
|
+
console.log('[Save] Saving topology with', (topology.drawnZones || []).length, 'zones');
|
|
43074
43090
|
const response = await fetch('../api/topology', {
|
|
43075
43091
|
method: 'PUT',
|
|
43076
43092
|
headers: { 'Content-Type': 'application/json' },
|
|
43077
43093
|
body: JSON.stringify(topology)
|
|
43078
43094
|
});
|
|
43079
|
-
if (response.ok) {
|
|
43080
|
-
|
|
43081
|
-
|
|
43095
|
+
if (response.ok) {
|
|
43096
|
+
console.log('[Save] Save successful');
|
|
43097
|
+
setStatus('Saved successfully', 'success');
|
|
43098
|
+
} else {
|
|
43099
|
+
const errorText = await response.text();
|
|
43100
|
+
console.error('[Save] Save failed:', response.status, errorText);
|
|
43101
|
+
setStatus('Failed to save: ' + errorText, 'error');
|
|
43102
|
+
}
|
|
43103
|
+
} catch (e) {
|
|
43104
|
+
console.error('Failed to save topology:', e);
|
|
43105
|
+
setStatus('Failed to save', 'error');
|
|
43106
|
+
}
|
|
43082
43107
|
}
|
|
43083
43108
|
|
|
43084
43109
|
function resizeCanvas() {
|
|
@@ -43878,9 +43903,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43878
43903
|
polygon: currentZonePoints.map(pt => ({ x: pt.x, y: pt.y })),
|
|
43879
43904
|
};
|
|
43880
43905
|
|
|
43906
|
+
console.log('[Zone] Creating zone:', zone.name, 'with', zone.polygon.length, 'points');
|
|
43907
|
+
|
|
43881
43908
|
if (!topology.drawnZones) topology.drawnZones = [];
|
|
43882
43909
|
topology.drawnZones.push(zone);
|
|
43883
43910
|
|
|
43911
|
+
console.log('[Zone] Total zones now:', topology.drawnZones.length);
|
|
43912
|
+
|
|
43884
43913
|
// Reset state
|
|
43885
43914
|
zoneDrawingMode = false;
|
|
43886
43915
|
currentZonePoints = [];
|
|
@@ -43889,7 +43918,19 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43889
43918
|
setTool('select');
|
|
43890
43919
|
updateUI();
|
|
43891
43920
|
render();
|
|
43892
|
-
|
|
43921
|
+
|
|
43922
|
+
console.log('[Zone] After updateUI/render, zones in topology:', (topology.drawnZones || []).length);
|
|
43923
|
+
|
|
43924
|
+
setStatus('Zone "' + zone.name + '" created - saving...', 'success');
|
|
43925
|
+
|
|
43926
|
+
// Auto-save after creating a zone
|
|
43927
|
+
saveTopology().then(() => {
|
|
43928
|
+
console.log('[Zone] Save completed, zones in topology:', (topology.drawnZones || []).length);
|
|
43929
|
+
setStatus('Zone "' + zone.name + '" saved', 'success');
|
|
43930
|
+
}).catch(err => {
|
|
43931
|
+
console.error('Failed to auto-save zone:', err);
|
|
43932
|
+
setStatus('Zone created but auto-save failed - click Save', 'warning');
|
|
43933
|
+
});
|
|
43893
43934
|
}
|
|
43894
43935
|
|
|
43895
43936
|
function selectZone(id) {
|
|
@@ -43914,9 +43955,9 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43914
43955
|
'<button class="btn btn-primary" onclick="deleteZone(\\'' + id + '\\')" style="background: #dc2626; width: 100%;">Delete Zone</button>';
|
|
43915
43956
|
}
|
|
43916
43957
|
|
|
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; } }
|
|
43958
|
+
function updateZoneName(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.name = value; updateUI(); render(); saveTopology(); } }
|
|
43959
|
+
function updateZoneType(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.type = value; updateUI(); render(); saveTopology(); } }
|
|
43960
|
+
function updateZoneDesc(id, value) { const z = (topology.drawnZones || []).find(z => z.id === id); if (z) { z.description = value || undefined; saveTopology(); } }
|
|
43920
43961
|
function deleteZone(id) {
|
|
43921
43962
|
if (!confirm('Delete this zone?')) return;
|
|
43922
43963
|
topology.drawnZones = (topology.drawnZones || []).filter(z => z.id !== id);
|
|
@@ -43924,7 +43965,8 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
|
|
|
43924
43965
|
document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select an item to edit.</p>';
|
|
43925
43966
|
updateUI();
|
|
43926
43967
|
render();
|
|
43927
|
-
setStatus('Zone deleted', 'success');
|
|
43968
|
+
setStatus('Zone deleted - saving...', 'success');
|
|
43969
|
+
saveTopology().then(() => setStatus('Zone deleted', 'success'));
|
|
43928
43970
|
}
|
|
43929
43971
|
|
|
43930
43972
|
function useBlankCanvas() {
|