@blueharford/scrypted-spatial-awareness 0.4.1 → 0.4.3
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/package.json +1 -1
- package/src/main.ts +41 -9
- package/src/ui/editor-html.ts +28 -2
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -15,6 +15,8 @@ import sdk, {
|
|
|
15
15
|
Readme,
|
|
16
16
|
} from '@scrypted/sdk';
|
|
17
17
|
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
18
20
|
import {
|
|
19
21
|
CameraTopology,
|
|
20
22
|
createEmptyTopology,
|
|
@@ -1044,14 +1046,34 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
1044
1046
|
}
|
|
1045
1047
|
}
|
|
1046
1048
|
|
|
1049
|
+
private getFloorPlanPath(): string {
|
|
1050
|
+
// Use plugin's data directory for persistent storage
|
|
1051
|
+
const pluginVolume = process.env.SCRYPTED_PLUGIN_VOLUME || '/server/volume/plugins/@blueharford/scrypted-spatial-awareness';
|
|
1052
|
+
const dataDir = path.join(pluginVolume, 'data');
|
|
1053
|
+
// Ensure directory exists
|
|
1054
|
+
if (!fs.existsSync(dataDir)) {
|
|
1055
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
1056
|
+
}
|
|
1057
|
+
return path.join(dataDir, 'floorplan.png');
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1047
1060
|
private handleFloorPlanRequest(request: HttpRequest, response: HttpResponse): void {
|
|
1048
1061
|
if (request.method === 'GET') {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1062
|
+
try {
|
|
1063
|
+
const floorPlanPath = this.getFloorPlanPath();
|
|
1064
|
+
if (fs.existsSync(floorPlanPath)) {
|
|
1065
|
+
const imageBuffer = fs.readFileSync(floorPlanPath);
|
|
1066
|
+
const imageData = 'data:image/png;base64,' + imageBuffer.toString('base64');
|
|
1067
|
+
response.send(JSON.stringify({ imageData }), {
|
|
1068
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1069
|
+
});
|
|
1070
|
+
} else {
|
|
1071
|
+
response.send(JSON.stringify({ imageData: null }), {
|
|
1072
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
this.console.error('Failed to read floor plan:', e);
|
|
1055
1077
|
response.send(JSON.stringify({ imageData: null }), {
|
|
1056
1078
|
headers: { 'Content-Type': 'application/json' },
|
|
1057
1079
|
});
|
|
@@ -1059,13 +1081,23 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
1059
1081
|
} else if (request.method === 'POST') {
|
|
1060
1082
|
try {
|
|
1061
1083
|
const body = JSON.parse(request.body!);
|
|
1062
|
-
|
|
1084
|
+
const imageData = body.imageData as string;
|
|
1085
|
+
|
|
1086
|
+
// Extract base64 data (remove data:image/xxx;base64, prefix)
|
|
1087
|
+
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, '');
|
|
1088
|
+
const imageBuffer = Buffer.from(base64Data, 'base64');
|
|
1089
|
+
|
|
1090
|
+
const floorPlanPath = this.getFloorPlanPath();
|
|
1091
|
+
fs.writeFileSync(floorPlanPath, imageBuffer);
|
|
1092
|
+
|
|
1093
|
+
this.console.log('Floor plan saved to:', floorPlanPath, 'size:', imageBuffer.length);
|
|
1063
1094
|
response.send(JSON.stringify({ success: true }), {
|
|
1064
1095
|
headers: { 'Content-Type': 'application/json' },
|
|
1065
1096
|
});
|
|
1066
1097
|
} catch (e) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1098
|
+
this.console.error('Failed to save floor plan:', e);
|
|
1099
|
+
response.send(JSON.stringify({ error: 'Failed to save floor plan' }), {
|
|
1100
|
+
code: 500,
|
|
1069
1101
|
headers: { 'Content-Type': 'application/json' },
|
|
1070
1102
|
});
|
|
1071
1103
|
}
|
package/src/ui/editor-html.ts
CHANGED
|
@@ -342,10 +342,27 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
342
342
|
if (response.ok) {
|
|
343
343
|
topology = await response.json();
|
|
344
344
|
if (!topology.drawings) topology.drawings = [];
|
|
345
|
+
// Load floor plan from separate storage (handles legacy imageData in topology too)
|
|
345
346
|
if (topology.floorPlan?.imageData) {
|
|
347
|
+
// Legacy: imageData was stored in topology
|
|
346
348
|
await loadFloorPlanImage(topology.floorPlan.imageData);
|
|
347
349
|
} else if (topology.floorPlan?.type === 'blank') {
|
|
348
350
|
blankCanvasMode = true;
|
|
351
|
+
} else {
|
|
352
|
+
// Always try to load from floor-plan endpoint (handles uploaded and missing cases)
|
|
353
|
+
try {
|
|
354
|
+
const fpResponse = await fetch('../api/floor-plan');
|
|
355
|
+
if (fpResponse.ok) {
|
|
356
|
+
const fpData = await fpResponse.json();
|
|
357
|
+
if (fpData.imageData) {
|
|
358
|
+
await loadFloorPlanImage(fpData.imageData);
|
|
359
|
+
// Update topology reference if not set
|
|
360
|
+
if (!topology.floorPlan || topology.floorPlan.type !== 'uploaded') {
|
|
361
|
+
topology.floorPlan = { type: 'uploaded', width: floorPlanImage.width, height: floorPlanImage.height };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
} catch (err) { console.error('Failed to load floor plan:', err); }
|
|
349
366
|
}
|
|
350
367
|
}
|
|
351
368
|
} catch (e) { console.error('Failed to load topology:', e); }
|
|
@@ -976,7 +993,16 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
976
993
|
reader.onload = async (e) => {
|
|
977
994
|
const imageData = e.target.result;
|
|
978
995
|
await loadFloorPlanImage(imageData);
|
|
979
|
-
|
|
996
|
+
// Store floor plan separately via API (not in topology JSON to avoid size issues)
|
|
997
|
+
try {
|
|
998
|
+
await fetch('../api/floor-plan', {
|
|
999
|
+
method: 'POST',
|
|
1000
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1001
|
+
body: JSON.stringify({ imageData })
|
|
1002
|
+
});
|
|
1003
|
+
} catch (err) { console.error('Failed to save floor plan:', err); }
|
|
1004
|
+
// Store reference in topology (without the large imageData)
|
|
1005
|
+
topology.floorPlan = { type: 'uploaded', width: floorPlanImage.width, height: floorPlanImage.height };
|
|
980
1006
|
closeModal('upload-modal');
|
|
981
1007
|
render();
|
|
982
1008
|
};
|
|
@@ -1138,7 +1164,7 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
1138
1164
|
function updateConnectionName(id, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.name = value; updateUI(); }
|
|
1139
1165
|
function updateTransitTime(id, field, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.transitTime[field] = parseInt(value) * 1000; }
|
|
1140
1166
|
function updateConnectionBidi(id, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.bidirectional = value; render(); }
|
|
1141
|
-
function deleteCamera(id) { if (!confirm('Delete this camera?')) return; topology.cameras = topology.cameras.filter(c => c.deviceId !== id); topology.connections = topology.connections.filter(c => c.fromCameraId !== id && c.toCameraId !== id); selectedItem = null; document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select a camera or connection.</p>'; updateUI(); render(); }
|
|
1167
|
+
function deleteCamera(id) { if (!confirm('Delete this camera?')) return; topology.cameras = topology.cameras.filter(c => c.deviceId !== id); topology.connections = topology.connections.filter(c => c.fromCameraId !== id && c.toCameraId !== id); selectedItem = null; document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select a camera or connection.</p>'; updateCameraSelects(); updateUI(); render(); }
|
|
1142
1168
|
function deleteConnection(id) { if (!confirm('Delete this connection?')) return; topology.connections = topology.connections.filter(c => c.id !== id); selectedItem = null; document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select a camera or connection.</p>'; updateUI(); render(); }
|
|
1143
1169
|
function setTool(tool) {
|
|
1144
1170
|
currentTool = tool;
|