@@ -43,6 +43,8 @@ interface GraphData {
4343 links : GraphLink [ ]
4444}
4545
46+ const START_NODE_ID = 'case_becker'
47+
4648// ────────────────────────────────────────────
4749// Color palette per node type
4850// ────────────────────────────────────────────
@@ -349,6 +351,17 @@ export function RELIEFKnowledgeGraph3D() {
349351
350352 const graphData = useMemo ( ( ) => buildCaseData ( ) , [ ] )
351353
354+ const getNodeId = useCallback ( ( ref : string | { id : string } ) => {
355+ return typeof ref === 'string' ? ref : ref . id
356+ } , [ ] )
357+
358+ const isLinkConnectedToSelected = useCallback ( ( link : GraphLink ) => {
359+ if ( ! selectedNode ) return false
360+ const src = getNodeId ( link . source )
361+ const tgt = getNodeId ( link . target )
362+ return src === selectedNode . id || tgt === selectedNode . id
363+ } , [ getNodeId , selectedNode ] )
364+
352365 useEffect ( ( ) => {
353366 const updateSize = ( ) => {
354367 if ( containerRef . current ) {
@@ -429,6 +442,8 @@ export function RELIEFKnowledgeGraph3D() {
429442
430443 const nodeThreeObject = useCallback ( ( node : GraphNode ) => {
431444 const group = new THREE . Group ( )
445+ const isSelected = selectedNode ?. id === node . id
446+ const isStartNode = node . id === START_NODE_ID
432447 const color = NODE_COLORS [ node . type ] || '#999'
433448 const size = node . type === 'case' ? 11
434449 : node . type === 'law' ? 10
@@ -449,6 +464,39 @@ export function RELIEFKnowledgeGraph3D() {
449464 const sphere = new THREE . Mesh ( geometry , material )
450465 group . add ( sphere )
451466
467+ if ( isStartNode ) {
468+ const startGlowGeo = new THREE . SphereGeometry ( size * 1.75 , 28 , 28 )
469+ const startGlowMat = new THREE . MeshPhongMaterial ( {
470+ color : new THREE . Color ( '#facc15' ) ,
471+ transparent : true ,
472+ opacity : 0.22 ,
473+ shininess : 90 ,
474+ } )
475+ group . add ( new THREE . Mesh ( startGlowGeo , startGlowMat ) )
476+
477+ const startRingGeo = new THREE . TorusGeometry ( size * 1.45 , 0.9 , 16 , 64 )
478+ const startRingMat = new THREE . MeshBasicMaterial ( { color : new THREE . Color ( '#fde047' ) } )
479+ const startRing = new THREE . Mesh ( startRingGeo , startRingMat )
480+ startRing . rotation . x = Math . PI / 2
481+ group . add ( startRing )
482+ }
483+
484+ if ( isSelected ) {
485+ const selectedRingGeo = new THREE . TorusGeometry ( size * 1.65 , 0.8 , 16 , 64 )
486+ const selectedRingMat = new THREE . MeshBasicMaterial ( { color : new THREE . Color ( '#ffffff' ) } )
487+ const selectedRing = new THREE . Mesh ( selectedRingGeo , selectedRingMat )
488+ selectedRing . rotation . x = Math . PI / 2
489+ group . add ( selectedRing )
490+
491+ const selectedHaloGeo = new THREE . SphereGeometry ( size * 1.95 , 24 , 24 )
492+ const selectedHaloMat = new THREE . MeshPhongMaterial ( {
493+ color : new THREE . Color ( '#ffffff' ) ,
494+ transparent : true ,
495+ opacity : 0.12 ,
496+ } )
497+ group . add ( new THREE . Mesh ( selectedHaloGeo , selectedHaloMat ) )
498+ }
499+
452500 // Glow effect for case, law, and ai nodes
453501 if ( node . type === 'law' || node . type === 'case' || node . type === 'ai' ) {
454502 const glowGeo = new THREE . SphereGeometry ( size * 1.4 , 24 , 24 )
@@ -462,17 +510,41 @@ export function RELIEFKnowledgeGraph3D() {
462510
463511 const label = new SpriteText ( node . label ) as any
464512 label . color = '#e2e8f0'
465- label . textHeight = node . type === 'case' ? 5.5 : node . type === 'law' ? 5 : node . type === 'ai' ? 4.5 : node . type === 'person' ? 4 : 3. 5
466- label . backgroundColor = 'rgba(15, 23, 42, 0.75 )'
467- label . padding = [ 2 , 4 ]
513+ label . textHeight = node . type === 'case' ? 7 : node . type === 'law' ? 6. 5 : node . type === 'ai' ? 6 : node . type === 'person' ? 5.5 : 5
514+ label . backgroundColor = isSelected ? 'rgba(15, 23, 42, 0.95)' : 'rgba(15, 23, 42, 0.88 )'
515+ label . padding = [ 3.5 , 6 ]
468516 label . borderRadius = 3
469- label . position . y = - ( size + 6 )
517+ label . position . y = - ( size + 9 )
518+ if ( label . material ) {
519+ label . material . depthTest = false
520+ label . material . depthWrite = false
521+ }
522+ label . renderOrder = 1000
470523 group . add ( label )
471524
525+ if ( isStartNode ) {
526+ const startTag = new SpriteText ( 'START' ) as any
527+ startTag . color = '#0f172a'
528+ startTag . textHeight = 4.2
529+ startTag . backgroundColor = 'rgba(250, 204, 21, 0.98)'
530+ startTag . padding = [ 2.5 , 5 ]
531+ startTag . borderRadius = 3
532+ startTag . position . y = size + 8
533+ if ( startTag . material ) {
534+ startTag . material . depthTest = false
535+ startTag . material . depthWrite = false
536+ }
537+ startTag . renderOrder = 1001
538+ group . add ( startTag )
539+ }
540+
472541 return group
473- } , [ ] )
542+ } , [ selectedNode ] )
474543
475544 const linkColor = useCallback ( ( link : GraphLink ) => {
545+ if ( selectedNode && ! isLinkConnectedToSelected ( link ) ) {
546+ return 'rgba(100, 116, 139, 0.08)'
547+ }
476548 switch ( link . type ) {
477549 case 'SR_CONTAINS' : return 'rgba(59, 130, 246, 0.5)'
478550 case 'SR_REALIZED_BY' : return 'rgba(16, 185, 129, 0.5)'
@@ -496,14 +568,51 @@ export function RELIEFKnowledgeGraph3D() {
496568 case 'SR_APPLIES_TO' : return 'rgba(6, 182, 212, 0.4)'
497569 default : return 'rgba(148, 163, 184, 0.3)'
498570 }
499- } , [ ] )
571+ } , [ isLinkConnectedToSelected , selectedNode ] )
572+
573+ const linkWidth = useCallback ( ( link : GraphLink ) => {
574+ if ( ! selectedNode ) return 1.8
575+ return isLinkConnectedToSelected ( link ) ? 3.8 : 0.5
576+ } , [ isLinkConnectedToSelected , selectedNode ] )
500577
501578 const linkDirectionalParticles = useCallback ( ( link : GraphLink ) => {
579+ if ( selectedNode && ! isLinkConnectedToSelected ( link ) ) return 0
502580 return link . type === 'SR_SEQUENCE' ? 3
503581 : link . type === 'SR_LOEST' ? 3
504582 : link . type === 'SR_FEHLT' ? 2
505583 : link . type === 'SR_HAT_PROBLEM' ? 2
506584 : 1
585+ } , [ isLinkConnectedToSelected , selectedNode ] )
586+
587+ const linkThreeObject = useCallback ( ( link : GraphLink ) => {
588+ const showLabel = ! selectedNode || isLinkConnectedToSelected ( link )
589+ if ( ! showLabel ) return null
590+
591+ const relationText = link . description && link . description . trim ( ) . length > 0
592+ ? link . description
593+ : link . type . replace ( 'SR_' , '' ) . replace ( / _ / g, ' ' )
594+
595+ const label = new SpriteText ( relationText ) as any
596+ label . color = '#f8fafc'
597+ label . textHeight = selectedNode ? 3.2 : 2.4
598+ label . backgroundColor = selectedNode ? 'rgba(15, 23, 42, 0.96)' : 'rgba(15, 23, 42, 0.72)'
599+ label . padding = selectedNode ? [ 1.8 , 3.6 ] : [ 1.2 , 2.6 ]
600+ label . borderRadius = 2
601+ if ( label . material ) {
602+ label . material . depthTest = false
603+ label . material . depthWrite = false
604+ }
605+ label . renderOrder = 1000
606+ return label
607+ } , [ isLinkConnectedToSelected , selectedNode ] )
608+
609+ const linkPositionUpdate = useCallback ( ( sprite : THREE . Object3D , coords : { start : { x : number ; y : number ; z : number } ; end : { x : number ; y : number ; z : number } } ) => {
610+ const middlePos = {
611+ x : coords . start . x + ( coords . end . x - coords . start . x ) * 0.5 ,
612+ y : coords . start . y + ( coords . end . y - coords . start . y ) * 0.5 ,
613+ z : coords . start . z + ( coords . end . z - coords . start . z ) * 0.5 ,
614+ }
615+ Object . assign ( sprite . position , middlePos )
507616 } , [ ] )
508617
509618 // Build adjacency for detail panel
@@ -555,27 +664,27 @@ export function RELIEFKnowledgeGraph3D() {
555664 </ div >
556665
557666 { /* Legend */ }
558- < div className = "absolute bottom-3 left-3 z-20 bg-slate-900/90 backdrop-blur-sm rounded-lg p-3 border border-slate-700" >
559- < div className = "text-xs text-slate-400 font-semibold mb-2" > Knotentypen</ div >
667+ < div className = "absolute bottom-3 left-3 z-20 bg-slate-900/90 backdrop-blur-sm rounded-lg p-4 border border-slate-700" >
668+ < div className = "text-sm text-slate-200 font-semibold mb-2" > Knotentypen</ div >
560669 < div className = "grid grid-cols-2 gap-x-4 gap-y-1" >
561670 { ( Object . keys ( NODE_LABELS ) as NodeType [ ] ) . map ( type => (
562671 < div key = { type } className = "flex items-center gap-1.5" >
563672 < span
564- className = "w-2.5 h-2.5 rounded-full flex-shrink-0"
673+ className = "w-3 h-3 rounded-full flex-shrink-0"
565674 style = { { backgroundColor : NODE_COLORS [ type ] } }
566675 />
567- < span className = "text-[10px] text-slate-300 whitespace-nowrap" > { NODE_LABELS [ type ] } </ span >
676+ < span className = "text-xs text-slate-200 whitespace-nowrap" > { NODE_LABELS [ type ] } </ span >
568677 </ div >
569678 ) ) }
570679 </ div >
571680 </ div >
572681
573682 { /* Stats */ }
574- < div className = "absolute top-3 left-3 z-20 bg-slate-900/90 backdrop-blur-sm rounded-lg px-3 py-2 border border-slate-700" >
575- < div className = "text-[10px] text-slate-400 " >
683+ < div className = "absolute top-3 left-3 z-20 bg-slate-900/90 backdrop-blur-sm rounded-lg px-4 py-3 border border-slate-700" >
684+ < div className = "text-sm text-slate-200 " >
576685 { graphData . nodes . length } Knoten · { graphData . links . length } Beziehungen
577686 </ div >
578- < div className = "text-[10px] text-blue-400 font-medium " > RELIEF E-AKTE Knowledge Graph</ div >
687+ < div className = "text-sm text-blue-300 font-semibold " > RELIEF E-AKTE Knowledge Graph</ div >
579688 </ div >
580689
581690 { /* Graph */ }
@@ -587,9 +696,11 @@ export function RELIEFKnowledgeGraph3D() {
587696 backgroundColor = "#0f172a"
588697 nodeThreeObject = { nodeThreeObject }
589698 onNodeClick = { handleNodeClick }
699+ nodeThreeObjectExtend = { true }
590700 linkColor = { linkColor }
591- linkWidth = { 1.5 }
592- linkOpacity = { 0.7 }
701+ linkWidth = { linkWidth }
702+ linkThreeObject = { linkThreeObject }
703+ linkPositionUpdate = { linkPositionUpdate }
593704 linkDirectionalArrowLength = { 4 }
594705 linkDirectionalArrowRelPos = { 0.85 }
595706 linkDirectionalParticles = { linkDirectionalParticles }
0 commit comments