@nyaruka/temba-components 0.138.6 → 0.140.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/.github/workflows/cla.yml +1 -1
- package/.github/workflows/copilot-setup-steps.yml +6 -1
- package/CHANGELOG.md +26 -0
- package/demo/data/flows/sample-flow.json +24 -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 +1112 -882
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +10 -7
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/Dropdown.js +3 -1
- package/out-tsc/src/display/Dropdown.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +25 -32
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/display/Thumbnail.js +163 -5
- package/out-tsc/src/display/Thumbnail.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 +70 -29
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +290 -239
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +118 -10
- package/out-tsc/src/flow/NodeEditor.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/StickyNote.js +13 -4
- package/out-tsc/src/flow/StickyNote.js.map +1 -1
- package/out-tsc/src/flow/actions/audio-player.js +112 -0
- package/out-tsc/src/flow/actions/audio-player.js.map +1 -0
- package/out-tsc/src/flow/actions/enter_flow.js +43 -0
- package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
- package/out-tsc/src/flow/actions/play_audio.js +57 -4
- package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
- package/out-tsc/src/flow/actions/say_msg.js +86 -3
- package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
- package/out-tsc/src/flow/config.js +11 -3
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/shared-rules.js +1 -1
- package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -1
- package/out-tsc/src/flow/nodes/terminal.js +7 -0
- package/out-tsc/src/flow/nodes/terminal.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js +77 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_dial.js +151 -0
- package/out-tsc/src/flow/nodes/wait_for_dial.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_digits.js +61 -1
- package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_menu.js +173 -2
- package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
- package/out-tsc/src/flow/operators.js +21 -5
- package/out-tsc/src/flow/operators.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/flow/utils.js +213 -65
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +4 -2
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +49 -0
- package/out-tsc/src/form/FieldRenderer.js.map +1 -1
- package/out-tsc/src/interfaces.js +2 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/layout/Dialog.js +52 -7
- package/out-tsc/src/layout/Dialog.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/TembaChart.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 +10 -3
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/src/store/AppState.js +89 -3
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/test/actions/play_audio.test.js +118 -0
- package/out-tsc/test/actions/play_audio.test.js.map +1 -0
- package/out-tsc/test/actions/say_msg.test.js +158 -0
- package/out-tsc/test/actions/say_msg.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_audio.test.js +156 -0
- package/out-tsc/test/nodes/wait_for_audio.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_dial.test.js +336 -0
- package/out-tsc/test/nodes/wait_for_dial.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_digits.test.js +198 -84
- package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
- package/out-tsc/test/nodes/wait_for_menu.test.js +340 -0
- package/out-tsc/test/nodes/wait_for_menu.test.js.map +1 -0
- 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 +473 -220
- 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/out-tsc/test/temba-node-type-selector.test.js +6 -6
- package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/play_audio/editor/expression-url.png +0 -0
- package/screenshots/truth/actions/play_audio/editor/static-url.png +0 -0
- package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
- package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/multiline-text.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/text-with-audio-url.png +0 -0
- package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
- package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
- package/screenshots/truth/editor/router.png +0 -0
- package/screenshots/truth/editor/wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_audio/editor/basic-audio-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/basic-dial.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/dial-with-limits.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/digits-with-rules.png +0 -0
- package/screenshots/truth/nodes/wait_for_menu/editor/menu-with-digits.png +0 -0
- package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/src/display/Chat.ts +13 -7
- package/src/display/Dropdown.ts +3 -1
- package/src/display/FloatingTab.ts +24 -33
- package/src/display/Thumbnail.ts +162 -2
- package/src/flow/CanvasMenu.ts +8 -3
- package/src/flow/CanvasNode.ts +75 -30
- package/src/flow/Editor.ts +336 -288
- package/src/flow/NodeEditor.ts +137 -9
- package/src/flow/Plumber.ts +1011 -457
- package/src/flow/StickyNote.ts +14 -4
- package/src/flow/actions/audio-player.ts +127 -0
- package/src/flow/actions/enter_flow.ts +44 -0
- package/src/flow/actions/play_audio.ts +64 -5
- package/src/flow/actions/say_msg.ts +94 -4
- package/src/flow/config.ts +11 -3
- package/src/flow/nodes/shared-rules.ts +1 -1
- package/src/flow/nodes/terminal.ts +9 -0
- package/src/flow/nodes/wait_for_audio.ts +88 -0
- package/src/flow/nodes/wait_for_dial.ts +176 -0
- package/src/flow/nodes/wait_for_digits.ts +86 -2
- package/src/flow/nodes/wait_for_menu.ts +209 -3
- package/src/flow/operators.ts +23 -5
- package/src/flow/types.ts +23 -1
- package/src/flow/utils.ts +238 -81
- package/src/form/ArrayEditor.ts +4 -2
- package/src/form/FieldRenderer.ts +64 -1
- package/src/interfaces.ts +3 -1
- package/src/layout/Dialog.ts +53 -7
- package/src/list/TicketList.ts +4 -1
- package/src/live/TembaChart.ts +1 -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 +13 -3
- package/src/store/AppState.ts +105 -1
- package/src/store/flow-definition.d.ts +2 -0
- package/test/actions/play_audio.test.ts +155 -0
- package/test/actions/say_msg.test.ts +196 -0
- package/test/nodes/wait_for_audio.test.ts +182 -0
- package/test/nodes/wait_for_dial.test.ts +382 -0
- package/test/nodes/wait_for_digits.test.ts +233 -109
- package/test/nodes/wait_for_menu.test.ts +383 -0
- package/test/temba-floating-tab.test.ts +4 -6
- package/test/temba-flow-collision.test.ts +495 -293
- 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
- package/test/temba-node-type-selector.test.ts +6 -6
- package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
|
@@ -304,18 +304,16 @@ describe('Collision Detection Utilities', () => {
|
|
|
304
304
|
|
|
305
305
|
describe('calculateReflowPositions', () => {
|
|
306
306
|
it('returns empty map when no collisions', () => {
|
|
307
|
-
const movedBounds: NodeBounds = {
|
|
308
|
-
uuid: 'moved',
|
|
309
|
-
left: 100,
|
|
310
|
-
top: 100,
|
|
311
|
-
right: 200,
|
|
312
|
-
bottom: 200,
|
|
313
|
-
width: 100,
|
|
314
|
-
height: 100
|
|
315
|
-
};
|
|
316
|
-
|
|
317
307
|
const allBounds: NodeBounds[] = [
|
|
318
|
-
|
|
308
|
+
{
|
|
309
|
+
uuid: 'moved',
|
|
310
|
+
left: 100,
|
|
311
|
+
top: 100,
|
|
312
|
+
right: 200,
|
|
313
|
+
bottom: 200,
|
|
314
|
+
width: 100,
|
|
315
|
+
height: 100
|
|
316
|
+
},
|
|
319
317
|
{
|
|
320
318
|
uuid: 'node1',
|
|
321
319
|
left: 300,
|
|
@@ -327,67 +325,50 @@ describe('Collision Detection Utilities', () => {
|
|
|
327
325
|
}
|
|
328
326
|
];
|
|
329
327
|
|
|
330
|
-
const positions = calculateReflowPositions(
|
|
331
|
-
'moved',
|
|
332
|
-
movedBounds,
|
|
333
|
-
allBounds,
|
|
334
|
-
false
|
|
335
|
-
);
|
|
328
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
336
329
|
expect(positions.size).to.equal(0);
|
|
337
330
|
});
|
|
338
331
|
|
|
339
|
-
it('moves colliding node
|
|
340
|
-
const movedBounds: NodeBounds = {
|
|
341
|
-
uuid: 'moved',
|
|
342
|
-
left: 100,
|
|
343
|
-
top: 100,
|
|
344
|
-
right: 200,
|
|
345
|
-
bottom: 200,
|
|
346
|
-
width: 100,
|
|
347
|
-
height: 100
|
|
348
|
-
};
|
|
349
|
-
|
|
332
|
+
it('moves colliding node out of the way', () => {
|
|
350
333
|
const allBounds: NodeBounds[] = [
|
|
351
|
-
|
|
334
|
+
{
|
|
335
|
+
uuid: 'moved',
|
|
336
|
+
left: 100,
|
|
337
|
+
top: 100,
|
|
338
|
+
right: 200,
|
|
339
|
+
bottom: 200,
|
|
340
|
+
width: 100,
|
|
341
|
+
height: 100
|
|
342
|
+
},
|
|
352
343
|
{
|
|
353
344
|
uuid: 'node1',
|
|
354
|
-
left:
|
|
345
|
+
left: 100,
|
|
355
346
|
top: 150,
|
|
356
|
-
right:
|
|
347
|
+
right: 200,
|
|
357
348
|
bottom: 250,
|
|
358
349
|
width: 100,
|
|
359
350
|
height: 100
|
|
360
351
|
}
|
|
361
352
|
];
|
|
362
353
|
|
|
363
|
-
const positions = calculateReflowPositions(
|
|
364
|
-
'moved',
|
|
365
|
-
movedBounds,
|
|
366
|
-
allBounds,
|
|
367
|
-
false
|
|
368
|
-
);
|
|
354
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
369
355
|
|
|
370
356
|
expect(positions.size).to.equal(1);
|
|
371
357
|
expect(positions.has('node1')).to.be.true;
|
|
372
|
-
|
|
373
|
-
const newPos = positions.get('node1')!;
|
|
374
|
-
expect(newPos.left).to.equal(150); // left unchanged
|
|
375
|
-
expect(newPos.top).to.be.greaterThan(200); // moved below the moved node
|
|
358
|
+
expect(positions.has('moved')).to.be.false;
|
|
376
359
|
});
|
|
377
360
|
|
|
378
|
-
it('
|
|
379
|
-
const movedBounds: NodeBounds = {
|
|
380
|
-
uuid: 'moved',
|
|
381
|
-
left: 100,
|
|
382
|
-
top: 150,
|
|
383
|
-
right: 200,
|
|
384
|
-
bottom: 250,
|
|
385
|
-
width: 100,
|
|
386
|
-
height: 100
|
|
387
|
-
};
|
|
388
|
-
|
|
361
|
+
it('sacred node never appears in returned positions', () => {
|
|
389
362
|
const allBounds: NodeBounds[] = [
|
|
390
|
-
|
|
363
|
+
{
|
|
364
|
+
uuid: 'dropped',
|
|
365
|
+
left: 100,
|
|
366
|
+
top: 100,
|
|
367
|
+
right: 200,
|
|
368
|
+
bottom: 200,
|
|
369
|
+
width: 100,
|
|
370
|
+
height: 100
|
|
371
|
+
},
|
|
391
372
|
{
|
|
392
373
|
uuid: 'existing',
|
|
393
374
|
left: 100,
|
|
@@ -399,117 +380,135 @@ describe('Collision Detection Utilities', () => {
|
|
|
399
380
|
}
|
|
400
381
|
];
|
|
401
382
|
|
|
402
|
-
const positions = calculateReflowPositions(
|
|
403
|
-
'moved',
|
|
404
|
-
movedBounds,
|
|
405
|
-
allBounds,
|
|
406
|
-
true // droppedBelowMidpoint
|
|
407
|
-
);
|
|
383
|
+
const positions = calculateReflowPositions(['dropped'], allBounds);
|
|
408
384
|
|
|
409
|
-
expect(positions.
|
|
410
|
-
expect(positions.has('
|
|
411
|
-
|
|
412
|
-
const newPos = positions.get('moved')!;
|
|
413
|
-
expect(newPos.top).to.be.greaterThan(200); // moved below existing node
|
|
385
|
+
expect(positions.has('dropped')).to.be.false;
|
|
386
|
+
expect(positions.has('existing')).to.be.true;
|
|
414
387
|
});
|
|
415
388
|
|
|
416
|
-
it('
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
// So dropped node should keep position, existing moves down
|
|
420
|
-
const movedBounds: NodeBounds = {
|
|
421
|
-
uuid: 'dropped',
|
|
422
|
-
left: 100,
|
|
423
|
-
top: 80,
|
|
424
|
-
right: 200,
|
|
425
|
-
bottom: 180,
|
|
426
|
-
width: 100,
|
|
427
|
-
height: 100
|
|
428
|
-
};
|
|
429
|
-
|
|
389
|
+
it('prefers least-displacement direction', () => {
|
|
390
|
+
// Sacred at (100,100)-(200,200), collider at (180,100)-(280,200)
|
|
391
|
+
// Right requires only 60px displacement, down requires 140px
|
|
430
392
|
const allBounds: NodeBounds[] = [
|
|
431
|
-
movedBounds,
|
|
432
393
|
{
|
|
433
|
-
uuid: '
|
|
394
|
+
uuid: 'sacred',
|
|
434
395
|
left: 100,
|
|
435
396
|
top: 100,
|
|
436
397
|
right: 200,
|
|
437
398
|
bottom: 200,
|
|
438
399
|
width: 100,
|
|
439
400
|
height: 100
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
uuid: 'collider',
|
|
404
|
+
left: 180,
|
|
405
|
+
top: 100,
|
|
406
|
+
right: 280,
|
|
407
|
+
bottom: 200,
|
|
408
|
+
width: 100,
|
|
409
|
+
height: 100
|
|
440
410
|
}
|
|
441
411
|
];
|
|
442
412
|
|
|
443
|
-
const positions = calculateReflowPositions(
|
|
444
|
-
'dropped',
|
|
445
|
-
movedBounds,
|
|
446
|
-
allBounds,
|
|
447
|
-
false // dropped node's bottom is above target midpoint, dropped node gets priority
|
|
448
|
-
);
|
|
449
|
-
|
|
450
|
-
// Existing node should be moved down
|
|
451
|
-
expect(positions.has('existing')).to.be.true;
|
|
452
|
-
expect(positions.has('dropped')).to.be.false; // dropped keeps its position
|
|
413
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
453
414
|
|
|
454
|
-
|
|
455
|
-
|
|
415
|
+
expect(positions.has('collider')).to.be.true;
|
|
416
|
+
const newPos = positions.get('collider')!;
|
|
417
|
+
// Should move right (shorter) rather than down (longer)
|
|
418
|
+
expect(newPos.left).to.be.greaterThan(200);
|
|
419
|
+
expect(newPos.top).to.equal(100); // vertical position unchanged
|
|
456
420
|
});
|
|
457
421
|
|
|
458
|
-
it('
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
422
|
+
it('prefers up when it is the shortest move', () => {
|
|
423
|
+
// Sacred at (100,200)-(200,300), collider at (100,180)-(200,280)
|
|
424
|
+
// Up: newTop=snapToGrid(200-100-30)=snapToGrid(70)=80, distance=100
|
|
425
|
+
// Down: newTop=snapToGrid(300+30)=340, distance=160
|
|
426
|
+
// Right: newLeft=snapToGrid(200+30)=240, distance=140
|
|
427
|
+
const allBounds: NodeBounds[] = [
|
|
428
|
+
{
|
|
429
|
+
uuid: 'sacred',
|
|
430
|
+
left: 100,
|
|
431
|
+
top: 200,
|
|
432
|
+
right: 200,
|
|
433
|
+
bottom: 300,
|
|
434
|
+
width: 100,
|
|
435
|
+
height: 100
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
uuid: 'collider',
|
|
439
|
+
left: 100,
|
|
440
|
+
top: 180,
|
|
441
|
+
right: 200,
|
|
442
|
+
bottom: 280,
|
|
443
|
+
width: 100,
|
|
444
|
+
height: 100
|
|
445
|
+
}
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
449
|
+
|
|
450
|
+
expect(positions.has('collider')).to.be.true;
|
|
451
|
+
const newPos = positions.get('collider')!;
|
|
452
|
+
// Should move up (shortest displacement)
|
|
453
|
+
expect(newPos.top).to.be.lessThan(200);
|
|
454
|
+
expect(newPos.left).to.equal(100); // horizontal position unchanged
|
|
455
|
+
});
|
|
471
456
|
|
|
457
|
+
it('prefers axis-matching direction even with a cascade', () => {
|
|
458
|
+
// Sacred at (100,100)-(200,200), collider at (100,150)-(200,250)
|
|
459
|
+
// Overlap is 100w x 50h (wide) = vertical collision = prefer down
|
|
460
|
+
// A blocker sits below at (100,280)-(200,380), so down causes a cascade
|
|
461
|
+
// But axis bias still prefers down over moving right
|
|
472
462
|
const allBounds: NodeBounds[] = [
|
|
473
|
-
movedBounds,
|
|
474
463
|
{
|
|
475
|
-
uuid: '
|
|
464
|
+
uuid: 'sacred',
|
|
476
465
|
left: 100,
|
|
477
466
|
top: 100,
|
|
478
467
|
right: 200,
|
|
479
468
|
bottom: 200,
|
|
480
469
|
width: 100,
|
|
481
470
|
height: 100
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
uuid: 'collider',
|
|
474
|
+
left: 100,
|
|
475
|
+
top: 150,
|
|
476
|
+
right: 200,
|
|
477
|
+
bottom: 250,
|
|
478
|
+
width: 100,
|
|
479
|
+
height: 100
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
uuid: 'blocker',
|
|
483
|
+
left: 100,
|
|
484
|
+
top: 280,
|
|
485
|
+
right: 200,
|
|
486
|
+
bottom: 380,
|
|
487
|
+
width: 100,
|
|
488
|
+
height: 100
|
|
482
489
|
}
|
|
483
490
|
];
|
|
484
491
|
|
|
485
|
-
const positions = calculateReflowPositions(
|
|
486
|
-
'dropped',
|
|
487
|
-
movedBounds,
|
|
488
|
-
allBounds,
|
|
489
|
-
true // dropped node's bottom is below target midpoint, target gets priority
|
|
490
|
-
);
|
|
492
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
491
493
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
expect(droppedNewPos.top).to.be.greaterThan(200); // moved below existing node
|
|
494
|
+
expect(positions.has('collider')).to.be.true;
|
|
495
|
+
const newPos = positions.get('collider')!;
|
|
496
|
+
// Axis bias prefers down (vertical) even though it cascades into blocker
|
|
497
|
+
expect(newPos.top).to.be.greaterThan(200);
|
|
498
|
+
expect(newPos.left).to.equal(100); // horizontal position unchanged
|
|
498
499
|
});
|
|
499
500
|
|
|
500
501
|
it('resolves cascading collisions', () => {
|
|
501
|
-
const movedBounds: NodeBounds = {
|
|
502
|
-
uuid: 'moved',
|
|
503
|
-
left: 100,
|
|
504
|
-
top: 100,
|
|
505
|
-
right: 200,
|
|
506
|
-
bottom: 200,
|
|
507
|
-
width: 100,
|
|
508
|
-
height: 100
|
|
509
|
-
};
|
|
510
|
-
|
|
511
502
|
const allBounds: NodeBounds[] = [
|
|
512
|
-
|
|
503
|
+
{
|
|
504
|
+
uuid: 'moved',
|
|
505
|
+
left: 100,
|
|
506
|
+
top: 100,
|
|
507
|
+
right: 200,
|
|
508
|
+
bottom: 200,
|
|
509
|
+
width: 100,
|
|
510
|
+
height: 100
|
|
511
|
+
},
|
|
513
512
|
{
|
|
514
513
|
uuid: 'node1',
|
|
515
514
|
left: 100,
|
|
@@ -530,44 +529,20 @@ describe('Collision Detection Utilities', () => {
|
|
|
530
529
|
}
|
|
531
530
|
];
|
|
532
531
|
|
|
533
|
-
const positions = calculateReflowPositions(
|
|
534
|
-
'moved',
|
|
535
|
-
movedBounds,
|
|
536
|
-
allBounds,
|
|
537
|
-
false
|
|
538
|
-
);
|
|
532
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
539
533
|
|
|
540
|
-
//
|
|
534
|
+
// At least one node should be repositioned
|
|
541
535
|
expect(positions.size).to.be.greaterThan(0);
|
|
542
536
|
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
const node1Pos = positions.get('node1')!;
|
|
546
|
-
const node2Pos = positions.get('node2')!;
|
|
547
|
-
|
|
548
|
-
// node2 should be below node1
|
|
549
|
-
expect(node2Pos.top).to.be.greaterThan(node1Pos.top);
|
|
550
|
-
}
|
|
537
|
+
// No node should overlap with the sacred node or each other after reflow
|
|
538
|
+
// (verified by the algorithm's correctness guarantee)
|
|
551
539
|
});
|
|
552
540
|
|
|
553
|
-
it('handles multiple
|
|
554
|
-
//
|
|
555
|
-
// This scenario has nodes that initially don't collide with moved node,
|
|
556
|
-
// but will collide with other nodes that get moved
|
|
557
|
-
const movedBounds: NodeBounds = {
|
|
558
|
-
uuid: 'moved',
|
|
559
|
-
left: 100,
|
|
560
|
-
top: 50,
|
|
561
|
-
right: 200,
|
|
562
|
-
bottom: 150,
|
|
563
|
-
width: 100,
|
|
564
|
-
height: 100
|
|
565
|
-
};
|
|
566
|
-
|
|
541
|
+
it('handles multiple sacred nodes', () => {
|
|
542
|
+
// Two sacred nodes with a non-sacred node overlapping one
|
|
567
543
|
const allBounds: NodeBounds[] = [
|
|
568
|
-
movedBounds,
|
|
569
544
|
{
|
|
570
|
-
uuid: '
|
|
545
|
+
uuid: 'sacred1',
|
|
571
546
|
left: 100,
|
|
572
547
|
top: 100,
|
|
573
548
|
right: 200,
|
|
@@ -576,198 +551,436 @@ describe('Collision Detection Utilities', () => {
|
|
|
576
551
|
height: 100
|
|
577
552
|
},
|
|
578
553
|
{
|
|
579
|
-
uuid: '
|
|
554
|
+
uuid: 'sacred2',
|
|
580
555
|
left: 100,
|
|
581
|
-
top:
|
|
556
|
+
top: 400,
|
|
582
557
|
right: 200,
|
|
583
|
-
bottom:
|
|
558
|
+
bottom: 500,
|
|
584
559
|
width: 100,
|
|
585
560
|
height: 100
|
|
586
561
|
},
|
|
587
562
|
{
|
|
588
|
-
uuid: '
|
|
563
|
+
uuid: 'collider',
|
|
589
564
|
left: 100,
|
|
590
|
-
top:
|
|
565
|
+
top: 150,
|
|
591
566
|
right: 200,
|
|
592
|
-
bottom:
|
|
567
|
+
bottom: 250,
|
|
593
568
|
width: 100,
|
|
594
569
|
height: 100
|
|
595
570
|
}
|
|
596
571
|
];
|
|
597
572
|
|
|
598
573
|
const positions = calculateReflowPositions(
|
|
599
|
-
'
|
|
600
|
-
|
|
601
|
-
allBounds,
|
|
602
|
-
false
|
|
574
|
+
['sacred1', 'sacred2'],
|
|
575
|
+
allBounds
|
|
603
576
|
);
|
|
604
577
|
|
|
605
|
-
//
|
|
606
|
-
expect(positions.
|
|
607
|
-
expect(positions.has('
|
|
608
|
-
|
|
609
|
-
expect(positions.has('
|
|
578
|
+
// Sacred nodes should never be moved
|
|
579
|
+
expect(positions.has('sacred1')).to.be.false;
|
|
580
|
+
expect(positions.has('sacred2')).to.be.false;
|
|
581
|
+
// Collider should be moved
|
|
582
|
+
expect(positions.has('collider')).to.be.true;
|
|
583
|
+
});
|
|
610
584
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
585
|
+
it('does not move a node into another sacred node', () => {
|
|
586
|
+
// Two sacred nodes close together with a collider between them
|
|
587
|
+
// Moving right would overlap sacred2, so it should choose another direction
|
|
588
|
+
const allBounds: NodeBounds[] = [
|
|
589
|
+
{
|
|
590
|
+
uuid: 'sacred1',
|
|
591
|
+
left: 100,
|
|
592
|
+
top: 100,
|
|
593
|
+
right: 200,
|
|
594
|
+
bottom: 200,
|
|
595
|
+
width: 100,
|
|
596
|
+
height: 100
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
uuid: 'sacred2',
|
|
600
|
+
left: 240,
|
|
601
|
+
top: 100,
|
|
602
|
+
right: 340,
|
|
603
|
+
bottom: 200,
|
|
604
|
+
width: 100,
|
|
605
|
+
height: 100
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
uuid: 'collider',
|
|
609
|
+
left: 150,
|
|
610
|
+
top: 100,
|
|
611
|
+
right: 250,
|
|
612
|
+
bottom: 200,
|
|
613
|
+
width: 100,
|
|
614
|
+
height: 100
|
|
615
|
+
}
|
|
616
|
+
];
|
|
614
617
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
});
|
|
618
|
+
const positions = calculateReflowPositions(
|
|
619
|
+
['sacred1', 'sacred2'],
|
|
620
|
+
allBounds
|
|
621
|
+
);
|
|
620
622
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
//
|
|
624
|
-
const
|
|
625
|
-
uuid: '
|
|
626
|
-
left:
|
|
627
|
-
top:
|
|
628
|
-
right:
|
|
629
|
-
bottom:
|
|
623
|
+
expect(positions.has('collider')).to.be.true;
|
|
624
|
+
const newPos = positions.get('collider')!;
|
|
625
|
+
// Should not overlap either sacred node after reflow
|
|
626
|
+
const newBounds: NodeBounds = {
|
|
627
|
+
uuid: 'collider',
|
|
628
|
+
left: newPos.left,
|
|
629
|
+
top: newPos.top,
|
|
630
|
+
right: newPos.left + 100,
|
|
631
|
+
bottom: newPos.top + 100,
|
|
630
632
|
width: 100,
|
|
631
633
|
height: 100
|
|
632
634
|
};
|
|
635
|
+
expect(nodesOverlap(newBounds, allBounds[0])).to.be.false;
|
|
636
|
+
expect(nodesOverlap(newBounds, allBounds[1])).to.be.false;
|
|
637
|
+
});
|
|
633
638
|
|
|
639
|
+
it('snaps reflow positions to grid', () => {
|
|
634
640
|
const allBounds: NodeBounds[] = [
|
|
635
|
-
movedBounds,
|
|
636
641
|
{
|
|
637
|
-
uuid: '
|
|
642
|
+
uuid: 'sacred',
|
|
643
|
+
left: 100,
|
|
644
|
+
top: 100,
|
|
645
|
+
right: 200,
|
|
646
|
+
bottom: 200,
|
|
647
|
+
width: 100,
|
|
648
|
+
height: 100
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
uuid: 'collider',
|
|
638
652
|
left: 100,
|
|
639
653
|
top: 150,
|
|
640
654
|
right: 200,
|
|
641
655
|
bottom: 250,
|
|
642
656
|
width: 100,
|
|
643
657
|
height: 100
|
|
658
|
+
}
|
|
659
|
+
];
|
|
660
|
+
|
|
661
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
662
|
+
|
|
663
|
+
expect(positions.has('collider')).to.be.true;
|
|
664
|
+
const newPos = positions.get('collider')!;
|
|
665
|
+
// Both coordinates should be multiples of 20 (grid size)
|
|
666
|
+
expect(newPos.left % 20).to.equal(0);
|
|
667
|
+
expect(newPos.top % 20).to.equal(0);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('clamps positions to zero (no negative coordinates)', () => {
|
|
671
|
+
// Sacred node near top-left, collider above it
|
|
672
|
+
// Moving up would go negative, so it should fall back
|
|
673
|
+
const allBounds: NodeBounds[] = [
|
|
674
|
+
{
|
|
675
|
+
uuid: 'sacred',
|
|
676
|
+
left: 0,
|
|
677
|
+
top: 0,
|
|
678
|
+
right: 200,
|
|
679
|
+
bottom: 200,
|
|
680
|
+
width: 200,
|
|
681
|
+
height: 200
|
|
644
682
|
},
|
|
645
683
|
{
|
|
646
|
-
uuid: '
|
|
684
|
+
uuid: 'collider',
|
|
685
|
+
left: 0,
|
|
686
|
+
top: 100,
|
|
687
|
+
right: 200,
|
|
688
|
+
bottom: 200,
|
|
689
|
+
width: 200,
|
|
690
|
+
height: 100
|
|
691
|
+
}
|
|
692
|
+
];
|
|
693
|
+
|
|
694
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
695
|
+
|
|
696
|
+
expect(positions.has('collider')).to.be.true;
|
|
697
|
+
const newPos = positions.get('collider')!;
|
|
698
|
+
expect(newPos.left).to.be.at.least(0);
|
|
699
|
+
expect(newPos.top).to.be.at.least(0);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it('does not move a lower node above the sacred node', () => {
|
|
703
|
+
// Collider is below sacred (collider.top > sacred.top)
|
|
704
|
+
// Up should be filtered, so collider goes down or to the side
|
|
705
|
+
const allBounds: NodeBounds[] = [
|
|
706
|
+
{
|
|
707
|
+
uuid: 'sacred',
|
|
708
|
+
left: 100,
|
|
709
|
+
top: 100,
|
|
710
|
+
right: 200,
|
|
711
|
+
bottom: 200,
|
|
712
|
+
width: 100,
|
|
713
|
+
height: 100
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
uuid: 'collider',
|
|
647
717
|
left: 100,
|
|
648
|
-
top:
|
|
718
|
+
top: 180,
|
|
649
719
|
right: 200,
|
|
650
|
-
bottom:
|
|
720
|
+
bottom: 280,
|
|
651
721
|
width: 100,
|
|
652
722
|
height: 100
|
|
653
723
|
}
|
|
654
724
|
];
|
|
655
725
|
|
|
656
|
-
const positions = calculateReflowPositions(
|
|
657
|
-
'moved',
|
|
658
|
-
movedBounds,
|
|
659
|
-
allBounds,
|
|
660
|
-
false
|
|
661
|
-
);
|
|
726
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
662
727
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
expect(
|
|
728
|
+
expect(positions.has('collider')).to.be.true;
|
|
729
|
+
const newPos = positions.get('collider')!;
|
|
730
|
+
// Should NOT move above the sacred node's top
|
|
731
|
+
expect(newPos.top).to.be.at.least(100);
|
|
732
|
+
});
|
|
667
733
|
|
|
668
|
-
|
|
669
|
-
|
|
734
|
+
it('does not move a right-of node left of the sacred node', () => {
|
|
735
|
+
// Collider is to the right of sacred (collider.left > sacred.left)
|
|
736
|
+
// Left should be filtered
|
|
737
|
+
const allBounds: NodeBounds[] = [
|
|
738
|
+
{
|
|
739
|
+
uuid: 'sacred',
|
|
740
|
+
left: 100,
|
|
741
|
+
top: 100,
|
|
742
|
+
right: 200,
|
|
743
|
+
bottom: 200,
|
|
744
|
+
width: 100,
|
|
745
|
+
height: 100
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
uuid: 'collider',
|
|
749
|
+
left: 180,
|
|
750
|
+
top: 100,
|
|
751
|
+
right: 280,
|
|
752
|
+
bottom: 200,
|
|
753
|
+
width: 100,
|
|
754
|
+
height: 100
|
|
755
|
+
}
|
|
756
|
+
];
|
|
670
757
|
|
|
671
|
-
|
|
672
|
-
expect(node1Pos.top).to.be.at.least(220); // 200 + 20
|
|
758
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
673
759
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
760
|
+
expect(positions.has('collider')).to.be.true;
|
|
761
|
+
const newPos = positions.get('collider')!;
|
|
762
|
+
// Should NOT move left of the sacred node's left
|
|
763
|
+
expect(newPos.left).to.be.at.least(100);
|
|
677
764
|
});
|
|
678
765
|
|
|
679
|
-
it('
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
766
|
+
it('prefers vertical for wide overlap (vertical collision)', () => {
|
|
767
|
+
// Nodes stacked: same horizontal position, slight vertical overlap
|
|
768
|
+
// Overlap: 100w x 30h (wider than tall) = vertical collision = prefer up/down
|
|
769
|
+
const allBounds: NodeBounds[] = [
|
|
770
|
+
{
|
|
771
|
+
uuid: 'sacred',
|
|
772
|
+
left: 100,
|
|
773
|
+
top: 100,
|
|
774
|
+
right: 200,
|
|
775
|
+
bottom: 200,
|
|
776
|
+
width: 100,
|
|
777
|
+
height: 100
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
uuid: 'collider',
|
|
781
|
+
left: 100,
|
|
782
|
+
top: 170,
|
|
783
|
+
right: 200,
|
|
784
|
+
bottom: 270,
|
|
785
|
+
width: 100,
|
|
786
|
+
height: 100
|
|
787
|
+
}
|
|
788
|
+
];
|
|
789
|
+
|
|
790
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
791
|
+
|
|
792
|
+
expect(positions.has('collider')).to.be.true;
|
|
793
|
+
const newPos = positions.get('collider')!;
|
|
794
|
+
// Should move vertically (down since collider is below)
|
|
795
|
+
expect(newPos.top).to.be.greaterThan(200);
|
|
796
|
+
expect(newPos.left).to.equal(100); // horizontal position unchanged
|
|
797
|
+
});
|
|
689
798
|
|
|
799
|
+
it('prefers horizontal for tall overlap (horizontal collision)', () => {
|
|
800
|
+
// Nodes side-by-side: same vertical position, slight horizontal overlap
|
|
801
|
+
// Overlap: 30w x 100h (taller than wide) = horizontal collision = prefer left/right
|
|
690
802
|
const allBounds: NodeBounds[] = [
|
|
691
|
-
movedBounds,
|
|
692
803
|
{
|
|
693
|
-
uuid: '
|
|
694
|
-
left:
|
|
695
|
-
top:
|
|
696
|
-
right:
|
|
697
|
-
bottom:
|
|
804
|
+
uuid: 'sacred',
|
|
805
|
+
left: 100,
|
|
806
|
+
top: 100,
|
|
807
|
+
right: 200,
|
|
808
|
+
bottom: 200,
|
|
809
|
+
width: 100,
|
|
810
|
+
height: 100
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
uuid: 'collider',
|
|
814
|
+
left: 170,
|
|
815
|
+
top: 100,
|
|
816
|
+
right: 270,
|
|
817
|
+
bottom: 200,
|
|
698
818
|
width: 100,
|
|
699
819
|
height: 100
|
|
700
820
|
}
|
|
701
821
|
];
|
|
702
822
|
|
|
703
|
-
const positions = calculateReflowPositions(
|
|
704
|
-
'moved',
|
|
705
|
-
movedBounds,
|
|
706
|
-
allBounds,
|
|
707
|
-
false
|
|
708
|
-
);
|
|
823
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
709
824
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
825
|
+
expect(positions.has('collider')).to.be.true;
|
|
826
|
+
const newPos = positions.get('collider')!;
|
|
827
|
+
// Should move horizontally (right since collider is right of sacred)
|
|
828
|
+
expect(newPos.left).to.be.greaterThan(200);
|
|
829
|
+
expect(newPos.top).to.equal(100); // vertical position unchanged
|
|
713
830
|
});
|
|
714
831
|
|
|
715
|
-
it('
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
832
|
+
it('axis bias tolerates a few cascading collisions', () => {
|
|
833
|
+
// Sacred at (100,100)-(200,200), collider at (100,170)-(200,270)
|
|
834
|
+
// Overlap: 100w x 30h = vertical collision = prefer down
|
|
835
|
+
// Two blockers below: moving down causes 2 cascades
|
|
836
|
+
// Moving right causes 0 cascades but is axis-mismatched
|
|
837
|
+
// Axis bias should still prefer down with 2 cascades
|
|
838
|
+
const allBounds: NodeBounds[] = [
|
|
839
|
+
{
|
|
840
|
+
uuid: 'sacred',
|
|
841
|
+
left: 100,
|
|
842
|
+
top: 100,
|
|
843
|
+
right: 200,
|
|
844
|
+
bottom: 200,
|
|
845
|
+
width: 100,
|
|
846
|
+
height: 100
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
uuid: 'collider',
|
|
850
|
+
left: 100,
|
|
851
|
+
top: 170,
|
|
852
|
+
right: 200,
|
|
853
|
+
bottom: 270,
|
|
854
|
+
width: 100,
|
|
855
|
+
height: 100
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
uuid: 'blocker1',
|
|
859
|
+
left: 100,
|
|
860
|
+
top: 260,
|
|
861
|
+
right: 200,
|
|
862
|
+
bottom: 360,
|
|
863
|
+
width: 100,
|
|
864
|
+
height: 100
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
uuid: 'blocker2',
|
|
868
|
+
left: 100,
|
|
869
|
+
top: 350,
|
|
870
|
+
right: 200,
|
|
871
|
+
bottom: 450,
|
|
872
|
+
width: 100,
|
|
873
|
+
height: 100
|
|
874
|
+
}
|
|
875
|
+
];
|
|
725
876
|
|
|
877
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
878
|
+
|
|
879
|
+
expect(positions.has('collider')).to.be.true;
|
|
880
|
+
const newPos = positions.get('collider')!;
|
|
881
|
+
// Should still prefer down (axis match) despite 2 cascades
|
|
882
|
+
expect(newPos.top).to.be.greaterThan(200);
|
|
883
|
+
expect(newPos.left).to.equal(100);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it('sacred node yields to existing top node when dropped below its top', () => {
|
|
887
|
+
// Existing node at top of canvas, sacred dropped overlapping from below
|
|
726
888
|
const allBounds: NodeBounds[] = [
|
|
727
|
-
movedBounds,
|
|
728
889
|
{
|
|
729
|
-
uuid: '
|
|
890
|
+
uuid: 'existing',
|
|
730
891
|
left: 100,
|
|
731
|
-
top:
|
|
892
|
+
top: 0,
|
|
732
893
|
right: 200,
|
|
733
|
-
bottom:
|
|
894
|
+
bottom: 100,
|
|
895
|
+
width: 100,
|
|
896
|
+
height: 100
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
uuid: 'dropped',
|
|
900
|
+
left: 100,
|
|
901
|
+
top: 50,
|
|
902
|
+
right: 200,
|
|
903
|
+
bottom: 150,
|
|
734
904
|
width: 100,
|
|
735
905
|
height: 100
|
|
736
906
|
}
|
|
737
907
|
];
|
|
738
908
|
|
|
739
|
-
const positions = calculateReflowPositions(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
);
|
|
909
|
+
const positions = calculateReflowPositions(['dropped'], allBounds);
|
|
910
|
+
|
|
911
|
+
// Sacred (dropped) should yield since it didn't drop above existing
|
|
912
|
+
// and existing has no room to move up
|
|
913
|
+
expect(positions.has('dropped')).to.be.true;
|
|
914
|
+
expect(positions.has('existing')).to.be.false;
|
|
915
|
+
|
|
916
|
+
const newPos = positions.get('dropped')!;
|
|
917
|
+
expect(newPos.top).to.be.greaterThanOrEqual(100); // moved below existing
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it('sacred keeps position when dropped above existing node', () => {
|
|
921
|
+
// Sacred node dropped above existing node - sacred gets priority
|
|
922
|
+
const allBounds: NodeBounds[] = [
|
|
923
|
+
{
|
|
924
|
+
uuid: 'existing',
|
|
925
|
+
left: 100,
|
|
926
|
+
top: 50,
|
|
927
|
+
right: 200,
|
|
928
|
+
bottom: 150,
|
|
929
|
+
width: 100,
|
|
930
|
+
height: 100
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
uuid: 'dropped',
|
|
934
|
+
left: 100,
|
|
935
|
+
top: 0,
|
|
936
|
+
right: 200,
|
|
937
|
+
bottom: 100,
|
|
938
|
+
width: 100,
|
|
939
|
+
height: 100
|
|
940
|
+
}
|
|
941
|
+
];
|
|
942
|
+
|
|
943
|
+
const positions = calculateReflowPositions(['dropped'], allBounds);
|
|
944
|
+
|
|
945
|
+
// Sacred dropped above existing, so it keeps priority
|
|
946
|
+
expect(positions.has('dropped')).to.be.false;
|
|
947
|
+
expect(positions.has('existing')).to.be.true;
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it('sacred keeps position when dropped at same top as existing node', () => {
|
|
951
|
+
// Both at top=0 - sacred keeps priority since it's not below existing
|
|
952
|
+
const allBounds: NodeBounds[] = [
|
|
953
|
+
{
|
|
954
|
+
uuid: 'existing',
|
|
955
|
+
left: 100,
|
|
956
|
+
top: 0,
|
|
957
|
+
right: 200,
|
|
958
|
+
bottom: 100,
|
|
959
|
+
width: 100,
|
|
960
|
+
height: 100
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
uuid: 'dropped',
|
|
964
|
+
left: 100,
|
|
965
|
+
top: 0,
|
|
966
|
+
right: 200,
|
|
967
|
+
bottom: 100,
|
|
968
|
+
width: 100,
|
|
969
|
+
height: 100
|
|
970
|
+
}
|
|
971
|
+
];
|
|
745
972
|
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
973
|
+
const positions = calculateReflowPositions(['dropped'], allBounds);
|
|
974
|
+
|
|
975
|
+
// Sacred at same top keeps priority - existing node moves
|
|
976
|
+
expect(positions.has('dropped')).to.be.false;
|
|
977
|
+
expect(positions.has('existing')).to.be.true;
|
|
749
978
|
});
|
|
750
979
|
});
|
|
751
980
|
|
|
752
981
|
describe('edge cases', () => {
|
|
753
982
|
it('handles empty allBounds array', () => {
|
|
754
|
-
const
|
|
755
|
-
uuid: 'moved',
|
|
756
|
-
left: 100,
|
|
757
|
-
top: 100,
|
|
758
|
-
right: 200,
|
|
759
|
-
bottom: 200,
|
|
760
|
-
width: 100,
|
|
761
|
-
height: 100
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
const positions = calculateReflowPositions(
|
|
765
|
-
'moved',
|
|
766
|
-
movedBounds,
|
|
767
|
-
[],
|
|
768
|
-
false
|
|
769
|
-
);
|
|
770
|
-
|
|
983
|
+
const positions = calculateReflowPositions(['moved'], []);
|
|
771
984
|
expect(positions.size).to.equal(0);
|
|
772
985
|
});
|
|
773
986
|
|
|
@@ -782,13 +995,7 @@ describe('Collision Detection Utilities', () => {
|
|
|
782
995
|
height: 100
|
|
783
996
|
};
|
|
784
997
|
|
|
785
|
-
const positions = calculateReflowPositions(
|
|
786
|
-
'moved',
|
|
787
|
-
movedBounds,
|
|
788
|
-
[movedBounds],
|
|
789
|
-
false
|
|
790
|
-
);
|
|
791
|
-
|
|
998
|
+
const positions = calculateReflowPositions(['moved'], [movedBounds]);
|
|
792
999
|
expect(positions.size).to.equal(0);
|
|
793
1000
|
});
|
|
794
1001
|
|
|
@@ -819,12 +1026,7 @@ describe('Collision Detection Utilities', () => {
|
|
|
819
1026
|
}
|
|
820
1027
|
|
|
821
1028
|
// Should complete without hanging
|
|
822
|
-
const positions = calculateReflowPositions(
|
|
823
|
-
'moved',
|
|
824
|
-
movedBounds,
|
|
825
|
-
allBounds,
|
|
826
|
-
false
|
|
827
|
-
);
|
|
1029
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
828
1030
|
|
|
829
1031
|
// Should have resolved some collisions
|
|
830
1032
|
expect(positions.size).to.be.greaterThan(0);
|