1+ import { Text } from '@react-three/drei'
2+ import { useThree , useFrame } from '@react-three/fiber'
3+ import { useState , useMemo } from 'react'
4+
5+ interface ViewportBounds {
6+ left : number ;
7+ right : number ;
8+ top : number ;
9+ bottom : number ;
10+ }
11+
12+ interface FixedTicksProps {
13+ color ?: string ;
14+ tickSize ?: number ;
15+ fontSize ?: number ;
16+ showGrid ?: boolean ;
17+ gridOpacity ?: number ;
18+ }
19+
20+
21+ export function FixedTicks ( {
22+ color = 'white' ,
23+ tickSize = 4 ,
24+ fontSize = 12 ,
25+ showGrid = true ,
26+ gridOpacity = 0.1
27+ } : FixedTicksProps ) {
28+ const { camera, viewport, size } = useThree ( )
29+ const [ bounds , setBounds ] = useState < ViewportBounds > ( { left : 0 , right : 0 , top : 0 , bottom : 0 } )
30+ const [ zoom , setZoom ] = useState ( camera . zoom )
31+
32+ const sizes = useMemo ( ( ) => {
33+ // Convert from pixels to scene units
34+ const pixelsPerUnit = size . height / ( viewport . height * camera . zoom )
35+ return {
36+ tickSize : tickSize / pixelsPerUnit ,
37+ fontSize : fontSize / pixelsPerUnit ,
38+ labelOffset : tickSize / pixelsPerUnit
39+ }
40+ } , [ size . height , viewport . height , camera . zoom , tickSize , fontSize ] )
41+
42+ // Update bounds when camera moves
43+ // TODO: update bounds when camera zooms
44+ useFrame ( ( ) => {
45+ if ( camera . zoom !== zoom ) {
46+ setZoom ( camera . zoom ) // this is not working properly
47+ }
48+ const worldWidth = viewport . width / camera . zoom
49+ const worldHeight = viewport . height / camera . zoom
50+
51+ const newBounds = {
52+ left : - worldWidth / 2 + camera . position . x ,
53+ right : worldWidth / 2 + camera . position . x ,
54+ top : worldHeight / 2 + camera . position . y ,
55+ bottom : - worldHeight / 2 + camera . position . y
56+ }
57+
58+ setBounds ( newBounds )
59+ } )
60+
61+ return (
62+ < group >
63+ { /* Grid Lines */ }
64+ { showGrid && (
65+ < >
66+ { /* Vertical grid lines */ }
67+ { Array . from ( { length : 10 } , ( _ , i ) => {
68+ if ( i === 0 || i === 9 ) return null ; // Skip edges
69+ const x = bounds . left + ( bounds . right - bounds . left ) * ( i / 9 )
70+ return (
71+ < line key = { `vgrid-${ i } ` } >
72+ < bufferGeometry >
73+ < float32BufferAttribute
74+ attach = "attributes-position"
75+ args = { [ new Float32Array ( [
76+ x , bounds . top , 0 ,
77+ x , bounds . bottom , 0
78+ ] ) , 3 ] }
79+ />
80+ </ bufferGeometry >
81+ < lineDashedMaterial
82+ color = { color }
83+ opacity = { gridOpacity }
84+ transparent
85+ dashSize = { 0.5 }
86+ gapSize = { 0.5 }
87+ />
88+ </ line >
89+ )
90+ } ) }
91+
92+ { /* Horizontal grid lines */ }
93+ { Array . from ( { length : 8 } , ( _ , i ) => {
94+ if ( i === 0 || i === 7 ) return null ; // Skip edges
95+ const y = bounds . bottom + ( bounds . top - bounds . bottom ) * ( i / 7 )
96+ return (
97+ < line key = { `hgrid-${ i } ` } >
98+ < bufferGeometry >
99+ < float32BufferAttribute
100+ attach = "attributes-position"
101+ args = { [ new Float32Array ( [
102+ bounds . left , y , 0 ,
103+ bounds . right , y , 0
104+ ] ) , 3 ] }
105+ />
106+ </ bufferGeometry >
107+ < lineDashedMaterial
108+ color = { color }
109+ opacity = { gridOpacity }
110+ transparent
111+ dashSize = { 0.5 }
112+ gapSize = { 0.5 }
113+ />
114+ </ line >
115+ )
116+ } ) }
117+ </ >
118+ ) }
119+ { /* Top Edge Ticks */ }
120+ { Array . from ( { length : 10 } , ( _ , i ) => {
121+ const x = bounds . left + ( bounds . right - bounds . left ) * ( i / 9 )
122+ return (
123+ < group key = { `top-tick-${ i } ` } position = { [ x , bounds . top , 0 ] } >
124+ < line >
125+ < bufferGeometry >
126+ < float32BufferAttribute
127+ attach = "attributes-position"
128+ args = { [ new Float32Array ( [ 0 , 0 , 0 , 0 , - sizes . tickSize , 0 ] ) , 3 ] }
129+ />
130+ </ bufferGeometry >
131+ < lineBasicMaterial color = { color } />
132+ </ line >
133+
134+ { /* Only show labels for non-edge ticks */ }
135+ { i !== 0 && i !== 9 && (
136+ < Text
137+ position = { [ 0 , sizes . tickSize / 4 - sizes . labelOffset , 0 ] }
138+ fontSize = { sizes . fontSize }
139+ color = { color }
140+ anchorX = "center"
141+ anchorY = "top"
142+ >
143+ { x . toFixed ( 1 ) }
144+ { /* do x.toString() when is not a number */ }
145+ </ Text >
146+ ) }
147+ </ group >
148+ )
149+ } ) }
150+
151+ { /* Right Edge Ticks */ }
152+ { Array . from ( { length : 6 } , ( _ , i ) => {
153+ const y = bounds . bottom + ( bounds . top - bounds . bottom ) * ( i / 6 )
154+ return (
155+ < group key = { `right-tick-${ i } ` } position = { [ bounds . right , y , 0 ] } >
156+ < line >
157+ < bufferGeometry >
158+ < float32BufferAttribute
159+ attach = "attributes-position"
160+ args = { [ new Float32Array ( [ 0 , 0 , 0 , - sizes . tickSize , 0 , 0 ] ) , 3 ] }
161+ />
162+ </ bufferGeometry >
163+ < lineBasicMaterial color = { color } />
164+ </ line >
165+ { /* Only show labels for non-edge ticks */ }
166+ { i !== 0 && i !== 6 && (
167+ < Text
168+ position = { [ - sizes . tickSize - sizes . labelOffset , 0 , 0 ] }
169+ fontSize = { sizes . fontSize }
170+ color = { color }
171+ anchorX = "right"
172+ anchorY = "middle"
173+ >
174+ { y . toFixed ( 1 ) }
175+ </ Text >
176+ ) }
177+ </ group >
178+ )
179+ } ) }
180+ </ group >
181+ )
182+ }
0 commit comments