@blueharford/scrypted-spatial-awareness 0.5.9 → 0.6.0

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/plugin.zip CHANGED
Binary file
@@ -40271,9 +40271,11 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40271
40271
  if (suggestion.type === 'zone' && suggestion.zone) {
40272
40272
  // Create a drawn zone from the discovery zone
40273
40273
  const zone = suggestion.zone;
40274
- // Find cameras that see this zone type to determine position
40275
- const cameraWithZone = suggestion.sourceCameras?.[0];
40276
- const camera = cameraWithZone ? topology.cameras.find(c => c.deviceId === cameraWithZone || c.name === cameraWithZone) : null;
40274
+ // Find cameras that see this zone
40275
+ const sourceCameras = suggestion.sourceCameras || [];
40276
+ const camera = sourceCameras[0]
40277
+ ? topology.cameras.find(c => c.deviceId === sourceCameras[0] || c.name === sourceCameras[0])
40278
+ : null;
40277
40279
  // Create a default polygon near the camera or at a default location
40278
40280
  let centerX = 300;
40279
40281
  let centerY = 200;
@@ -40283,8 +40285,10 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40283
40285
  }
40284
40286
  // Create a rectangular zone (user can edit later)
40285
40287
  const size = 100;
40288
+ const timestamp = Date.now();
40289
+ // 1. Create DrawnZone (visual on floor plan)
40286
40290
  const drawnZone = {
40287
- id: `zone_${Date.now()}`,
40291
+ id: `zone_${timestamp}`,
40288
40292
  name: zone.name,
40289
40293
  type: (zone.type || 'custom'),
40290
40294
  description: zone.description,
@@ -40294,41 +40298,75 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40294
40298
  { x: centerX + size / 2, y: centerY + size / 2 },
40295
40299
  { x: centerX - size / 2, y: centerY + size / 2 },
40296
40300
  ],
40301
+ linkedCameras: sourceCameras,
40297
40302
  };
40298
40303
  if (!topology.drawnZones) {
40299
40304
  topology.drawnZones = [];
40300
40305
  }
40301
40306
  topology.drawnZones.push(drawnZone);
40307
+ // 2. Create GlobalZone (for tracking/alerting)
40308
+ const globalZoneType = this.mapZoneTypeToGlobalType(zone.type);
40309
+ const cameraZones = sourceCameras
40310
+ .map(camRef => {
40311
+ const cam = topology.cameras.find(c => c.deviceId === camRef || c.name === camRef);
40312
+ if (!cam)
40313
+ return null;
40314
+ return {
40315
+ cameraId: cam.deviceId,
40316
+ zone: [[0, 0], [100, 0], [100, 100], [0, 100]], // Full frame default
40317
+ };
40318
+ })
40319
+ .filter((z) => z !== null);
40320
+ if (cameraZones.length > 0) {
40321
+ const globalZone = {
40322
+ id: `global_${timestamp}`,
40323
+ name: zone.name,
40324
+ type: globalZoneType,
40325
+ cameraZones,
40326
+ };
40327
+ if (!topology.globalZones) {
40328
+ topology.globalZones = [];
40329
+ }
40330
+ topology.globalZones.push(globalZone);
40331
+ this.console.log(`[Discovery] Added zone: ${zone.name} (${zone.type}) - DrawnZone and GlobalZone created`);
40332
+ }
40333
+ else {
40334
+ this.console.log(`[Discovery] Added zone: ${zone.name} (${zone.type}) - DrawnZone only (no cameras matched)`);
40335
+ }
40302
40336
  updated = true;
40303
- this.console.log(`[Discovery] Added zone: ${zone.name} (${zone.type})`);
40304
40337
  }
