diff --git a/App.js b/App.js index b245738..1641a3b 100644 --- a/App.js +++ b/App.js @@ -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); @@ -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) @@ -64,11 +83,7 @@ export default function App() { return ( // ScrollView - - - - - + - - setShouldScroll(false) } onGestureEnd={ () => setShouldScroll(true) } + screenOrientation={screenOrientation} /> + - - ); @@ -99,7 +117,8 @@ export default function App() { const styles = StyleSheet.create({ container: { - flex: 1, + display: 'flex', backgroundColor: '#fff', + marginTop: 40, }, }); diff --git a/PlaneViewer.js b/PlaneViewer.js index ecd31ed..f1591ed 100644 --- a/PlaneViewer.js +++ b/PlaneViewer.js @@ -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) { @@ -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. @@ -68,11 +66,12 @@ 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 @@ -80,7 +79,23 @@ function useGLCanvas(viewWidth, viewHeight, headers, data, plane) { 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]); + + const setZoomFactorCB = useCallback( (newZoom) => { if (!gl) { console.log('No gl for setZoomFactorCB'); return; @@ -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); } @@ -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) { @@ -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, ` @@ -342,7 +357,6 @@ function useGLCanvas(viewWidth, viewHeight, headers, data, plane) { 0, ); - // Calculate display uniforms const spaceSizeUniformLocation = gl.getUniformLocation(program, "u_spacesize"); if (spaceSizeUniformLocation) { @@ -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().current; - return { + + const canvas = useMemo(() => { + return ; + }, [viewWidth]); + + return { setSliceNum: setSliceNum, setCrosshairs: setCrosshairs, canvas: canvas, @@ -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( () => { @@ -576,14 +591,19 @@ export function PlaneViewer({headers, data, plane, SliceNo, label, onSliceChange throw new Error("Invalid plane"); } return ( - - + {label} (Size: {sliderMax}) {canvas} ); diff --git a/SegmentSlider.js b/SegmentSlider.js index b08b26b..cf5a296 100644 --- a/SegmentSlider.js +++ b/SegmentSlider.js @@ -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 ? {props.label} : null; return ( diff --git a/Viewer.js b/Viewer.js index e434376..fa7fa58 100644 --- a/Viewer.js +++ b/Viewer.js @@ -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'; @@ -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; @@ -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 ( - + + viewWidth={viewWidth} + /> + viewWidth={viewWidth} + /> ); } diff --git a/app.json b/app.json index 850a088..e4a5cf4 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "name": "BrainViewer", "slug": "BrainViewer", "version": "1.0.0", - "orientation": "portrait", + "orientation": "default", "icon": "./assets/icon.png", "userInterfaceStyle": "light", "splash": { diff --git a/package-lock.json b/package-lock.json index d00fa6e..0135039 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "expo": "~48.0.18", "expo-2d-context": "^0.0.3", "expo-gl": "~12.4.0", + "expo-screen-orientation": "~5.1.1", "expo-secure-store": "~12.1.1", "expo-status-bar": "~1.4.4", "pako": "^2.1.0", @@ -7449,6 +7450,14 @@ "invariant": "^2.2.4" } }, + "node_modules/expo-screen-orientation": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/expo-screen-orientation/-/expo-screen-orientation-5.1.1.tgz", + "integrity": "sha512-rYpHUwHpuKlEErxupl3wM+KWcySrSJTXb5Une5gDINMmjWbLBx0N1P2MG1aSPA2eT7bShTO/xDJyegjf438l8w==", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-secure-store": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-12.1.1.tgz", diff --git a/package.json b/package.json index ec7052a..2d5c9e2 100644 --- a/package.json +++ b/package.json @@ -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"