|
@@ -469,7 +469,10 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
469
|
469
|
}
|
470
|
470
|
|
471
|
471
|
onPersist = () => {
|
472
|
|
- this.broadcastPageChanges()
|
|
472
|
+ // If we are part of a room, send our changes to the server
|
|
473
|
+ if (this.callbacks.onChangePage) {
|
|
474
|
+ this.broadcastPageChanges()
|
|
475
|
+ }
|
473
|
476
|
}
|
474
|
477
|
|
475
|
478
|
private prevSelectedIds = this.selectedIds
|
|
@@ -493,6 +496,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
493
|
496
|
|
494
|
497
|
/* ----------- Managing Multiplayer State ----------- */
|
495
|
498
|
|
|
499
|
+ private justSent = false
|
496
|
500
|
private prevShapes = this.page.shapes
|
497
|
501
|
private prevBindings = this.page.bindings
|
498
|
502
|
|
|
@@ -502,6 +506,26 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
502
|
506
|
const changedShapes: Record<string, TDShape | undefined> = {}
|
503
|
507
|
const changedBindings: Record<string, TDBinding | undefined> = {}
|
504
|
508
|
|
|
509
|
+ // const visitedIds = new Set<string>()
|
|
510
|
+ // const shapesToVisit = this.shapes
|
|
511
|
+
|
|
512
|
+ // while (shapesToVisit.length > 0) {
|
|
513
|
+ // const shape = shapesToVisit.pop()
|
|
514
|
+ // if (!shape) break
|
|
515
|
+ // visitedIds.add(shape.id)
|
|
516
|
+ // if (this.prevShapes[shape.id] !== shape) {
|
|
517
|
+ // changedShapes[shape.id] = shape
|
|
518
|
+
|
|
519
|
+ // if (shape.parentId !== this.currentPageId) {
|
|
520
|
+ // shapesToVisit.push(this.page.shapes[shape.parentId])
|
|
521
|
+ // }
|
|
522
|
+
|
|
523
|
+ // if (shape.children) {
|
|
524
|
+
|
|
525
|
+ // }
|
|
526
|
+ // }
|
|
527
|
+ // }
|
|
528
|
+
|
505
|
529
|
this.shapes.forEach((shape) => {
|
506
|
530
|
visited.add(shape.id)
|
507
|
531
|
if (this.prevShapes[shape.id] !== shape) {
|
|
@@ -512,6 +536,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
512
|
536
|
Object.keys(this.prevShapes)
|
513
|
537
|
.filter((id) => !visited.has(id))
|
514
|
538
|
.forEach((id) => {
|
|
539
|
+ // After visiting all the current shapes, if we haven't visited a
|
|
540
|
+ // previously present shape, then it was deleted
|
515
|
541
|
changedShapes[id] = undefined
|
516
|
542
|
})
|
517
|
543
|
|
|
@@ -525,16 +551,73 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
525
|
551
|
Object.keys(this.prevBindings)
|
526
|
552
|
.filter((id) => !visited.has(id))
|
527
|
553
|
.forEach((id) => {
|
|
554
|
+ // After visiting all the current bindings, if we haven't visited a
|
|
555
|
+ // previously present shape, then it was deleted
|
528
|
556
|
changedBindings[id] = undefined
|
529
|
557
|
})
|
530
|
558
|
|
|
559
|
+ this.justSent = true
|
531
|
560
|
this.callbacks.onChangePage?.(this, changedShapes, changedBindings)
|
532
|
|
-
|
533
|
561
|
this.callbacks.onPersist?.(this)
|
534
|
562
|
this.prevShapes = this.page.shapes
|
535
|
563
|
this.prevBindings = this.page.bindings
|
536
|
564
|
}
|
537
|
565
|
|
|
566
|
+ getReservedContent = (ids: string[], pageId = this.currentPageId) => {
|
|
567
|
+ const { bindings } = this.document.pages[pageId]
|
|
568
|
+
|
|
569
|
+ // We want to know which shapes we need to
|
|
570
|
+ const reservedShapes: Record<string, TDShape> = {}
|
|
571
|
+ const reservedBindings: Record<string, TDBinding> = {}
|
|
572
|
+
|
|
573
|
+ // Quick lookup maps for bindings
|
|
574
|
+ const bindingsArr = Object.values(bindings)
|
|
575
|
+ const boundTos = new Map(bindingsArr.map((binding) => [binding.toId, binding]))
|
|
576
|
+ const boundFroms = new Map(bindingsArr.map((binding) => [binding.fromId, binding]))
|
|
577
|
+ const bindingMaps = [boundTos, boundFroms]
|
|
578
|
+
|
|
579
|
+ // Unique set of shape ids that are going to be reserved
|
|
580
|
+ const reservedShapeIds: string[] = []
|
|
581
|
+
|
|
582
|
+ if (this.session) ids.forEach((id) => reservedShapeIds.push(id))
|
|
583
|
+
|
|
584
|
+ const strongReservedShapeIds = new Set(reservedShapeIds)
|
|
585
|
+
|
|
586
|
+ // Which shape ids have we already visited?
|
|
587
|
+ const visited = new Set<string>()
|
|
588
|
+
|
|
589
|
+ // Time to visit every reserved shape and every related shape and binding.
|
|
590
|
+ while (reservedShapeIds.length > 0) {
|
|
591
|
+ const id = reservedShapeIds.pop()
|
|
592
|
+ if (!id) break
|
|
593
|
+ if (visited.has(id)) continue
|
|
594
|
+
|
|
595
|
+ // Add to set so that we don't process this id a second time
|
|
596
|
+ visited.add(id)
|
|
597
|
+
|
|
598
|
+ // Get the shape and reserve it
|
|
599
|
+ const shape = this.getShape(id)
|
|
600
|
+ reservedShapes[id] = shape
|
|
601
|
+
|
|
602
|
+ if (shape.parentId !== pageId) reservedShapeIds.push(shape.parentId)
|
|
603
|
+
|
|
604
|
+ // If the shape has children, add the shape's children to the list of ids to process
|
|
605
|
+ if (shape.children) reservedShapeIds.push(...shape.children)
|
|
606
|
+
|
|
607
|
+ // If there are binding for this shape, reserve the bindings and
|
|
608
|
+ // add its related shapes to the list of ids to process
|
|
609
|
+ bindingMaps
|
|
610
|
+ .map((map) => map.get(shape.id)!)
|
|
611
|
+ .filter(Boolean)
|
|
612
|
+ .forEach((binding) => {
|
|
613
|
+ reservedBindings[binding.id] = binding
|
|
614
|
+ reservedShapeIds.push(binding.toId, binding.fromId)
|
|
615
|
+ })
|
|
616
|
+ }
|
|
617
|
+
|
|
618
|
+ return { reservedShapes, reservedBindings, strongReservedShapeIds }
|
|
619
|
+ }
|
|
620
|
+
|
538
|
621
|
/**
|
539
|
622
|
* Manually patch a set of shapes.
|
540
|
623
|
* @param shapes An array of shape partials, containing the changes to be made to each shape.
|
|
@@ -545,19 +628,75 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
545
|
628
|
bindings: Record<string, TDBinding>,
|
546
|
629
|
pageId = this.currentPageId
|
547
|
630
|
): this => {
|
|
631
|
+ // This will be called a few times: once by our own change,
|
|
632
|
+ // once by the change to shapes, and once by the change to bindings
|
|
633
|
+
|
|
634
|
+ if (this.justSent) {
|
|
635
|
+ this.justSent = false
|
|
636
|
+ return this
|
|
637
|
+ }
|
|
638
|
+
|
548
|
639
|
this.useStore.setState((current) => {
|
549
|
640
|
const { hoveredId, editingId, bindingId, selectedIds } = current.document.pageStates[pageId]
|
550
|
641
|
|
551
|
|
- const keepShapes: Record<string, TDShape> = {}
|
552
|
|
- const keepBindings: Record<string, TDBinding> = {}
|
|
642
|
+ const coreReservedIds = [...selectedIds]
|
553
|
643
|
|
554
|
|
- if (this.session) {
|
555
|
|
- selectedIds.forEach((id) => (keepShapes[id] = this.getShape(id)))
|
556
|
|
- Object.assign(keepBindings, this.bindings) // ROUGH
|
|
644
|
+ if (editingId) coreReservedIds.push(editingId)
|
|
645
|
+
|
|
646
|
+ const { reservedShapes, reservedBindings, strongReservedShapeIds } = this.getReservedContent(
|
|
647
|
+ coreReservedIds,
|
|
648
|
+ this.currentPageId
|
|
649
|
+ )
|
|
650
|
+
|
|
651
|
+ // Merge in certain changes to reserved shapes
|
|
652
|
+ Object.values(reservedShapes)
|
|
653
|
+ // Don't merge updates to shapes with text (Text or Sticky)
|
|
654
|
+ .filter((reservedShape) => !('text' in reservedShape))
|
|
655
|
+ .forEach((reservedShape) => {
|
|
656
|
+ const incomingShape = shapes[reservedShape.id]
|
|
657
|
+ if (!incomingShape) return
|
|
658
|
+
|
|
659
|
+ // If the shape isn't "strongly reserved", then use the incoming shape;
|
|
660
|
+ // note that this is only if the incoming shape exists! If the shape was
|
|
661
|
+ // deleted in the incoming shapes, then we'll keep out reserved shape.
|
|
662
|
+ // This logic would need more work for arrows, because the incoming shape
|
|
663
|
+ // include a binding change that we'll need to resolve with our reserved bindings.
|
|
664
|
+ if (
|
|
665
|
+ !(
|
|
666
|
+ reservedShape.type === TDShapeType.Arrow ||
|
|
667
|
+ strongReservedShapeIds.has(reservedShape.id)
|
|
668
|
+ )
|
|
669
|
+ ) {
|
|
670
|
+ reservedShapes[reservedShape.id] = incomingShape
|
|
671
|
+ return
|
|
672
|
+ }
|
|
673
|
+
|
|
674
|
+ // Only allow certain merges.
|
|
675
|
+
|
|
676
|
+ // Allow decorations (of an arrow) to be changed
|
|
677
|
+ if ('decorations' in incomingShape && 'decorations' in reservedShape) {
|
|
678
|
+ reservedShape.decorations = incomingShape.decorations
|
|
679
|
+ }
|
|
680
|
+
|
|
681
|
+ // Allow the shape's style to be changed
|
|
682
|
+ reservedShape.style = incomingShape.style
|
|
683
|
+ })
|
|
684
|
+
|
|
685
|
+ // Use the incoming shapes / bindings as comparisons for what
|
|
686
|
+ // will have changed. This is important because we want to restore
|
|
687
|
+ // related shapes that may not have changed on our side, but which
|
|
688
|
+ // were deleted on the server.
|
|
689
|
+ this.prevShapes = shapes
|
|
690
|
+ this.prevBindings = bindings
|
|
691
|
+
|
|
692
|
+ const nextShapes = {
|
|
693
|
+ ...shapes,
|
|
694
|
+ ...reservedShapes,
|
557
|
695
|
}
|
558
|
696
|
|
559
|
|
- if (editingId) {
|
560
|
|
- keepShapes[editingId] = this.getShape(editingId)
|
|
697
|
+ const nextBindings = {
|
|
698
|
+ ...bindings,
|
|
699
|
+ ...reservedBindings,
|
561
|
700
|
}
|
562
|
701
|
|
563
|
702
|
const next: TDSnapshot = {
|
|
@@ -567,29 +706,23 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
567
|
706
|
pages: {
|
568
|
707
|
[pageId]: {
|
569
|
708
|
...current.document.pages[pageId],
|
570
|
|
- shapes: {
|
571
|
|
- ...shapes,
|
572
|
|
- ...keepShapes,
|
573
|
|
- },
|
574
|
|
- bindings: {
|
575
|
|
- ...bindings,
|
576
|
|
- ...keepBindings,
|
577
|
|
- },
|
|
709
|
+ shapes: nextShapes,
|
|
710
|
+ bindings: nextBindings,
|
578
|
711
|
},
|
579
|
712
|
},
|
580
|
713
|
pageStates: {
|
581
|
714
|
...current.document.pageStates,
|
582
|
715
|
[pageId]: {
|
583
|
716
|
...current.document.pageStates[pageId],
|
584
|
|
- selectedIds: selectedIds.filter((id) => shapes[id] !== undefined),
|
|
717
|
+ selectedIds: selectedIds.filter((id) => nextShapes[id] !== undefined),
|
585
|
718
|
hoveredId: hoveredId
|
586
|
|
- ? shapes[hoveredId] === undefined
|
|
719
|
+ ? nextShapes[hoveredId] === undefined
|
587
|
720
|
? undefined
|
588
|
721
|
: hoveredId
|
589
|
722
|
: undefined,
|
590
|
723
|
editingId: editingId,
|
591
|
724
|
bindingId: bindingId
|
592
|
|
- ? bindings[bindingId] === undefined
|
|
725
|
+ ? nextBindings[bindingId] === undefined
|
593
|
726
|
? undefined
|
594
|
727
|
: bindingId
|
595
|
728
|
: undefined,
|
|
@@ -598,9 +731,66 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
598
|
731
|
},
|
599
|
732
|
}
|
600
|
733
|
|
|
734
|
+ // Get bindings related to the changed shapes
|
|
735
|
+ const bindingsToUpdate = TLDR.getRelatedBindings(next, Object.keys(nextShapes), pageId)
|
|
736
|
+
|
|
737
|
+ const page = next.document.pages[pageId]
|
|
738
|
+
|
|
739
|
+ // Update all of the bindings we've just collected
|
|
740
|
+ bindingsToUpdate.forEach((binding) => {
|
|
741
|
+ if (!page.bindings[binding.id]) {
|
|
742
|
+ return
|
|
743
|
+ }
|
|
744
|
+
|
|
745
|
+ const toShape = page.shapes[binding.toId]
|
|
746
|
+ const fromShape = page.shapes[binding.fromId]
|
|
747
|
+
|
|
748
|
+ const toUtils = TLDR.getShapeUtil(toShape)
|
|
749
|
+
|
|
750
|
+ const fromUtils = TLDR.getShapeUtil(fromShape)
|
|
751
|
+
|
|
752
|
+ // We only need to update the binding's "from" shape
|
|
753
|
+ const fromDelta = fromUtils.onBindingChange?.(
|
|
754
|
+ fromShape,
|
|
755
|
+ binding,
|
|
756
|
+ toShape,
|
|
757
|
+ toUtils.getBounds(toShape),
|
|
758
|
+ toUtils.getCenter(toShape)
|
|
759
|
+ )
|
|
760
|
+
|
|
761
|
+ if (fromDelta) {
|
|
762
|
+ const nextShape = {
|
|
763
|
+ ...fromShape,
|
|
764
|
+ ...fromDelta,
|
|
765
|
+ } as TDShape
|
|
766
|
+
|
|
767
|
+ page.shapes[fromShape.id] = nextShape
|
|
768
|
+ }
|
|
769
|
+ })
|
|
770
|
+
|
|
771
|
+ Object.values(nextShapes).forEach((shape) => {
|
|
772
|
+ if (shape.type !== TDShapeType.Group) return
|
|
773
|
+
|
|
774
|
+ const children = shape.children.filter((id) => page.shapes[id] !== undefined)
|
|
775
|
+
|
|
776
|
+ const commonBounds = Utils.getCommonBounds(
|
|
777
|
+ children
|
|
778
|
+ .map((id) => page.shapes[id])
|
|
779
|
+ .filter(Boolean)
|
|
780
|
+ .map((shape) => TLDR.getRotatedBounds(shape))
|
|
781
|
+ )
|
|
782
|
+
|
|
783
|
+ page.shapes[shape.id] = {
|
|
784
|
+ ...shape,
|
|
785
|
+ point: [commonBounds.minX, commonBounds.minY],
|
|
786
|
+ size: [commonBounds.width, commonBounds.height],
|
|
787
|
+ children,
|
|
788
|
+ }
|
|
789
|
+ })
|
|
790
|
+
|
601
|
791
|
this.state.document = next.document
|
602
|
|
- this.prevShapes = next.document.pages[this.currentPageId].shapes
|
603
|
|
- this.prevBindings = next.document.pages[this.currentPageId].bindings
|
|
792
|
+ // this.prevShapes = nextShapes
|
|
793
|
+ // this.prevBindings = nextBindings
|
604
|
794
|
|
605
|
795
|
return next
|
606
|
796
|
}, true)
|