@@ -537,14 +537,30 @@ export const createAllRelationShapeUtils = (
537537 const relations = Object . values ( discourseContext . relations ) . flat ( ) ;
538538 const relation = relations . find ( ( r ) => r . id === arrow . type ) ;
539539 if ( ! relation ) return ;
540- const possibleTargets = discourseContext . relations [ relation . label ]
541- . filter ( ( r ) => r . source === relation . source )
542- . map ( ( r ) => r . destination ) ;
543540
544- if ( ! possibleTargets . includes ( target . type ) ) {
545- const uniqueTargets = [ ...new Set ( possibleTargets ) ] ;
546- const uniqueTargetTexts = uniqueTargets . map (
547- ( t ) => discourseContext . nodes [ t ] . text ,
541+ const sourceNodeType = source . type ;
542+ const targetNodeType = target . type ;
543+
544+ const { isDirect, isReverse } = this . checkConnectionType (
545+ relation ,
546+ sourceNodeType ,
547+ targetNodeType ,
548+ ) ;
549+
550+ if ( ! isDirect && ! isReverse ) {
551+ const possibleTargets = discourseContext . relations [ relation . label ]
552+ . filter ( ( r ) => r . source === relation . source )
553+ . map ( ( r ) => r . destination ) ;
554+ const possibleReverseTargets = discourseContext . relations [
555+ relation . label
556+ ]
557+ . filter ( ( r ) => r . destination === relation . source )
558+ . map ( ( r ) => r . source ) ;
559+ const allPossibleTargets = [
560+ ...new Set ( [ ...possibleTargets , ...possibleReverseTargets ] ) ,
561+ ] ;
562+ const uniqueTargetTexts = allPossibleTargets . map (
563+ ( t ) => discourseContext . nodes [ t ] ?. text || t ,
548564 ) ;
549565 return deleteAndWarn (
550566 `Target node must be of type ${ uniqueTargetTexts . join ( ", " ) } ` ,
@@ -553,6 +569,7 @@ export const createAllRelationShapeUtils = (
553569 if ( arrow . type !== target . type ) {
554570 editor . updateShapes ( [ { id : arrow . id , type : target . type } ] ) ;
555571 }
572+ arrow = editor . getShape ( arrow . id ) as DiscourseRelationShape ;
556573 if ( getSetting ( "use-reified-relations" ) ) {
557574 const sourceAsDNS = asDiscourseNodeShape ( source , editor ) ;
558575 const targetAsDNS = asDiscourseNodeShape ( target , editor ) ;
@@ -572,8 +589,9 @@ export const createAllRelationShapeUtils = (
572589 } ) . catch ( ( ) => undefined ) ;
573590 }
574591 } else {
575- const { triples, label : relationLabel } = relation ;
576- const isOriginal = arrow . props . text === relationLabel ;
592+ const { triples } = relation ;
593+ const isOriginal = isDirect && ! isReverse ;
594+
577595 const newTriples = triples
578596 . map ( ( t ) => {
579597 if ( / i s a / i. test ( t [ 1 ] ) ) {
@@ -756,6 +774,52 @@ export const createAllRelationShapeUtils = (
756774 return update ;
757775 }
758776
777+ // Validate target node type compatibility before creating binding
778+ if (
779+ target . type !== "arrow" &&
780+ otherBinding &&
781+ target . id !== otherBinding . toId &&
782+ ( ! currentBinding || target . id !== currentBinding . toId )
783+ ) {
784+ const sourceNodeId = otherBinding . toId ;
785+ const sourceNode = this . editor . getShape ( sourceNodeId ) ;
786+ const targetNodeType = target . type ;
787+ const sourceNodeType = sourceNode ?. type ;
788+
789+ if ( sourceNodeType && targetNodeType && shape . type ) {
790+ const isValidConnection = this . isValidNodeConnection (
791+ sourceNodeType ,
792+ targetNodeType ,
793+ shape . type ,
794+ ) ;
795+
796+ if ( ! isValidConnection ) {
797+ const sourceNodeTypeText =
798+ discourseContext . nodes [ sourceNodeType ] ?. text || sourceNodeType ;
799+ const targetNodeTypeText =
800+ discourseContext . nodes [ targetNodeType ] ?. text || targetNodeType ;
801+ const relations = Object . values (
802+ discourseContext . relations ,
803+ ) . flat ( ) ;
804+ const relation = relations . find ( ( r ) => r . id === shape . type ) ;
805+ const relationLabel = relation ?. label || shape . type ;
806+
807+ const errorMessage = `Cannot connect "${ sourceNodeTypeText } " to "${ targetNodeTypeText } " with "${ relationLabel } " relation` ;
808+ dispatchToastEvent ( {
809+ id : `tldraw-invalid-connection-${ shape . id } ` ,
810+ title : "Invalid Connection" ,
811+ description : errorMessage ,
812+ severity : "error" ,
813+ } ) ;
814+
815+ removeArrowBinding ( this . editor , shape , handleId ) ;
816+ update . props ! [ handleId ] = { x : handle . x , y : handle . y } ;
817+ this . editor . deleteShapes ( [ shape . id ] ) ;
818+ return update ;
819+ }
820+ }
821+ }
822+
759823 // we've got a target! the handle is being dragged over a shape, bind to it
760824
761825 const targetGeometry = this . editor . getShapeGeometry ( target ) ;
@@ -832,6 +896,37 @@ export const createAllRelationShapeUtils = (
832896 this . editor . setHintingShapes ( [ target . id ] ) ;
833897
834898 const newBindings = getArrowBindings ( this . editor , shape ) ;
899+
900+ // Check if both ends are bound and determine the correct text based on direction
901+ if ( newBindings . start && newBindings . end ) {
902+ const relations = Object . values ( discourseContext . relations ) . flat ( ) ;
903+ const relation = relations . find ( ( r ) => r . id === shape . type ) ;
904+
905+ if ( relation ) {
906+ const startNode = this . editor . getShape ( newBindings . start . toId ) ;
907+ const endNode = this . editor . getShape ( newBindings . end . toId ) ;
908+
909+ if ( startNode && endNode ) {
910+ const startNodeType = startNode . type ;
911+ const endNodeType = endNode . type ;
912+
913+ const { isDirect, isReverse } = this . checkConnectionType (
914+ relation ,
915+ startNodeType ,
916+ endNodeType ,
917+ ) ;
918+
919+ const newText =
920+ isReverse && ! isDirect ? relation . complement : relation . label ;
921+
922+ if ( shape . props . text !== newText ) {
923+ update . props = update . props || { } ;
924+ update . props . text = newText ;
925+ }
926+ }
927+ }
928+ }
929+
835930 if (
836931 newBindings . start &&
837932 newBindings . end &&
@@ -1454,6 +1549,40 @@ export class BaseDiscourseRelationUtil extends ShapeUtil<DiscourseRelationShape>
14541549 ] ;
14551550 }
14561551
1552+ checkConnectionType (
1553+ relation : { source : string ; destination : string } ,
1554+ sourceNodeType : string ,
1555+ targetNodeType : string ,
1556+ ) : { isDirect : boolean ; isReverse : boolean } {
1557+ const isDirect =
1558+ sourceNodeType === relation . source &&
1559+ targetNodeType === relation . destination ;
1560+
1561+ const isReverse =
1562+ sourceNodeType === relation . destination &&
1563+ targetNodeType === relation . source ;
1564+
1565+ return { isDirect, isReverse } ;
1566+ }
1567+
1568+ isValidNodeConnection (
1569+ sourceNodeType : string ,
1570+ targetNodeType : string ,
1571+ relationId : string ,
1572+ ) : boolean {
1573+ const relations = Object . values ( discourseContext . relations ) . flat ( ) ;
1574+ const relation = relations . find ( ( r ) => r . id === relationId ) ;
1575+ if ( ! relation ) return false ;
1576+
1577+ const { isDirect, isReverse } = this . checkConnectionType (
1578+ relation ,
1579+ sourceNodeType ,
1580+ targetNodeType ,
1581+ ) ;
1582+
1583+ return isDirect || isReverse ;
1584+ }
1585+
14571586 component ( shape : DiscourseRelationShape ) {
14581587 // eslint-disable-next-line react-hooks/rules-of-hooks
14591588 // const theme = useDefaultColorTheme();
0 commit comments