40305
40338
  if (suggestion.type === 'connection' && suggestion.connection) {
40306
40339
  // Add new connection to topology
40307
40340
  const conn = suggestion.connection;
40308
- // Ensure cameras have floor plan positions for visibility
40309
- const fromCamera = topology.cameras.find(c => c.deviceId === conn.fromCameraId || c.name === conn.fromCameraId);
40310
- const toCamera = topology.cameras.find(c => c.deviceId === conn.toCameraId || c.name === conn.toCameraId);
40311
- // Auto-assign floor plan positions if missing
40312
- if (fromCamera && !fromCamera.floorPlanPosition) {
40313
- const idx = topology.cameras.indexOf(fromCamera);
40314
- fromCamera.floorPlanPosition = {
40315
- x: 150 + (idx % 3) * 200,
40316
- y: 150 + Math.floor(idx / 3) * 150,
40317
- };
40318
- this.console.log(`[Discovery] Auto-positioned camera: ${fromCamera.name}`);
40341
+ // Resolve camera references - LLM may return names instead of deviceIds
40342
+ // Use case-insensitive matching for names
40343
+ const fromCamera = topology.cameras.find(c => c.deviceId === conn.fromCameraId ||
40344
+ c.name === conn.fromCameraId ||
40345
+ c.name.toLowerCase() === conn.fromCameraId.toLowerCase());
40346
+ const toCamera = topology.cameras.find(c => c.deviceId === conn.toCameraId ||
40347
+ c.name === conn.toCameraId ||
40348
+ c.name.toLowerCase() === conn.toCameraId.toLowerCase());
40349
+ // Don't create connection if cameras not found
40350
+ if (!fromCamera || !toCamera) {
40351
+ this.console.warn(`[Discovery] Cannot create connection: camera not found. from="${conn.fromCameraId}" to="${conn.toCameraId}"`);
40352
+ this.console.warn(`[Discovery] Available cameras: ${topology.cameras.map(c => `${c.name} (${c.deviceId})`).join(', ')}`);
40353
+ return;
40319
40354
  }
40320
- if (toCamera && !toCamera.floorPlanPosition) {
40321
- const idx = topology.cameras.indexOf(toCamera);
40322
- toCamera.floorPlanPosition = {
40323
- x: 150 + (idx % 3) * 200,
40324
- y: 150 + Math.floor(idx / 3) * 150,
40325
- };
40326
- this.console.log(`[Discovery] Auto-positioned camera: ${toCamera.name}`);
40355
+ // Check if connection already exists
40356
+ const existingConn = topology.connections.find(c => (c.fromCameraId === fromCamera.deviceId && c.toCameraId === toCamera.deviceId) ||
40357
+ (c.bidirectional && c.fromCameraId === toCamera.deviceId && c.toCameraId === fromCamera.deviceId));
40358
+ if (existingConn) {
40359
+ this.console.log(`[Discovery] Connection already exists between ${fromCamera.name} and ${toCamera.name}`);
40360
+ return;
40361
+ }
40362
+ // Warn if cameras don't have positions yet
40363
+ if (!fromCamera.floorPlanPosition || !toCamera.floorPlanPosition) {
40364
+ this.console.warn(`[Discovery] Note: One or both cameras not positioned on floor plan yet`);
40327
40365
  }
40328
40366
  const newConnection = {
40329
40367
  id: `conn_${Date.now()}`,
40330
- fromCameraId: fromCamera?.deviceId || conn.fromCameraId,
40331
- toCameraId: toCamera?.deviceId || conn.toCameraId,
40368
+ fromCameraId: fromCamera.deviceId,
40369
+ toCameraId: toCamera.deviceId,
40332
40370
  bidirectional: conn.bidirectional,
40333
40371
  // Default exit/entry zones covering full frame
40334
40372
  exitZone: [[0, 0], [100, 0], [100, 100], [0, 100]],
@@ -40342,7 +40380,7 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40342
40380
  };
40343
40381
  topology.connections.push(newConnection);
40344
40382
  updated = true;
40345
- this.console.log(`[Discovery] Added connection: ${fromCamera?.name || conn.fromCameraId} -> ${toCamera?.name || conn.toCameraId}`);
40383
+ this.console.log(`[Discovery] Added connection: ${fromCamera.name} (${fromCamera.deviceId}) -> ${toCamera.name} (${toCamera.deviceId})`);
40346
40384
  }
40347
40385
  if (updated) {
40348
40386
  // Save updated topology
@@ -40350,6 +40388,24 @@ class SpatialAwarenessPlugin extends sdk_1.ScryptedDeviceBase {
40350
40388
  this.trackingEngine.updateTopology(topology);
40351
40389
  }
40352
40390
  }
40391
+ /** Map discovered zone types to GlobalZone types for tracking/alerting */
40392
+ mapZoneTypeToGlobalType(type) {
40393
+ const mapping = {
40394
+ 'yard': 'dwell',
40395
+ 'driveway': 'entry',
40396
+ 'street': 'entry',
40397
+ 'patio': 'dwell',
40398
+ 'walkway': 'entry',
40399
+ 'parking': 'dwell',
40400
+ 'garden': 'dwell',
40401
+ 'pool': 'restricted',
40402
+ 'entrance': 'entry',
40403
+ 'garage': 'entry',
40404
+ 'deck': 'dwell',
40405
+ 'custom': 'dwell',
40406
+ };
40407
+ return mapping[type] || 'dwell';
40408
+ }
40353
40409
  serveEditorUI(response) {
40354
40410
  response.send(editor_html_1.EDITOR_HTML, {
40355
40411
  headers: { 'Content-Type': 'text/html' },
@@ -41480,7 +41536,7 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
41480
41536
  <button class="btn btn-small btn-primary" id="scan-now-btn" onclick="runDiscoveryScan()">Scan Now</button>
41481
41537
  </div>
41482
41538
  <div id="discovery-status" style="font-size: 11px; color: #888; margin-bottom: 8px;">
41483
- <span id="discovery-status-text">Not scanned yet</span>
41539
+ <span id="discovery-status-text">Position cameras first, then scan</span>
41484
41540
  </div>
41485
41541
  <div id="discovery-suggestions-list"></div>
41486
41542
  </div>
@@ -41513,6 +41569,7 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
41513
41569
  </div>
41514
41570
  <div class="toolbar-group">
41515
41571
  <button class="btn" onclick="clearDrawings()">Clear Drawings</button>
41572
+ <button class="btn" onclick="clearAllTopology()" style="background: #dc2626;">Delete All</button>
41516
41573
  </div>
41517
41574
  <div class="toolbar-group">
41518
41575
  <button class="btn btn-primary" onclick="saveTopology()">Save</button>
@@ -42015,6 +42072,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
42015
42072
  let scanPollingInterval = null;
42016
42073
 
42017
42074
  async function runDiscoveryScan() {
42075
+ // Check if cameras are positioned on the floor plan
42076
+ const positionedCameras = topology.cameras.filter(c => c.floorPlanPosition);
42077
+ if (positionedCameras.length === 0) {
42078
+ alert('Please position at least one camera on the floor plan before running discovery.\\n\\nSteps:\\n1. Click "Place Camera" in the toolbar\\n2. Click on the floor plan where the camera is located\\n3. Select the camera from the dropdown\\n4. Drag the rotation handle to set its direction\\n5. Then run discovery to detect zones and connections');
42079
+ return;
42080
+ }
42081
+
42018
42082
  const scanBtn = document.getElementById('scan-now-btn');
42019
42083
  const statusText = document.getElementById('discovery-status-text');
42020
42084
  scanBtn.disabled = true;
@@ -42622,6 +42686,29 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
42622
42686
  function drawCamera(camera) {
42623
42687
  const pos = camera.floorPlanPosition;
42624
42688
  const isSelected = selectedItem?.type === 'camera' && selectedItem?.id === camera.deviceId;
42689
+
42690
+ // Get FOV settings or defaults
42691
+ const fov = camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 };
42692
+ const direction = (fov.mode === 'simple' || !fov.mode) ? (fov.direction || 0) : 0;
42693
+ const fovAngle = (fov.mode === 'simple' || !fov.mode) ? (fov.angle || 90) : 90;
42694
+ const range = (fov.mode === 'simple' || !fov.mode) ? (fov.range || 80) : 80;
42695
+
42696
+ // Convert direction to radians (0 = up/north, 90 = right/east)
42697
+ const dirRad = (direction - 90) * Math.PI / 180;
42698
+ const halfFov = (fovAngle / 2) * Math.PI / 180;
42699
+
42700
+ // Draw FOV cone
42701
+ ctx.beginPath();
42702
+ ctx.moveTo(pos.x, pos.y);
42703
+ ctx.arc(pos.x, pos.y, range, dirRad - halfFov, dirRad + halfFov);
42704
+ ctx.closePath();
42705
+ ctx.fillStyle = isSelected ? 'rgba(233, 69, 96, 0.15)' : 'rgba(76, 175, 80, 0.15)';
42706
+ ctx.fill();
42707
+ ctx.strokeStyle = isSelected ? 'rgba(233, 69, 96, 0.5)' : 'rgba(76, 175, 80, 0.5)';
42708
+ ctx.lineWidth = 1;
42709
+ ctx.stroke();
42710
+
42711
+ // Draw camera circle
42625
42712
  ctx.beginPath();
42626
42713
  ctx.arc(pos.x, pos.y, 20, 0, Math.PI * 2);
42627
42714
  ctx.fillStyle = isSelected ? '#e94560' : '#0f3460';
@@ -42629,12 +42716,41 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
42629
42716
  ctx.strokeStyle = '#fff';
42630
42717
  ctx.lineWidth = 2;
42631
42718
  ctx.stroke();
42719
+
42720
+ // Draw camera icon/text
42632
42721
  ctx.fillStyle = '#fff';
42633
42722
  ctx.font = '12px sans-serif';
42634
42723
  ctx.textAlign = 'center';
42635
42724
  ctx.textBaseline = 'middle';
42636
42725
  ctx.fillText('CAM', pos.x, pos.y);
42637
42726
  ctx.fillText(camera.name, pos.x, pos.y + 35);
42727
+
42728
+ // Draw direction handle (when selected) for rotation
42729
+ if (isSelected) {
42730
+ const handleLength = 45;
42731
+ const handleX = pos.x + Math.cos(dirRad) * handleLength;
42732
+ const handleY = pos.y + Math.sin(dirRad) * handleLength;
42733
+
42734
+ // Handle line
42735
+ ctx.beginPath();
42736
+ ctx.moveTo(pos.x + Math.cos(dirRad) * 20, pos.y + Math.sin(dirRad) * 20);
42737
+ ctx.lineTo(handleX, handleY);
42738
+ ctx.strokeStyle = '#ff6b6b';
42739
+ ctx.lineWidth = 3;
42740
+ ctx.stroke();
42741
+
42742
+ // Handle grip (circle at end)
42743
+ ctx.beginPath();
42744
+ ctx.arc(handleX, handleY, 8, 0, Math.PI * 2);
42745
+ ctx.fillStyle = '#ff6b6b';
42746
+ ctx.fill();
42747
+ ctx.strokeStyle = '#fff';
42748
+ ctx.lineWidth = 2;
42749
+ ctx.stroke();
42750
+
42751
+ // Store handle position for hit detection
42752
+ camera._handlePos = { x: handleX, y: handleY };
42753
+ }
42638
42754
  }
42639
42755
 
42640
42756
  function drawConnection(from, to, conn) {
@@ -42895,7 +43011,16 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
42895
43011
 
42896
43012
  function showCameraProperties(camera) {
42897
43013
  const panel = document.getElementById('properties-panel');
42898
- panel.innerHTML = '<h3>Camera Properties</h3><div class="form-group"><label>Name</label><input type="text" value="' + camera.name + '" onchange="updateCameraName(\\'' + camera.deviceId + '\\', this.value)"></div><div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isEntryPoint ? 'checked' : '') + ' onchange="updateCameraEntry(\\'' + camera.deviceId + '\\', this.checked)">Entry Point</label></div><div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isExitPoint ? 'checked' : '') + ' onchange="updateCameraExit(\\'' + camera.deviceId + '\\', this.checked)">Exit Point</label></div><div class="form-group"><button class="btn" style="width: 100%; background: #f44336;" onclick="deleteCamera(\\'' + camera.deviceId + '\\')">Delete Camera</button></div>';
43014
+ const fov = camera.fov || { mode: 'simple', angle: 90, direction: 0, range: 80 };
43015
+ panel.innerHTML = '<h3>Camera Properties</h3>' +
43016
+ '<div class="form-group"><label>Name</label><input type="text" value="' + camera.name + '" onchange="updateCameraName(\\'' + camera.deviceId + '\\', this.value)"></div>' +
43017
+ '<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isEntryPoint ? 'checked' : '') + ' onchange="updateCameraEntry(\\'' + camera.deviceId + '\\', this.checked)">Entry Point</label></div>' +
43018
+ '<div class="form-group"><label class="checkbox-group"><input type="checkbox" ' + (camera.isExitPoint ? 'checked' : '') + ' onchange="updateCameraExit(\\'' + camera.deviceId + '\\', this.checked)">Exit Point</label></div>' +
43019
+ '<h4 style="margin-top: 15px; margin-bottom: 10px; color: #888;">Field of View</h4>' +
43020
+ '<div class="form-group"><label>Direction (0=up, 90=right)</label><input type="number" value="' + Math.round(fov.direction || 0) + '" min="0" max="359" onchange="updateCameraFov(\\'' + camera.deviceId + '\\', \\'direction\\', this.value)"></div>' +
43021
+ '<div class="form-group"><label>FOV Angle (degrees)</label><input type="number" value="' + (fov.angle || 90) + '" min="30" max="180" onchange="updateCameraFov(\\'' + camera.deviceId + '\\', \\'angle\\', this.value)"></div>' +
43022
+ '<div class="form-group"><label>Range (pixels)</label><input type="number" value="' + (fov.range || 80) + '" min="20" max="300" onchange="updateCameraFov(\\'' + camera.deviceId + '\\', \\'range\\', this.value)"></div>' +
43023
+ '<div class="form-group"><button class="btn" style="width: 100%; background: #f44336;" onclick="deleteCamera(\\'' + camera.deviceId + '\\')">Delete Camera</button></div>';
42899
43024
  }
42900
43025
 
42901
43026
  function showConnectionProperties(connection) {
@@ -42906,6 +43031,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
42906
43031
  function updateCameraName(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.name = value; updateUI(); }
42907
43032
  function updateCameraEntry(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isEntryPoint = value; }
42908
43033
  function updateCameraExit(id, value) { const camera = topology.cameras.find(c => c.deviceId === id); if (camera) camera.isExitPoint = value; }
43034
+ function updateCameraFov(id, field, value) {
43035
+ const camera = topology.cameras.find(c => c.deviceId === id);
43036
+ if (!camera) return;
43037
+ if (!camera.fov) camera.fov = { mode: 'simple', angle: 90, direction: 0, range: 80 };
43038
+ camera.fov[field] = parseFloat(value);
43039
+ render();
43040
+ }
42909
43041
  function updateConnectionName(id, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.name = value; updateUI(); }
42910
43042
  function updateTransitTime(id, field, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.transitTime[field] = parseInt(value) * 1000; }
42911
43043
  function updateConnectionBidi(id, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.bidirectional = value; render(); }
@@ -43049,10 +43181,30 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43049
43181
  setStatus('Drawings cleared', 'success');
43050
43182
  }
43051
43183
 
43184
+ function clearAllTopology() {
43185
+ if (!confirm('DELETE ALL TOPOLOGY DATA?\\n\\nThis will remove:\\n- All cameras\\n- All connections\\n- All landmarks\\n- All zones\\n- All drawings\\n\\nThis cannot be undone.')) return;
43186
+
43187
+ topology.cameras = [];
43188
+ topology.connections = [];
43189
+ topology.landmarks = [];
43190
+ topology.globalZones = [];
43191
+ topology.drawnZones = [];
43192
+ topology.drawings = [];
43193
+ topology.relationships = [];
43194
+
43195
+ selectedItem = null;
43196
+ document.getElementById('properties-panel').innerHTML = '<h3>Properties</h3><p style="color: #666;">Select an item to edit.</p>';
43197
+ updateCameraSelects();
43198
+ updateUI();
43199
+ render();
43200
+ setStatus('All topology data cleared', 'warning');
43201
+ }
43202
+
43052
43203
  function closeModal(id) { document.getElementById(id).classList.remove('active'); }
43053
43204
  function setStatus(text, type) { document.getElementById('status-text').textContent = text; const dot = document.getElementById('status-dot'); dot.className = 'status-dot'; if (type === 'warning') dot.classList.add('warning'); if (type === 'error') dot.classList.add('error'); }
43054
43205
 
43055
43206
  let dragging = null;
43207
+ let rotatingCamera = null;
43056
43208
 
43057
43209
  canvas.addEventListener('mousedown', (e) => {
43058
43210
  const rect = canvas.getBoundingClientRect();
@@ -43068,7 +43220,20 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43068
43220
  }
43069
43221
 
43070
43222
  if (currentTool === 'select') {
43071
- // Check cameras first
43223
+ // Check for rotation handle on selected camera first
43224
+ if (selectedItem?.type === 'camera') {
43225
+ const camera = topology.cameras.find(c => c.deviceId === selectedItem.id);
43226
+ if (camera?._handlePos) {
43227
+ const dist = Math.hypot(x - camera._handlePos.x, y - camera._handlePos.y);
43228
+ if (dist < 15) {
43229
+ rotatingCamera = camera;
43230
+ setStatus('Drag to rotate camera direction', 'warning');
43231
+ return;
43232
+ }
43233
+ }
43234
+ }
43235
+
43236
+ // Check cameras
43072
43237
  for (const camera of topology.cameras) {
43073
43238
  if (camera.floorPlanPosition) {
43074
43239
  const dist = Math.hypot(x - camera.floorPlanPosition.x, y - camera.floorPlanPosition.y);
@@ -43132,6 +43297,20 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43132
43297
  const x = e.clientX - rect.left;
43133
43298
  const y = e.clientY - rect.top;
43134
43299
 
43300
+ // Handle camera rotation
43301
+ if (rotatingCamera) {
43302
+ const pos = rotatingCamera.floorPlanPosition;
43303
+ const angle = Math.atan2(y - pos.y, x - pos.x);
43304
+ // Convert to our direction system (0 = up/north, 90 = right/east)
43305
+ const direction = (angle * 180 / Math.PI) + 90;
43306
+ if (!rotatingCamera.fov) {
43307
+ rotatingCamera.fov = { mode: 'simple', angle: 90, direction: 0, range: 80 };
43308
+ }
43309
+ rotatingCamera.fov.direction = ((direction % 360) + 360) % 360; // Normalize 0-360
43310
+ render();
43311
+ return;
43312
+ }
43313
+
43135
43314
  if (dragging) {
43136
43315
  if (dragging.type === 'camera') {
43137
43316
  dragging.item.floorPlanPosition.x = x;
@@ -43154,6 +43333,13 @@ exports.EDITOR_HTML = `<!DOCTYPE html>
43154
43333
  });
43155
43334
 
43156
43335
  canvas.addEventListener('mouseup', (e) => {
43336
+ // Clear camera rotation
43337
+ if (rotatingCamera) {
43338
+ setStatus('Camera direction updated', 'success');
43339
+ rotatingCamera = null;
43340
+ return;
43341
+ }
43342
+
43157
43343
  if (isDrawing && currentDrawing) {
43158
43344
  if (!topology.drawings) topology.drawings = [];
43159
43345
  // Normalize room coordinates if drawn backwards