1- import { useEffect , useMemo } from "react" ;
1+ import { useEffect , useMemo , useState } from "react" ;
22import { Canvas , useThree } from "@react-three/fiber" ;
33import { OrbitControls , GizmoHelper , GizmoViewport } from "@react-three/drei" ;
44import * as THREE from "three" ;
@@ -22,6 +22,7 @@ export default function PointCloudViewer({
2222 layers,
2323 background = "#0a0f1a" ,
2424} : Props ) {
25+ const [ sizeMult , setSizeMult ] = useState ( 1 ) ;
2526 const { center, radius } = useMemo ( ( ) => unionBounds ( layers ) , [ layers ] ) ;
2627
2728 const initialPos : [ number , number , number ] = [
@@ -30,40 +31,86 @@ export default function PointCloudViewer({
3031 center . z + radius * 1.4 ,
3132 ] ;
3233
34+ // Apply the multiplier on the way down so PointsLayer doesn't need to know
35+ // about it. This keeps the slider state local to the viewer and means
36+ // pages stay unchanged.
37+ const scaledLayers = useMemo (
38+ ( ) =>
39+ layers . map ( ( l ) => ( {
40+ ...l ,
41+ size : ( l . size ?? 0.05 ) * sizeMult ,
42+ } ) ) ,
43+ [ layers , sizeMult ] ,
44+ ) ;
45+
46+ return (
47+ < div className = "relative h-full w-full" >
48+ < Canvas
49+ style = { { background } }
50+ camera = { {
51+ position : initialPos ,
52+ fov : 50 ,
53+ near : Math . max ( radius * 0.001 , 0.0001 ) ,
54+ far : radius * 50 + 1000 ,
55+ } }
56+ dpr = { [ 1 , 2 ] }
57+ >
58+ < ambientLight intensity = { 0.6 } />
59+ { scaledLayers . map ( ( l , idx ) => (
60+ < PointsLayer key = { idx } layer = { l } />
61+ ) ) }
62+ < axesHelper args = { [ Math . max ( 0.05 , radius * 0.2 ) ] } />
63+ < ViewFrame
64+ cx = { center . x }
65+ cy = { center . y }
66+ cz = { center . z }
67+ radius = { radius }
68+ />
69+ < OrbitControls enableDamping dampingFactor = { 0.08 } makeDefault />
70+ < GizmoHelper alignment = "bottom-right" margin = { [ 64 , 64 ] } >
71+ < GizmoViewport
72+ axisColors = { [ "#ef4444" , "#22c55e" , "#3b82f6" ] }
73+ labelColor = "#e2e8f0"
74+ />
75+ </ GizmoHelper >
76+ </ Canvas >
77+
78+ < PointSizeControl value = { sizeMult } onChange = { setSizeMult } />
79+ </ div >
80+ ) ;
81+ }
82+
83+ function PointSizeControl ( {
84+ value,
85+ onChange,
86+ } : {
87+ value : number ;
88+ onChange : ( v : number ) => void ;
89+ } ) {
3390 return (
34- < Canvas
35- style = { { background } }
36- camera = { {
37- position : initialPos ,
38- fov : 50 ,
39- near : Math . max ( radius * 0.001 , 0.0001 ) ,
40- far : radius * 50 + 1000 ,
41- } }
42- dpr = { [ 1 , 2 ] }
91+ < div
92+ className = "absolute left-3 top-3 z-10 flex items-center gap-2 rounded-md border border-[var(--border)] bg-[color:rgba(10,15,26,0.7)] px-2.5 py-1 backdrop-blur-sm"
93+ // Drag/click on the slider should not start an orbit gesture below.
94+ onPointerDown = { ( e ) => e . stopPropagation ( ) }
95+ onWheel = { ( e ) => e . stopPropagation ( ) }
4396 >
44- < ambientLight intensity = { 0.6 } />
45- { layers . map ( ( l , idx ) => (
46- < PointsLayer key = { idx } layer = { l } />
47- ) ) }
48- < axesHelper args = { [ Math . max ( 0.05 , radius * 0.2 ) ] } />
49- < ViewFrame
50- cx = { center . x }
51- cy = { center . y }
52- cz = { center . z }
53- radius = { radius }
54- />
55- < OrbitControls
56- enableDamping
57- dampingFactor = { 0.08 }
58- makeDefault
97+ < span className = "code-font text-[10px] uppercase tracking-wider text-[var(--mut)]" >
98+ pt
99+ </ span >
100+ < input
101+ type = "range"
102+ min = { 0.25 }
103+ max = { 8 }
104+ step = { 0.05 }
105+ value = { value }
106+ onChange = { ( e ) => onChange ( parseFloat ( e . target . value ) ) }
107+ className = "w-24 sm:w-28"
108+ aria-label = "Point size"
59109 />
60- < GizmoHelper alignment = "bottom-right" margin = { [ 64 , 64 ] } >
61- < GizmoViewport
62- axisColors = { [ "#ef4444" , "#22c55e" , "#3b82f6" ] }
63- labelColor = "#e2e8f0"
64- />
65- </ GizmoHelper >
66- </ Canvas >
110+ < span className = "code-font min-w-[2.6em] text-right text-[10px] text-[var(--text)]" >
111+ { value . toFixed ( 2 ) } ×
112+ </ span >
113+ </ div >
67114 ) ;
68115}
69116
0 commit comments