@nyaruka/temba-components 0.138.4 → 0.139.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/CHANGELOG.md +15 -0
- package/dist/locales/es.js +5 -5
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +5 -5
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/locale-codes.js +2 -11
- package/dist/locales/locale-codes.js.map +1 -1
- package/dist/locales/pt.js +5 -5
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +816 -852
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +23 -30
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +5 -3
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +6 -7
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +152 -235
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +757 -403
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/utils.js +138 -66
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/list/TicketList.js +4 -1
- package/out-tsc/src/list/TicketList.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +18 -1
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/locales/es.js +5 -5
- package/out-tsc/src/locales/es.js.map +1 -1
- package/out-tsc/src/locales/fr.js +5 -5
- package/out-tsc/src/locales/fr.js.map +1 -1
- package/out-tsc/src/locales/locale-codes.js +2 -11
- package/out-tsc/src/locales/locale-codes.js.map +1 -1
- package/out-tsc/src/locales/pt.js +5 -5
- package/out-tsc/src/locales/pt.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +1 -0
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/test/temba-floating-tab.test.js +4 -6
- package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
- package/out-tsc/test/temba-flow-collision.test.js +221 -223
- package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor.test.js +0 -2
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +102 -93
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/package.json +1 -1
- package/src/display/FloatingTab.ts +22 -31
- package/src/flow/CanvasMenu.ts +8 -3
- package/src/flow/CanvasNode.ts +6 -7
- package/src/flow/Editor.ts +184 -279
- package/src/flow/Plumber.ts +1011 -457
- package/src/flow/utils.ts +162 -84
- package/src/interfaces.ts +2 -1
- package/src/list/TicketList.ts +4 -1
- package/src/live/ContactChat.ts +19 -1
- package/src/locales/es.ts +13 -18
- package/src/locales/fr.ts +13 -18
- package/src/locales/locale-codes.ts +2 -11
- package/src/locales/pt.ts +13 -18
- package/src/simulator/Simulator.ts +1 -0
- package/test/temba-floating-tab.test.ts +4 -6
- package/test/temba-flow-collision.test.ts +225 -303
- package/test/temba-flow-editor.test.ts +0 -2
- package/test/temba-flow-plumber-connections.test.ts +97 -97
- package/test/temba-flow-plumber.test.ts +116 -103
|
@@ -265,17 +265,16 @@ describe('Collision Detection Utilities', () => {
|
|
|
265
265
|
});
|
|
266
266
|
describe('calculateReflowPositions', () => {
|
|
267
267
|
it('returns empty map when no collisions', () => {
|
|
268
|
-
const movedBounds = {
|
|
269
|
-
uuid: 'moved',
|
|
270
|
-
left: 100,
|
|
271
|
-
top: 100,
|
|
272
|
-
right: 200,
|
|
273
|
-
bottom: 200,
|
|
274
|
-
width: 100,
|
|
275
|
-
height: 100
|
|
276
|
-
};
|
|
277
268
|
const allBounds = [
|
|
278
|
-
|
|
269
|
+
{
|
|
270
|
+
uuid: 'moved',
|
|
271
|
+
left: 100,
|
|
272
|
+
top: 100,
|
|
273
|
+
right: 200,
|
|
274
|
+
bottom: 200,
|
|
275
|
+
width: 100,
|
|
276
|
+
height: 100
|
|
277
|
+
},
|
|
279
278
|
{
|
|
280
279
|
uuid: 'node1',
|
|
281
280
|
left: 300,
|
|
@@ -286,82 +285,46 @@ describe('Collision Detection Utilities', () => {
|
|
|
286
285
|
height: 100
|
|
287
286
|
}
|
|
288
287
|
];
|
|
289
|
-
const positions = calculateReflowPositions('moved',
|
|
288
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
290
289
|
expect(positions.size).to.equal(0);
|
|
291
290
|
});
|
|
292
|
-
it('moves colliding node
|
|
293
|
-
const movedBounds = {
|
|
294
|
-
uuid: 'moved',
|
|
295
|
-
left: 100,
|
|
296
|
-
top: 100,
|
|
297
|
-
right: 200,
|
|
298
|
-
bottom: 200,
|
|
299
|
-
width: 100,
|
|
300
|
-
height: 100
|
|
301
|
-
};
|
|
291
|
+
it('moves colliding node out of the way', () => {
|
|
302
292
|
const allBounds = [
|
|
303
|
-
|
|
293
|
+
{
|
|
294
|
+
uuid: 'moved',
|
|
295
|
+
left: 100,
|
|
296
|
+
top: 100,
|
|
297
|
+
right: 200,
|
|
298
|
+
bottom: 200,
|
|
299
|
+
width: 100,
|
|
300
|
+
height: 100
|
|
301
|
+
},
|
|
304
302
|
{
|
|
305
303
|
uuid: 'node1',
|
|
306
|
-
left:
|
|
304
|
+
left: 100,
|
|
307
305
|
top: 150,
|
|
308
|
-
right:
|
|
306
|
+
right: 200,
|
|
309
307
|
bottom: 250,
|
|
310
308
|
width: 100,
|
|
311
309
|
height: 100
|
|
312
310
|
}
|
|
313
311
|
];
|
|
314
|
-
const positions = calculateReflowPositions('moved',
|
|
312
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
315
313
|
expect(positions.size).to.equal(1);
|
|
316
314
|
expect(positions.has('node1')).to.be.true;
|
|
317
|
-
|
|
318
|
-
expect(newPos.left).to.equal(150); // left unchanged
|
|
319
|
-
expect(newPos.top).to.be.greaterThan(200); // moved below the moved node
|
|
315
|
+
expect(positions.has('moved')).to.be.false;
|
|
320
316
|
});
|
|
321
|
-
it('
|
|
322
|
-
const movedBounds = {
|
|
323
|
-
uuid: 'moved',
|
|
324
|
-
left: 100,
|
|
325
|
-
top: 150,
|
|
326
|
-
right: 200,
|
|
327
|
-
bottom: 250,
|
|
328
|
-
width: 100,
|
|
329
|
-
height: 100
|
|
330
|
-
};
|
|
317
|
+
it('sacred node never appears in returned positions', () => {
|
|
331
318
|
const allBounds = [
|
|
332
|
-
movedBounds,
|
|
333
319
|
{
|
|
334
|
-
uuid: '
|
|
320
|
+
uuid: 'dropped',
|
|
335
321
|
left: 100,
|
|
336
322
|
top: 100,
|
|
337
323
|
right: 200,
|
|
338
324
|
bottom: 200,
|
|
339
325
|
width: 100,
|
|
340
326
|
height: 100
|
|
341
|
-
}
|
|
342
|
-
];
|
|
343
|
-
const positions = calculateReflowPositions('moved', movedBounds, allBounds, true // droppedBelowMidpoint
|
|
344
|
-
);
|
|
345
|
-
expect(positions.size).to.equal(1);
|
|
346
|
-
expect(positions.has('moved')).to.be.true;
|
|
347
|
-
const newPos = positions.get('moved');
|
|
348
|
-
expect(newPos.top).to.be.greaterThan(200); // moved below existing node
|
|
349
|
-
});
|
|
350
|
-
it('gives priority to dropped node when dropped above midpoint', () => {
|
|
351
|
-
// Dropped node overlaps with bottom of existing node
|
|
352
|
-
// Bottom of dropped (180) is above midpoint of existing (150)
|
|
353
|
-
// So dropped node should keep position, existing moves down
|
|
354
|
-
const movedBounds = {
|
|
355
|
-
uuid: 'dropped',
|
|
356
|
-
left: 100,
|
|
357
|
-
top: 80,
|
|
358
|
-
right: 200,
|
|
359
|
-
bottom: 180,
|
|
360
|
-
width: 100,
|
|
361
|
-
height: 100
|
|
362
|
-
};
|
|
363
|
-
const allBounds = [
|
|
364
|
-
movedBounds,
|
|
327
|
+
},
|
|
365
328
|
{
|
|
366
329
|
uuid: 'existing',
|
|
367
330
|
left: 100,
|
|
@@ -372,106 +335,79 @@ describe('Collision Detection Utilities', () => {
|
|
|
372
335
|
height: 100
|
|
373
336
|
}
|
|
374
337
|
];
|
|
375
|
-
const positions = calculateReflowPositions('dropped',
|
|
376
|
-
);
|
|
377
|
-
// Existing node should be moved down
|
|
338
|
+
const positions = calculateReflowPositions(['dropped'], allBounds);
|
|
339
|
+
expect(positions.has('dropped')).to.be.false;
|
|
378
340
|
expect(positions.has('existing')).to.be.true;
|
|
379
|
-
expect(positions.has('dropped')).to.be.false; // dropped keeps its position
|
|
380
|
-
const existingNewPos = positions.get('existing');
|
|
381
|
-
expect(existingNewPos.top).to.be.greaterThan(180); // moved below dropped node
|
|
382
341
|
});
|
|
383
|
-
it('
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
// So existing node keeps position, dropped moves down
|
|
387
|
-
const movedBounds = {
|
|
388
|
-
uuid: 'dropped',
|
|
389
|
-
left: 100,
|
|
390
|
-
top: 120,
|
|
391
|
-
right: 200,
|
|
392
|
-
bottom: 220,
|
|
393
|
-
width: 100,
|
|
394
|
-
height: 100
|
|
395
|
-
};
|
|
342
|
+
it('prefers least-displacement direction', () => {
|
|
343
|
+
// Sacred at (100,100)-(200,200), collider at (180,100)-(280,200)
|
|
344
|
+
// Right requires only 60px displacement, down requires 140px
|
|
396
345
|
const allBounds = [
|
|
397
|
-
movedBounds,
|
|
398
346
|
{
|
|
399
|
-
uuid: '
|
|
347
|
+
uuid: 'sacred',
|
|
400
348
|
left: 100,
|
|
401
349
|
top: 100,
|
|
402
350
|
right: 200,
|
|
403
351
|
bottom: 200,
|
|
404
352
|
width: 100,
|
|
405
353
|
height: 100
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
uuid: 'collider',
|
|
357
|
+
left: 180,
|
|
358
|
+
top: 100,
|
|
359
|
+
right: 280,
|
|
360
|
+
bottom: 200,
|
|
361
|
+
width: 100,
|
|
362
|
+
height: 100
|
|
406
363
|
}
|
|
407
364
|
];
|
|
408
|
-
const positions = calculateReflowPositions('
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
expect(
|
|
413
|
-
|
|
414
|
-
expect(droppedNewPos.top).to.be.greaterThan(200); // moved below existing node
|
|
365
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
366
|
+
expect(positions.has('collider')).to.be.true;
|
|
367
|
+
const newPos = positions.get('collider');
|
|
368
|
+
// Should move right (shorter) rather than down (longer)
|
|
369
|
+
expect(newPos.left).to.be.greaterThan(200);
|
|
370
|
+
expect(newPos.top).to.equal(100); // vertical position unchanged
|
|
415
371
|
});
|
|
416
|
-
it('
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
right: 200,
|
|
422
|
-
bottom: 200,
|
|
423
|
-
width: 100,
|
|
424
|
-
height: 100
|
|
425
|
-
};
|
|
372
|
+
it('prefers up when it is the shortest move', () => {
|
|
373
|
+
// Sacred at (100,200)-(200,300), collider at (100,180)-(200,280)
|
|
374
|
+
// Up: newTop=snapToGrid(200-100-30)=snapToGrid(70)=80, distance=100
|
|
375
|
+
// Down: newTop=snapToGrid(300+30)=340, distance=160
|
|
376
|
+
// Right: newLeft=snapToGrid(200+30)=240, distance=140
|
|
426
377
|
const allBounds = [
|
|
427
|
-
movedBounds,
|
|
428
378
|
{
|
|
429
|
-
uuid: '
|
|
379
|
+
uuid: 'sacred',
|
|
430
380
|
left: 100,
|
|
431
|
-
top:
|
|
381
|
+
top: 200,
|
|
432
382
|
right: 200,
|
|
433
|
-
bottom:
|
|
383
|
+
bottom: 300,
|
|
434
384
|
width: 100,
|
|
435
385
|
height: 100
|
|
436
386
|
},
|
|
437
387
|
{
|
|
438
|
-
uuid: '
|
|
388
|
+
uuid: 'collider',
|
|
439
389
|
left: 100,
|
|
440
|
-
top:
|
|
390
|
+
top: 180,
|
|
441
391
|
right: 200,
|
|
442
|
-
bottom:
|
|
392
|
+
bottom: 280,
|
|
443
393
|
width: 100,
|
|
444
394
|
height: 100
|
|
445
395
|
}
|
|
446
396
|
];
|
|
447
|
-
const positions = calculateReflowPositions('
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const node2Pos = positions.get('node2');
|
|
454
|
-
// node2 should be below node1
|
|
455
|
-
expect(node2Pos.top).to.be.greaterThan(node1Pos.top);
|
|
456
|
-
}
|
|
397
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
398
|
+
expect(positions.has('collider')).to.be.true;
|
|
399
|
+
const newPos = positions.get('collider');
|
|
400
|
+
// Should move up (shortest displacement)
|
|
401
|
+
expect(newPos.top).to.be.lessThan(200);
|
|
402
|
+
expect(newPos.left).to.equal(100); // horizontal position unchanged
|
|
457
403
|
});
|
|
458
|
-
it('
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
const movedBounds = {
|
|
463
|
-
uuid: 'moved',
|
|
464
|
-
left: 100,
|
|
465
|
-
top: 50,
|
|
466
|
-
right: 200,
|
|
467
|
-
bottom: 150,
|
|
468
|
-
width: 100,
|
|
469
|
-
height: 100
|
|
470
|
-
};
|
|
404
|
+
it('prefers direction with fewer cascading collisions', () => {
|
|
405
|
+
// Sacred at (100,100)-(200,200), collider at (100,150)-(200,250)
|
|
406
|
+
// A node sits below at (100,280)-(200,380) blocking the downward path
|
|
407
|
+
// Down causes cascade, right does not
|
|
471
408
|
const allBounds = [
|
|
472
|
-
movedBounds,
|
|
473
409
|
{
|
|
474
|
-
uuid: '
|
|
410
|
+
uuid: 'sacred',
|
|
475
411
|
left: 100,
|
|
476
412
|
top: 100,
|
|
477
413
|
right: 200,
|
|
@@ -480,52 +416,41 @@ describe('Collision Detection Utilities', () => {
|
|
|
480
416
|
height: 100
|
|
481
417
|
},
|
|
482
418
|
{
|
|
483
|
-
uuid: '
|
|
419
|
+
uuid: 'collider',
|
|
484
420
|
left: 100,
|
|
485
|
-
top:
|
|
421
|
+
top: 150,
|
|
486
422
|
right: 200,
|
|
487
|
-
bottom:
|
|
423
|
+
bottom: 250,
|
|
488
424
|
width: 100,
|
|
489
425
|
height: 100
|
|
490
426
|
},
|
|
491
427
|
{
|
|
492
|
-
uuid: '
|
|
428
|
+
uuid: 'blocker',
|
|
493
429
|
left: 100,
|
|
494
|
-
top:
|
|
430
|
+
top: 280,
|
|
495
431
|
right: 200,
|
|
496
|
-
bottom:
|
|
432
|
+
bottom: 380,
|
|
497
433
|
width: 100,
|
|
498
434
|
height: 100
|
|
499
435
|
}
|
|
500
436
|
];
|
|
501
|
-
const positions = calculateReflowPositions('
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
expect(positions.has('
|
|
506
|
-
expect(positions.has('node3')).to.be.true;
|
|
507
|
-
const node1Pos = positions.get('node1');
|
|
508
|
-
const node2Pos = positions.get('node2');
|
|
509
|
-
const node3Pos = positions.get('node3');
|
|
510
|
-
// Nodes should be stacked vertically with proper spacing
|
|
511
|
-
expect(node1Pos.top).to.be.at.least(170); // 150 (bottom of moved) + 20
|
|
512
|
-
expect(node2Pos.top).to.be.at.least(node1Pos.top + 120); // node1 top + height + spacing
|
|
513
|
-
expect(node3Pos.top).to.be.at.least(node2Pos.top + 120); // node2 top + height + spacing
|
|
437
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
438
|
+
expect(positions.has('collider')).to.be.true;
|
|
439
|
+
// Should avoid moving down (would cascade into blocker)
|
|
440
|
+
// blocker should not need to move
|
|
441
|
+
expect(positions.has('blocker')).to.be.false;
|
|
514
442
|
});
|
|
515
|
-
it('
|
|
516
|
-
// This test creates a scenario where node3 doesn't collide with the moved node,
|
|
517
|
-
// but collides with node2 which itself got repositioned due to collision with node1
|
|
518
|
-
const movedBounds = {
|
|
519
|
-
uuid: 'moved',
|
|
520
|
-
left: 100,
|
|
521
|
-
top: 100,
|
|
522
|
-
right: 200,
|
|
523
|
-
bottom: 200,
|
|
524
|
-
width: 100,
|
|
525
|
-
height: 100
|
|
526
|
-
};
|
|
443
|
+
it('resolves cascading collisions', () => {
|
|
527
444
|
const allBounds = [
|
|
528
|
-
|
|
445
|
+
{
|
|
446
|
+
uuid: 'moved',
|
|
447
|
+
left: 100,
|
|
448
|
+
top: 100,
|
|
449
|
+
right: 200,
|
|
450
|
+
bottom: 200,
|
|
451
|
+
width: 100,
|
|
452
|
+
height: 100
|
|
453
|
+
},
|
|
529
454
|
{
|
|
530
455
|
uuid: 'node1',
|
|
531
456
|
left: 100,
|
|
@@ -538,67 +463,118 @@ describe('Collision Detection Utilities', () => {
|
|
|
538
463
|
{
|
|
539
464
|
uuid: 'node2',
|
|
540
465
|
left: 100,
|
|
541
|
-
top:
|
|
466
|
+
top: 200,
|
|
542
467
|
right: 200,
|
|
543
|
-
bottom:
|
|
468
|
+
bottom: 300,
|
|
544
469
|
width: 100,
|
|
545
470
|
height: 100
|
|
546
471
|
}
|
|
547
472
|
];
|
|
548
|
-
const positions = calculateReflowPositions('moved',
|
|
549
|
-
//
|
|
550
|
-
expect(positions.size).to.
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const node1Pos = positions.get('node1');
|
|
554
|
-
const node2Pos = positions.get('node2');
|
|
555
|
-
// node1 should be moved below moved node
|
|
556
|
-
expect(node1Pos.top).to.be.at.least(220); // 200 + 20
|
|
557
|
-
// node2 should be moved below the new position of node1
|
|
558
|
-
// This tests the code path where we check against already repositioned nodes
|
|
559
|
-
expect(node2Pos.top).to.be.at.least(node1Pos.top + 120);
|
|
473
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
474
|
+
// At least one node should be repositioned
|
|
475
|
+
expect(positions.size).to.be.greaterThan(0);
|
|
476
|
+
// No node should overlap with the sacred node or each other after reflow
|
|
477
|
+
// (verified by the algorithm's correctness guarantee)
|
|
560
478
|
});
|
|
561
|
-
it('
|
|
562
|
-
|
|
563
|
-
uuid: 'moved',
|
|
564
|
-
left: 100,
|
|
565
|
-
top: 100,
|
|
566
|
-
right: 200,
|
|
567
|
-
bottom: 200,
|
|
568
|
-
width: 100,
|
|
569
|
-
height: 100
|
|
570
|
-
};
|
|
479
|
+
it('handles multiple sacred nodes', () => {
|
|
480
|
+
// Two sacred nodes with a non-sacred node overlapping one
|
|
571
481
|
const allBounds = [
|
|
572
|
-
movedBounds,
|
|
573
482
|
{
|
|
574
|
-
uuid: '
|
|
575
|
-
left:
|
|
483
|
+
uuid: 'sacred1',
|
|
484
|
+
left: 100,
|
|
485
|
+
top: 100,
|
|
486
|
+
right: 200,
|
|
487
|
+
bottom: 200,
|
|
488
|
+
width: 100,
|
|
489
|
+
height: 100
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
uuid: 'sacred2',
|
|
493
|
+
left: 100,
|
|
494
|
+
top: 400,
|
|
495
|
+
right: 200,
|
|
496
|
+
bottom: 500,
|
|
497
|
+
width: 100,
|
|
498
|
+
height: 100
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
uuid: 'collider',
|
|
502
|
+
left: 100,
|
|
576
503
|
top: 150,
|
|
577
|
-
right:
|
|
504
|
+
right: 200,
|
|
578
505
|
bottom: 250,
|
|
579
506
|
width: 100,
|
|
580
507
|
height: 100
|
|
581
508
|
}
|
|
582
509
|
];
|
|
583
|
-
const positions = calculateReflowPositions('
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
expect(
|
|
510
|
+
const positions = calculateReflowPositions(['sacred1', 'sacred2'], allBounds);
|
|
511
|
+
// Sacred nodes should never be moved
|
|
512
|
+
expect(positions.has('sacred1')).to.be.false;
|
|
513
|
+
expect(positions.has('sacred2')).to.be.false;
|
|
514
|
+
// Collider should be moved
|
|
515
|
+
expect(positions.has('collider')).to.be.true;
|
|
587
516
|
});
|
|
588
|
-
it('
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
517
|
+
it('does not move a node into another sacred node', () => {
|
|
518
|
+
// Two sacred nodes close together with a collider between them
|
|
519
|
+
// Moving right would overlap sacred2, so it should choose another direction
|
|
520
|
+
const allBounds = [
|
|
521
|
+
{
|
|
522
|
+
uuid: 'sacred1',
|
|
523
|
+
left: 100,
|
|
524
|
+
top: 100,
|
|
525
|
+
right: 200,
|
|
526
|
+
bottom: 200,
|
|
527
|
+
width: 100,
|
|
528
|
+
height: 100
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
uuid: 'sacred2',
|
|
532
|
+
left: 240,
|
|
533
|
+
top: 100,
|
|
534
|
+
right: 340,
|
|
535
|
+
bottom: 200,
|
|
536
|
+
width: 100,
|
|
537
|
+
height: 100
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
uuid: 'collider',
|
|
541
|
+
left: 150,
|
|
542
|
+
top: 100,
|
|
543
|
+
right: 250,
|
|
544
|
+
bottom: 200,
|
|
545
|
+
width: 100,
|
|
546
|
+
height: 100
|
|
547
|
+
}
|
|
548
|
+
];
|
|
549
|
+
const positions = calculateReflowPositions(['sacred1', 'sacred2'], allBounds);
|
|
550
|
+
expect(positions.has('collider')).to.be.true;
|
|
551
|
+
const newPos = positions.get('collider');
|
|
552
|
+
// Should not overlap either sacred node after reflow
|
|
553
|
+
const newBounds = {
|
|
554
|
+
uuid: 'collider',
|
|
555
|
+
left: newPos.left,
|
|
556
|
+
top: newPos.top,
|
|
557
|
+
right: newPos.left + 100,
|
|
558
|
+
bottom: newPos.top + 100,
|
|
595
559
|
width: 100,
|
|
596
560
|
height: 100
|
|
597
561
|
};
|
|
562
|
+
expect(nodesOverlap(newBounds, allBounds[0])).to.be.false;
|
|
563
|
+
expect(nodesOverlap(newBounds, allBounds[1])).to.be.false;
|
|
564
|
+
});
|
|
565
|
+
it('snaps reflow positions to grid', () => {
|
|
598
566
|
const allBounds = [
|
|
599
|
-
movedBounds,
|
|
600
567
|
{
|
|
601
|
-
uuid: '
|
|
568
|
+
uuid: 'sacred',
|
|
569
|
+
left: 100,
|
|
570
|
+
top: 100,
|
|
571
|
+
right: 200,
|
|
572
|
+
bottom: 200,
|
|
573
|
+
width: 100,
|
|
574
|
+
height: 100
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
uuid: 'collider',
|
|
602
578
|
left: 100,
|
|
603
579
|
top: 150,
|
|
604
580
|
right: 200,
|
|
@@ -607,24 +583,46 @@ describe('Collision Detection Utilities', () => {
|
|
|
607
583
|
height: 100
|
|
608
584
|
}
|
|
609
585
|
];
|
|
610
|
-
const positions = calculateReflowPositions('
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
586
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
587
|
+
expect(positions.has('collider')).to.be.true;
|
|
588
|
+
const newPos = positions.get('collider');
|
|
589
|
+
// Both coordinates should be multiples of 20 (grid size)
|
|
590
|
+
expect(newPos.left % 20).to.equal(0);
|
|
591
|
+
expect(newPos.top % 20).to.equal(0);
|
|
592
|
+
});
|
|
593
|
+
it('clamps positions to zero (no negative coordinates)', () => {
|
|
594
|
+
// Sacred node near top-left, collider above it
|
|
595
|
+
// Moving up would go negative, so it should fall back
|
|
596
|
+
const allBounds = [
|
|
597
|
+
{
|
|
598
|
+
uuid: 'sacred',
|
|
599
|
+
left: 0,
|
|
600
|
+
top: 0,
|
|
601
|
+
right: 200,
|
|
602
|
+
bottom: 200,
|
|
603
|
+
width: 200,
|
|
604
|
+
height: 200
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
uuid: 'collider',
|
|
608
|
+
left: 0,
|
|
609
|
+
top: 100,
|
|
610
|
+
right: 200,
|
|
611
|
+
bottom: 200,
|
|
612
|
+
width: 200,
|
|
613
|
+
height: 100
|
|
614
|
+
}
|
|
615
|
+
];
|
|
616
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
617
|
+
expect(positions.has('collider')).to.be.true;
|
|
618
|
+
const newPos = positions.get('collider');
|
|
619
|
+
expect(newPos.left).to.be.at.least(0);
|
|
620
|
+
expect(newPos.top).to.be.at.least(0);
|
|
614
621
|
});
|
|
615
622
|
});
|
|
616
623
|
describe('edge cases', () => {
|
|
617
624
|
it('handles empty allBounds array', () => {
|
|
618
|
-
const
|
|
619
|
-
uuid: 'moved',
|
|
620
|
-
left: 100,
|
|
621
|
-
top: 100,
|
|
622
|
-
right: 200,
|
|
623
|
-
bottom: 200,
|
|
624
|
-
width: 100,
|
|
625
|
-
height: 100
|
|
626
|
-
};
|
|
627
|
-
const positions = calculateReflowPositions('moved', movedBounds, [], false);
|
|
625
|
+
const positions = calculateReflowPositions(['moved'], []);
|
|
628
626
|
expect(positions.size).to.equal(0);
|
|
629
627
|
});
|
|
630
628
|
it('handles single node (no other nodes to collide with)', () => {
|
|
@@ -637,7 +635,7 @@ describe('Collision Detection Utilities', () => {
|
|
|
637
635
|
width: 100,
|
|
638
636
|
height: 100
|
|
639
637
|
};
|
|
640
|
-
const positions = calculateReflowPositions('moved',
|
|
638
|
+
const positions = calculateReflowPositions(['moved'], [movedBounds]);
|
|
641
639
|
expect(positions.size).to.equal(0);
|
|
642
640
|
});
|
|
643
641
|
it('prevents infinite loops with complex collisions', () => {
|
|
@@ -664,7 +662,7 @@ describe('Collision Detection Utilities', () => {
|
|
|
664
662
|
});
|
|
665
663
|
}
|
|
666
664
|
// Should complete without hanging
|
|
667
|
-
const positions = calculateReflowPositions('moved',
|
|
665
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
668
666
|
// Should have resolved some collisions
|
|
669
667
|
expect(positions.size).to.be.greaterThan(0);
|
|
670
668
|
});
|