@blueharford/scrypted-spatial-awareness 0.1.6 → 0.1.8
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 +22 -58
- package/src/ui/editor-html.ts +169 -25
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -335,63 +335,13 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
335
335
|
async getSettings(): Promise<Setting[]> {
|
|
336
336
|
const settings = await this.storageSettings.getSettings();
|
|
337
337
|
|
|
338
|
-
// Topology editor button that opens modal overlay
|
|
338
|
+
// Topology editor button that opens modal overlay (appended to body for proper z-index)
|
|
339
339
|
settings.push({
|
|
340
340
|
key: 'topologyEditor',
|
|
341
341
|
title: 'Topology Editor',
|
|
342
342
|
type: 'html' as any,
|
|
343
343
|
value: `
|
|
344
344
|
<style>
|
|
345
|
-
.sa-modal-overlay {
|
|
346
|
-
display: none;
|
|
347
|
-
position: fixed;
|
|
348
|
-
top: 0;
|
|
349
|
-
left: 0;
|
|
350
|
-
right: 0;
|
|
351
|
-
bottom: 0;
|
|
352
|
-
background: rgba(0, 0, 0, 0.85);
|
|
353
|
-
z-index: 999999;
|
|
354
|
-
align-items: center;
|
|
355
|
-
justify-content: center;
|
|
356
|
-
}
|
|
357
|
-
.sa-modal-overlay.active {
|
|
358
|
-
display: flex;
|
|
359
|
-
}
|
|
360
|
-
.sa-modal-container {
|
|
361
|
-
width: 95vw;
|
|
362
|
-
height: 90vh;
|
|
363
|
-
max-width: 1600px;
|
|
364
|
-
background: #1a1a2e;
|
|
365
|
-
border-radius: 12px;
|
|
366
|
-
overflow: hidden;
|
|
367
|
-
position: relative;
|
|
368
|
-
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
|
369
|
-
}
|
|
370
|
-
.sa-modal-close {
|
|
371
|
-
position: absolute;
|
|
372
|
-
top: 10px;
|
|
373
|
-
right: 10px;
|
|
374
|
-
z-index: 1000000;
|
|
375
|
-
background: #e94560;
|
|
376
|
-
color: white;
|
|
377
|
-
border: none;
|
|
378
|
-
width: 36px;
|
|
379
|
-
height: 36px;
|
|
380
|
-
border-radius: 50%;
|
|
381
|
-
font-size: 20px;
|
|
382
|
-
cursor: pointer;
|
|
383
|
-
display: flex;
|
|
384
|
-
align-items: center;
|
|
385
|
-
justify-content: center;
|
|
386
|
-
}
|
|
387
|
-
.sa-modal-close:hover {
|
|
388
|
-
background: #ff6b6b;
|
|
389
|
-
}
|
|
390
|
-
.sa-modal-iframe {
|
|
391
|
-
width: 100%;
|
|
392
|
-
height: 100%;
|
|
393
|
-
border: none;
|
|
394
|
-
}
|
|
395
345
|
.sa-open-btn {
|
|
396
346
|
background: linear-gradient(135deg, #e94560 0%, #0f3460 100%);
|
|
397
347
|
color: white;
|
|
@@ -424,16 +374,30 @@ export class SpatialAwarenessPlugin extends ScryptedDeviceBase
|
|
|
424
374
|
</style>
|
|
425
375
|
<div class="sa-btn-container">
|
|
426
376
|
<p class="sa-btn-desc">Configure camera positions, connections, and transit times</p>
|
|
427
|
-
<button class="sa-open-btn" onclick="
|
|
377
|
+
<button class="sa-open-btn" onclick="window.openSATopologyEditor()">
|
|
428
378
|
<span>⚙</span> Open Topology Editor
|
|
429
379
|
</button>
|
|
430
380
|
</div>
|
|
431
|
-
<
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
381
|
+
<script>
|
|
382
|
+
window.openSATopologyEditor = function() {
|
|
383
|
+
// Remove existing modal if any
|
|
384
|
+
const existing = document.getElementById('sa-topology-modal');
|
|
385
|
+
if (existing) existing.remove();
|
|
386
|
+
|
|
387
|
+
// Create modal and append to body
|
|
388
|
+
const modal = document.createElement('div');
|
|
389
|
+
modal.id = 'sa-topology-modal';
|
|
390
|
+
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);z-index:2147483647;display:flex;align-items:center;justify-content:center;';
|
|
391
|
+
modal.innerHTML = \`
|
|
392
|
+
<div style="width:95vw;height:92vh;max-width:1800px;background:#1a1a2e;border-radius:12px;overflow:hidden;position:relative;box-shadow:0 25px 50px rgba(0,0,0,0.5);">
|
|
393
|
+
<button onclick="document.getElementById('sa-topology-modal').remove()" style="position:absolute;top:15px;right:15px;z-index:2147483647;background:#e94560;color:white;border:none;width:40px;height:40px;border-radius:50%;font-size:24px;cursor:pointer;display:flex;align-items:center;justify-content:center;">×</button>
|
|
394
|
+
<iframe src="/endpoint/@blueharford/scrypted-spatial-awareness/ui/editor" style="width:100%;height:100%;border:none;"></iframe>
|
|
395
|
+
</div>
|
|
396
|
+
\`;
|
|
397
|
+
modal.onclick = function(e) { if (e.target === modal) modal.remove(); };
|
|
398
|
+
document.body.appendChild(modal);
|
|
399
|
+
};
|
|
400
|
+
</script>
|
|
437
401
|
`,
|
|
438
402
|
group: 'Topology',
|
|
439
403
|
});
|
package/src/ui/editor-html.ts
CHANGED
|
@@ -94,12 +94,18 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
94
94
|
<div class="editor">
|
|
95
95
|
<div class="toolbar">
|
|
96
96
|
<div class="toolbar-group">
|
|
97
|
-
<button class="btn" onclick="uploadFloorPlan()">Upload
|
|
97
|
+
<button class="btn" onclick="uploadFloorPlan()">Upload Image</button>
|
|
98
|
+
<button class="btn" onclick="useBlankCanvas()">Blank Canvas</button>
|
|
98
99
|
</div>
|
|
99
100
|
<div class="toolbar-group">
|
|
100
|
-
<button class="btn" onclick="setTool('select')">Select</button>
|
|
101
|
-
<button class="btn" onclick="setTool('
|
|
102
|
-
<button class="btn" onclick="setTool('
|
|
101
|
+
<button class="btn" id="tool-select" onclick="setTool('select')">Select</button>
|
|
102
|
+
<button class="btn" id="tool-wall" onclick="setTool('wall')">Draw Wall</button>
|
|
103
|
+
<button class="btn" id="tool-room" onclick="setTool('room')">Draw Room</button>
|
|
104
|
+
<button class="btn" id="tool-camera" onclick="setTool('camera')">Place Camera</button>
|
|
105
|
+
<button class="btn" id="tool-connect" onclick="setTool('connect')">Connect</button>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="toolbar-group">
|
|
108
|
+
<button class="btn" onclick="clearDrawings()">Clear Drawings</button>
|
|
103
109
|
</div>
|
|
104
110
|
<div class="toolbar-group">
|
|
105
111
|
<button class="btn btn-primary" onclick="saveTopology()">Save</button>
|
|
@@ -109,9 +115,12 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
109
115
|
<canvas id="floor-plan-canvas"></canvas>
|
|
110
116
|
<div class="canvas-placeholder" id="canvas-placeholder">
|
|
111
117
|
<h2>Floor Plan Editor</h2>
|
|
112
|
-
<p>Upload a
|
|
118
|
+
<p>Upload an image or use a blank canvas to draw your floor plan</p>
|
|
113
119
|
<br>
|
|
114
|
-
<
|
|
120
|
+
<div style="display: flex; gap: 15px; justify-content: center;">
|
|
121
|
+
<button class="btn btn-primary" onclick="uploadFloorPlan()">Upload Image</button>
|
|
122
|
+
<button class="btn" onclick="useBlankCanvas()">Use Blank Canvas</button>
|
|
123
|
+
</div>
|
|
115
124
|
</div>
|
|
116
125
|
</div>
|
|
117
126
|
<div class="status-bar">
|
|
@@ -215,11 +224,15 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
215
224
|
</div>
|
|
216
225
|
|
|
217
226
|
<script>
|
|
218
|
-
let topology = { version: '1.0', cameras: [], connections: [], globalZones: [], floorPlan: null };
|
|
227
|
+
let topology = { version: '1.0', cameras: [], connections: [], globalZones: [], floorPlan: null, drawings: [] };
|
|
219
228
|
let selectedItem = null;
|
|
220
229
|
let currentTool = 'select';
|
|
221
230
|
let floorPlanImage = null;
|
|
222
231
|
let availableCameras = [];
|
|
232
|
+
let isDrawing = false;
|
|
233
|
+
let drawStart = null;
|
|
234
|
+
let currentDrawing = null;
|
|
235
|
+
let blankCanvasMode = false;
|
|
223
236
|
const canvas = document.getElementById('floor-plan-canvas');
|
|
224
237
|
const ctx = canvas.getContext('2d');
|
|
225
238
|
|
|
@@ -236,8 +249,11 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
236
249
|
const response = await fetch('../api/topology');
|
|
237
250
|
if (response.ok) {
|
|
238
251
|
topology = await response.json();
|
|
252
|
+
if (!topology.drawings) topology.drawings = [];
|
|
239
253
|
if (topology.floorPlan?.imageData) {
|
|
240
254
|
await loadFloorPlanImage(topology.floorPlan.imageData);
|
|
255
|
+
} else if (topology.floorPlan?.type === 'blank') {
|
|
256
|
+
blankCanvasMode = true;
|
|
241
257
|
}
|
|
242
258
|
}
|
|
243
259
|
} catch (e) { console.error('Failed to load topology:', e); }
|
|
@@ -269,7 +285,22 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
269
285
|
|
|
270
286
|
function render() {
|
|
271
287
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
272
|
-
|
|
288
|
+
|
|
289
|
+
// Draw grid for blank canvas
|
|
290
|
+
if (blankCanvasMode && !floorPlanImage) {
|
|
291
|
+
document.getElementById('canvas-placeholder').style.display = 'none';
|
|
292
|
+
ctx.fillStyle = '#1a1a2e';
|
|
293
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
294
|
+
ctx.strokeStyle = '#2a2a4e';
|
|
295
|
+
ctx.lineWidth = 1;
|
|
296
|
+
const gridSize = 40;
|
|
297
|
+
for (let x = 0; x < canvas.width; x += gridSize) {
|
|
298
|
+
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
|
|
299
|
+
}
|
|
300
|
+
for (let y = 0; y < canvas.height; y += gridSize) {
|
|
301
|
+
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
|
|
302
|
+
}
|
|
303
|
+
} else if (floorPlanImage) {
|
|
273
304
|
document.getElementById('canvas-placeholder').style.display = 'none';
|
|
274
305
|
const scale = Math.min(canvas.width / floorPlanImage.width, canvas.height / floorPlanImage.height) * 0.9;
|
|
275
306
|
const x = (canvas.width - floorPlanImage.width * scale) / 2;
|
|
@@ -278,6 +309,48 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
278
309
|
} else {
|
|
279
310
|
document.getElementById('canvas-placeholder').style.display = 'block';
|
|
280
311
|
}
|
|
312
|
+
|
|
313
|
+
// Draw saved drawings (walls and rooms)
|
|
314
|
+
if (topology.drawings) {
|
|
315
|
+
for (const drawing of topology.drawings) {
|
|
316
|
+
if (drawing.type === 'wall') {
|
|
317
|
+
ctx.beginPath();
|
|
318
|
+
ctx.moveTo(drawing.x1, drawing.y1);
|
|
319
|
+
ctx.lineTo(drawing.x2, drawing.y2);
|
|
320
|
+
ctx.strokeStyle = '#888';
|
|
321
|
+
ctx.lineWidth = 4;
|
|
322
|
+
ctx.stroke();
|
|
323
|
+
} else if (drawing.type === 'room') {
|
|
324
|
+
ctx.strokeStyle = '#666';
|
|
325
|
+
ctx.lineWidth = 2;
|
|
326
|
+
ctx.strokeRect(drawing.x, drawing.y, drawing.width, drawing.height);
|
|
327
|
+
ctx.fillStyle = 'rgba(100, 100, 150, 0.1)';
|
|
328
|
+
ctx.fillRect(drawing.x, drawing.y, drawing.width, drawing.height);
|
|
329
|
+
if (drawing.label) {
|
|
330
|
+
ctx.fillStyle = '#888';
|
|
331
|
+
ctx.font = '12px sans-serif';
|
|
332
|
+
ctx.textAlign = 'center';
|
|
333
|
+
ctx.fillText(drawing.label, drawing.x + drawing.width/2, drawing.y + drawing.height/2);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Draw current drawing in progress
|
|
340
|
+
if (currentDrawing) {
|
|
341
|
+
if (currentDrawing.type === 'wall') {
|
|
342
|
+
ctx.beginPath();
|
|
343
|
+
ctx.moveTo(currentDrawing.x1, currentDrawing.y1);
|
|
344
|
+
ctx.lineTo(currentDrawing.x2, currentDrawing.y2);
|
|
345
|
+
ctx.strokeStyle = '#e94560';
|
|
346
|
+
ctx.lineWidth = 4;
|
|
347
|
+
ctx.stroke();
|
|
348
|
+
} else if (currentDrawing.type === 'room') {
|
|
349
|
+
ctx.strokeStyle = '#e94560';
|
|
350
|
+
ctx.lineWidth = 2;
|
|
351
|
+
ctx.strokeRect(currentDrawing.x, currentDrawing.y, currentDrawing.width, currentDrawing.height);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
281
354
|
for (const conn of topology.connections) {
|
|
282
355
|
const fromCam = topology.cameras.find(c => c.deviceId === conn.fromCameraId);
|
|
283
356
|
const toCam = topology.cameras.find(c => c.deviceId === conn.toCameraId);
|
|
@@ -361,6 +434,9 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
361
434
|
const name = document.getElementById('camera-name-input').value || 'New Camera';
|
|
362
435
|
const isEntry = document.getElementById('camera-entry-checkbox').checked;
|
|
363
436
|
const isExit = document.getElementById('camera-exit-checkbox').checked;
|
|
437
|
+
// Use pending position from click, or default to center
|
|
438
|
+
const pos = topology._pendingCameraPos || { x: canvas.width / 2 + Math.random() * 100 - 50, y: canvas.height / 2 + Math.random() * 100 - 50 };
|
|
439
|
+
delete topology._pendingCameraPos;
|
|
364
440
|
const camera = {
|
|
365
441
|
deviceId: deviceId || 'camera-' + Date.now(),
|
|
366
442
|
nativeId: 'cam-' + Date.now(),
|
|
@@ -368,7 +444,7 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
368
444
|
isEntryPoint: isEntry,
|
|
369
445
|
isExitPoint: isExit,
|
|
370
446
|
trackClasses: ['person', 'car', 'animal'],
|
|
371
|
-
floorPlanPosition:
|
|
447
|
+
floorPlanPosition: pos
|
|
372
448
|
};
|
|
373
449
|
topology.cameras.push(camera);
|
|
374
450
|
closeModal('add-camera-modal');
|
|
@@ -464,47 +540,115 @@ export const EDITOR_HTML = `<!DOCTYPE html>
|
|
|
464
540
|
function updateConnectionBidi(id, value) { const conn = topology.connections.find(c => c.id === id); if (conn) conn.bidirectional = value; render(); }
|
|
465
541
|
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(); }
|
|
466
542
|
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(); }
|
|
467
|
-
function setTool(tool) {
|
|
543
|
+
function setTool(tool) {
|
|
544
|
+
currentTool = tool;
|
|
545
|
+
setStatus('Tool: ' + tool, 'success');
|
|
546
|
+
document.querySelectorAll('.toolbar .btn').forEach(b => b.style.background = '');
|
|
547
|
+
const btn = document.getElementById('tool-' + tool);
|
|
548
|
+
if (btn) btn.style.background = '#e94560';
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function useBlankCanvas() {
|
|
552
|
+
blankCanvasMode = true;
|
|
553
|
+
floorPlanImage = null;
|
|
554
|
+
topology.floorPlan = { type: 'blank', width: canvas.width, height: canvas.height };
|
|
555
|
+
render();
|
|
556
|
+
setStatus('Blank canvas ready - use Draw Wall or Draw Room tools', 'success');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function clearDrawings() {
|
|
560
|
+
if (!confirm('Clear all drawings (walls and rooms)?')) return;
|
|
561
|
+
topology.drawings = [];
|
|
562
|
+
render();
|
|
563
|
+
setStatus('Drawings cleared', 'success');
|
|
564
|
+
}
|
|
565
|
+
|
|
468
566
|
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
|
|
469
567
|
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'); }
|
|
470
568
|
|
|
569
|
+
let dragging = null;
|
|
570
|
+
|
|
471
571
|
canvas.addEventListener('mousedown', (e) => {
|
|
472
572
|
const rect = canvas.getBoundingClientRect();
|
|
473
573
|
const x = e.clientX - rect.left;
|
|
474
574
|
const y = e.clientY - rect.top;
|
|
575
|
+
|
|
475
576
|
if (currentTool === 'select') {
|
|
476
577
|
for (const camera of topology.cameras) {
|
|
477
578
|
if (camera.floorPlanPosition) {
|
|
478
579
|
const dist = Math.hypot(x - camera.floorPlanPosition.x, y - camera.floorPlanPosition.y);
|
|
479
|
-
if (dist < 25) { selectCamera(camera.deviceId); return; }
|
|
580
|
+
if (dist < 25) { selectCamera(camera.deviceId); dragging = camera; return; }
|
|
480
581
|
}
|
|
481
582
|
}
|
|
583
|
+
} else if (currentTool === 'wall') {
|
|
584
|
+
isDrawing = true;
|
|
585
|
+
drawStart = { x, y };
|
|
586
|
+
currentDrawing = { type: 'wall', x1: x, y1: y, x2: x, y2: y };
|
|
587
|
+
} else if (currentTool === 'room') {
|
|
588
|
+
isDrawing = true;
|
|
589
|
+
drawStart = { x, y };
|
|
590
|
+
currentDrawing = { type: 'room', x: x, y: y, width: 0, height: 0 };
|
|
591
|
+
} else if (currentTool === 'camera') {
|
|
592
|
+
openAddCameraModal();
|
|
593
|
+
// Will position camera at click location after adding
|
|
594
|
+
topology._pendingCameraPos = { x, y };
|
|
482
595
|
}
|
|
483
596
|
});
|
|
484
597
|
|
|
485
|
-
|
|
486
|
-
canvas.addEventListener('mousedown', (e) => {
|
|
487
|
-
if (currentTool !== 'select') return;
|
|
598
|
+
canvas.addEventListener('mousemove', (e) => {
|
|
488
599
|
const rect = canvas.getBoundingClientRect();
|
|
489
600
|
const x = e.clientX - rect.left;
|
|
490
601
|
const y = e.clientY - rect.top;
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
602
|
+
|
|
603
|
+
if (dragging) {
|
|
604
|
+
dragging.floorPlanPosition.x = x;
|
|
605
|
+
dragging.floorPlanPosition.y = y;
|
|
606
|
+
render();
|
|
607
|
+
} else if (isDrawing && currentDrawing) {
|
|
608
|
+
if (currentDrawing.type === 'wall') {
|
|
609
|
+
currentDrawing.x2 = x;
|
|
610
|
+
currentDrawing.y2 = y;
|
|
611
|
+
} else if (currentDrawing.type === 'room') {
|
|
612
|
+
currentDrawing.width = x - drawStart.x;
|
|
613
|
+
currentDrawing.height = y - drawStart.y;
|
|
495
614
|
}
|
|
615
|
+
render();
|
|
496
616
|
}
|
|
497
617
|
});
|
|
498
618
|
|
|
499
|
-
canvas.addEventListener('
|
|
500
|
-
if (
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
619
|
+
canvas.addEventListener('mouseup', (e) => {
|
|
620
|
+
if (isDrawing && currentDrawing) {
|
|
621
|
+
if (!topology.drawings) topology.drawings = [];
|
|
622
|
+
// Normalize room coordinates if drawn backwards
|
|
623
|
+
if (currentDrawing.type === 'room') {
|
|
624
|
+
if (currentDrawing.width < 0) {
|
|
625
|
+
currentDrawing.x += currentDrawing.width;
|
|
626
|
+
currentDrawing.width = Math.abs(currentDrawing.width);
|
|
627
|
+
}
|
|
628
|
+
if (currentDrawing.height < 0) {
|
|
629
|
+
currentDrawing.y += currentDrawing.height;
|
|
630
|
+
currentDrawing.height = Math.abs(currentDrawing.height);
|
|
631
|
+
}
|
|
632
|
+
// Only add if room is big enough
|
|
633
|
+
if (currentDrawing.width > 20 && currentDrawing.height > 20) {
|
|
634
|
+
const label = prompt('Room name (optional):');
|
|
635
|
+
if (label) currentDrawing.label = label;
|
|
636
|
+
topology.drawings.push(currentDrawing);
|
|
637
|
+
}
|
|
638
|
+
} else if (currentDrawing.type === 'wall') {
|
|
639
|
+
// Only add if wall is long enough
|
|
640
|
+
const len = Math.hypot(currentDrawing.x2 - currentDrawing.x1, currentDrawing.y2 - currentDrawing.y1);
|
|
641
|
+
if (len > 20) {
|
|
642
|
+
topology.drawings.push(currentDrawing);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
isDrawing = false;
|
|
646
|
+
currentDrawing = null;
|
|
647
|
+
render();
|
|
648
|
+
}
|
|
649
|
+
dragging = null;
|
|
505
650
|
});
|
|
506
651
|
|
|
507
|
-
canvas.addEventListener('mouseup', () => { dragging = null; });
|
|
508
652
|
window.addEventListener('resize', () => { resizeCanvas(); render(); });
|
|
509
653
|
init();
|
|
510
654
|
</script>
|