Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PlaneViewer responsive to rotation and different screen sizes #22

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 29 additions & 10 deletions App.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {hdf5Loader} from './MincLoader';
import {Viewer} from './Viewer';
import {Login} from './Login';
import * as SecureStore from 'expo-secure-store';
import * as ScreenOrientation from 'expo-screen-orientation';

async function getValueFor(key) {
return await SecureStore.getItemAsync(key);
Expand All @@ -18,6 +19,24 @@ export default function App() {
const [rawData, setRawData] = useState(null);
const [headerData, setHeaderData] = useState(null);
const [shouldScroll, setShouldScroll] = useState(true);
const [screenOrientation, setScreenOrientation] = useState(0);

useEffect(() => {
checkOrientation();
const subscription = ScreenOrientation.addOrientationChangeListener(
handleOrientationChange
);
return () => {
ScreenOrientation.removeOrientationChangeListeners(subscription);
};
}, []);
const checkOrientation = async () => {
const orientation = await ScreenOrientation.getOrientationAsync();
setScreenOrientation(orientation);
};
const handleOrientationChange = (o) => {
setScreenOrientation(o.orientationInfo.orientation);
};

useEffect(() => {
if (token)
Expand Down Expand Up @@ -64,11 +83,7 @@ export default function App() {
return (
// ScrollView
<ScrollView scrollEnabled={shouldScroll} style={styles.container}>
<GestureHandlerRootView styles={{flex: 1}}>
<Text></Text>
<Text> </Text>
<Text> </Text>
<Text> </Text>
<GestureHandlerRootView>
<View >
<Text style={{
textAlign: "center", fontSize: 25
Expand All @@ -83,14 +98,17 @@ export default function App() {
By Dave MacFarlane, Camille Beaudoin, and Jefferson Casimir
</Text>
</View>
<Text> </Text>
<Viewer rawData={rawData} headers={headerData}
<Viewer
rawData={rawData}
headers={headerData}
onGestureStart={ () => setShouldScroll(false) }
onGestureEnd={ () => setShouldScroll(true) }
screenOrientation={screenOrientation}
/>
<View style={{
paddingBottom: 40,
}}/>
<StatusBar style="auto" />
<Text></Text>
<Text></Text>
</GestureHandlerRootView>
</ScrollView>
);
Expand All @@ -99,7 +117,8 @@ export default function App() {

const styles = StyleSheet.create({
container: {
flex: 1,
display: 'flex',
backgroundColor: '#fff',
marginTop: 40,
},
});
72 changes: 46 additions & 26 deletions PlaneViewer.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React, {useMemo, useRef, useState, useCallback, useEffect} from 'react';
import { Gesture, GestureDetector} from 'react-native-gesture-handler';
import { Pressable, View, Text, PanResponder } from 'react-native';
import { GLView } from 'expo-gl';
import Expo2DContext from "expo-2d-context";
import { SegmentSlider } from './SegmentSlider';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import {Text, View} from 'react-native';
import {GLView} from 'expo-gl';
import {SegmentSlider} from './SegmentSlider';

function getScreenPlanes(plane, headers) {
switch (plane) {
Expand Down Expand Up @@ -35,9 +34,8 @@ function loadIntensityTexture(gl, headers, data) {
gl.bindTexture(gl.TEXTURE_3D, texture);

const values = new Uint8Array(data.floats.length);
for(i = 0; i < data.floats.length; i++) {
const val = ((data.floats[i] -data.min) / (data.max -data.min)) * 255;
values[i] = val;
for(let i = 0; i < data.floats.length; i++) {
values[i] = ((data.floats[i] - data.min) / (data.max - data.min)) * 255;
}

// Set the parameters so we can render any size image.
Expand Down Expand Up @@ -68,19 +66,36 @@ function useGLCanvas(viewWidth, viewHeight, headers, data, plane) {
const [offsetPos, setOffsetPos] = useState({x: 0, y: 0});
const [zoomUniform, setZoomUniform] = useState(null);
const [viewOffsetUniform, setViewOffsetUniform] = useState(null);
const [pixelsPerUnit, setPixelsPerUnit] = useState(0);
const draw = useCallback(() => {
if (!gl) {
console.warn('No gl in draw');
return;
}
}
// We draw the 1 square which consists of 2 triangles
// covering the whole viewport. The program set up
// a_position in onContextCreate
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.flush();
gl.endFrameEXP();
}, [gl]);
const setZoomFactorCB = useCallback( (newZoom) => {

// Force re-render on viewWidth change
useEffect(() => {
if (!gl)
return;
gl.drawingBufferWidth = Math.ceil(viewWidth * pixelsPerUnit);
gl.drawingBufferHeight = Math.ceil(viewHeight * pixelsPerUnit);
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
// TODO: Replace with better alternative
setTimeout(() => {
gl.clear(gl.COLOR_BUFFER_BIT);
requestAnimationFrame(draw);
gl.endFrameEXP();
}, 100);
}, [viewWidth]);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this also depends on gl and draw?


const setZoomFactorCB = useCallback( (newZoom) => {
if (!gl) {
console.log('No gl for setZoomFactorCB');
return;
Expand Down Expand Up @@ -129,7 +144,7 @@ function useGLCanvas(viewWidth, viewHeight, headers, data, plane) {
gl.compileShader(shader);
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!success) {
msg = gl.getShaderInfoLog(shader);
const msg = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error("Could not compile shader:" + msg);
}
Expand All @@ -138,8 +153,8 @@ function useGLCanvas(viewWidth, viewHeight, headers, data, plane) {

const linkProgram = (vertex, fragment) => {
const program = gl.createProgram();
gl.attachShader(program, vert);
gl.attachShader(program, frag);
gl.attachShader(program, vertex);
gl.attachShader(program, fragment);
gl.linkProgram(program);
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!success) {
Expand All @@ -151,8 +166,8 @@ function useGLCanvas(viewWidth, viewHeight, headers, data, plane) {
};

gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
setPixelsPerUnit(gl.drawingBufferWidth / viewWidth);
gl.clearColor(0, 1, 0, 1);

// Create the vertex shader (position & size)
const vert = compileShader(gl.VERTEX_SHADER,
`
Expand Down Expand Up @@ -342,7 +357,6 @@ function useGLCanvas(viewWidth, viewHeight, headers, data, plane) {
0,
);


// Calculate display uniforms
const spaceSizeUniformLocation = gl.getUniformLocation(program, "u_spacesize");
if (spaceSizeUniformLocation) {
Expand Down Expand Up @@ -438,10 +452,14 @@ function useGLCanvas(viewWidth, viewHeight, headers, data, plane) {
gl.uniform2f(crosshairsUniform, crosshairs.x, crosshairs.y);
draw();
}, [gl, draw, crosshairsUniform]);
const canvas = useRef(<GLView collapsable={false} style={{ width: viewWidth, height: viewHeight, borderWidth: 2, borderColor: 'green' }}
onContextCreate={onContextCreate} />).current;

return {

const canvas = useMemo(() => {
return <GLView collapsable={false} style={{ width: viewWidth, height: viewHeight, borderWidth: 2, borderColor: 'green' }}
onContextCreate={onContextCreate}/>;
}, [viewWidth]);

return {
setSliceNum: setSliceNum,
setCrosshairs: setCrosshairs,
canvas: canvas,
Expand Down Expand Up @@ -486,15 +504,12 @@ function calculateTouchPos(plane, headers, sliceNo, zoomFactor, canvasSize, x, y
}
}

export function PlaneViewer({headers, data, plane, SliceNo, label, onSliceChange, crosshairs, setPosition, onGestureStart, onGestureEnd}) {
// FIXME: Width and height shouldn't be fixed values
const canvasSize = {x: 350, y: 400};
export function PlaneViewer({headers, data, plane, SliceNo, label, onSliceChange, crosshairs, setPosition, onGestureStart, onGestureEnd, viewWidth}) {
const canvasSize = {x: viewWidth, y: viewWidth * 8 / 7};
const {setSliceNum, canvas, setCrosshairs, zoomFactor, scaleZoomFactor, setZoomFactor, panViewport, setViewOffset} = useGLCanvas(canvasSize.x, canvasSize.y, headers, data, plane);
const onSliderChange = useCallback( (newValue) => {
onSliceChange(newValue);

setSliceNum(newValue);

}, [setSliceNum]);

const gestures = useMemo( () => {
Expand Down Expand Up @@ -576,14 +591,19 @@ export function PlaneViewer({headers, data, plane, SliceNo, label, onSliceChange
throw new Error("Invalid plane");
}
return (

<View style={{flex: 1, justifyContent: 'center', flexDirection: 'column', alignItems: 'center', margin: 20,}}>
<View style={{
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
}}>
<Text>{label} (Size: {sliderMax})</Text>
<GestureDetector gesture={gestures}>{canvas}</GestureDetector>
<SegmentSlider
val={SliceNo}
max={sliderMax}
onSliderChange={onSliderChange}
viewWidth={viewWidth}
/>
</View>
);
Expand Down
5 changes: 2 additions & 3 deletions SegmentSlider.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React from 'react';
import { View, Text, Dimensions } from 'react-native';
import { View, Text } from 'react-native';
import Slider from '@react-native-community/slider';

export function SegmentSlider(props) {
// Calculate the width based on the screen dimensions
const { width } = Dimensions.get('window');
const sliderWidth = width * 0.9; // 70% of screen width
const sliderWidth = props.viewWidth * 1.1;

const label = props.label ? <View><Text>{props.label}</Text></View> : null;
return (
Expand Down
30 changes: 22 additions & 8 deletions Viewer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import React, {useState, useEffect} from 'react';
import { Pressable, View, Text } from 'react-native';
import { GLView } from 'expo-gl';
import Expo2DContext from "expo-2d-context";
import { SegmentSlider } from './SegmentSlider';
import { Dimensions, View, Text } from 'react-native';
import {PlaneViewer} from './PlaneViewer';


Expand Down Expand Up @@ -31,9 +28,18 @@ function preprocess(rawdata) {
}
}

export function Viewer({rawData, headers, onGestureStart, onGestureEnd}) {
export function Viewer({rawData, headers, onGestureStart, onGestureEnd, screenOrientation}) {
const [data, setData] = useState(null);
const [coord, setCoord] = useState({x: 0, y: 0, z: 0});
const [viewWidth, setViewWidth] = useState(0);

useEffect(() => {
setViewWidth(screenOrientation < 3
? Dimensions.get('window').width * 0.85 // Portrait
: Dimensions.get('window').width * 0.3 // Landscape
);
}, [screenOrientation]);

useEffect( () => {
if (!rawData) {
return;
Expand Down Expand Up @@ -63,7 +69,12 @@ export function Viewer({rawData, headers, onGestureStart, onGestureEnd}) {
// This was reached by trial and error with 1 sample file and
// is not reliable
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<View style={{
display: 'flex',
// alignItems: 'center',
justifyContent: 'space-evenly',
flexDirection: screenOrientation < 3 ? 'column' : 'row',
}}>
<PlaneViewer data={data} plane='z' headers={headers}
SliceNo={coord.z}
label="Sagittal"
Expand All @@ -78,6 +89,7 @@ export function Viewer({rawData, headers, onGestureStart, onGestureEnd}) {
crosshairs={ {x: coord.x, y: coord.y} }
onGestureStart={onGestureStart}
onGestureEnd={onGestureEnd}
viewWidth={viewWidth}
/>
<PlaneViewer data={data} plane='x' headers={headers}
SliceNo={coord.x}
Expand All @@ -93,7 +105,8 @@ export function Viewer({rawData, headers, onGestureStart, onGestureEnd}) {
crosshairs={ {x: coord.y, y: coord.z} }
onGestureStart={onGestureStart}
onGestureEnd={onGestureEnd}
/>
viewWidth={viewWidth}
/>
<PlaneViewer data={data}
headers={headers}
onGestureStart={onGestureStart}
Expand All @@ -110,7 +123,8 @@ export function Viewer({rawData, headers, onGestureStart, onGestureEnd}) {
}
}
crosshairs={ {x: coord.x, y: coord.z} }
/>
viewWidth={viewWidth}
/>
</View>
);
}
2 changes: 1 addition & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "BrainViewer",
"slug": "BrainViewer",
"version": "1.0.0",
"orientation": "portrait",
"orientation": "default",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"react": "18.2.0",
"react-native": "0.71.8",
"three": "^0.154.0",
"react-native-gesture-handler": "~2.9.0"
"react-native-gesture-handler": "~2.9.0",
"expo-screen-orientation": "~5.1.1"
},
"devDependencies": {
"@babel/core": "^7.20.0"
Expand Down