diff --git a/tools/Python/mcdisplay/webgl/.gitignore b/tools/Python/mcdisplay/webgl/.gitignore index d092610196..38d5273f2b 100644 --- a/tools/Python/mcdisplay/webgl/.gitignore +++ b/tools/Python/mcdisplay/webgl/.gitignore @@ -1,2 +1,10 @@ +# Dependency directories +node_modules/ + +# Build directories +dist/ + +# Project specific instrument.json particles.json +package-lock.json diff --git a/tools/Python/mcdisplay/webgl/Contexts/CameraContext.jsx b/tools/Python/mcdisplay/webgl/Contexts/CameraContext.jsx index aa8ac3c05d..09320151df 100644 --- a/tools/Python/mcdisplay/webgl/Contexts/CameraContext.jsx +++ b/tools/Python/mcdisplay/webgl/Contexts/CameraContext.jsx @@ -10,12 +10,11 @@ const CameraContext = createContext({ setCamPosHome: () => {}, setCamPosSide: () => {}, setCamPosTop: () => {}, - logMessage: () => {}, }); export const CameraProvider = ({ children }) => { - const [camPos, setCamPos] = useState(new Vector3(10, 20, 30)); - const [camPosHome, setCamPosHome] = useState(new Vector3(10, 20, 30)); + const [camPos, setCamPos] = useState(new Vector3(0, 10, 0)); + const [camPosHome, setCamPosHome] = useState(new Vector3(0, 10, 0)); const [camPosSide, setCamPosSide] = useState(new Vector3(0, 0, 30)); const [camPosTop, setCamPosTop] = useState(new Vector3(0, 30, 0)); diff --git a/tools/Python/mcdisplay/webgl/Contexts/ComponentsContext.tsx b/tools/Python/mcdisplay/webgl/Contexts/ComponentsContext.tsx deleted file mode 100644 index c53d21e47c..0000000000 --- a/tools/Python/mcdisplay/webgl/Contexts/ComponentsContext.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { - ReactNode, - createContext, - useContext, - useState, - useEffect, -} from "react"; -import { Component } from "../model/Component"; -import { fetchJSON } from "../utils/fetch"; -import { initializeInstrument } from "../data-utils/initInstrument"; - -type ComponentsContextType = { - components: Component[]; - setComponents: React.Dispatch>; -}; - -const ComponentsContext = createContext({ - components: [], - setComponents: () => {}, -}); - -interface ComponentsProviderProps { - children: ReactNode; -} - -export const ComponentsProvider: React.FC = ({ - children, -}) => { - const [components, _setComponents] = useState([]); - - useEffect(() => { - fetchJSON("../instrument.json").then((data) => { - if (data) { - const instrument = initializeInstrument(data); - _setComponents(instrument.components); - } else { - console.warn("Instrument data is missing"); - } - }); - }, []); - - const setComponents = (newComponents: React.SetStateAction) => { - _setComponents(newComponents); - }; - - return ( - - {children} - - ); -}; - -export const useComponentsContext = () => useContext(ComponentsContext); diff --git a/tools/Python/mcdisplay/webgl/Contexts/GridContext.jsx b/tools/Python/mcdisplay/webgl/Contexts/GridContext.jsx index e0f8c2221c..e6c8022a40 100644 --- a/tools/Python/mcdisplay/webgl/Contexts/GridContext.jsx +++ b/tools/Python/mcdisplay/webgl/Contexts/GridContext.jsx @@ -11,9 +11,13 @@ const GridContext = createContext({ toggleXY: () => {}, toggleXZ: () => {}, toggleYZ: () => {}, + + showAxes: true, + toggleAxes: () => {}, }); export const GridProvider = ({ children }) => { + const [showAxes, setShowAxes] = useState(true); const [gridSize, setGridSize] = useState(100); const [gridDivisions, setGridDivisions] = useState(100); const updateGridSize = (size, divisions) => { @@ -28,6 +32,7 @@ export const GridProvider = ({ children }) => { const toggleXY = () => setShowXY(!showXY); const toggleXZ = () => setShowXZ(!showXZ); const toggleYZ = () => setShowYZ(!showYZ); + const toggleAxes = () => setShowAxes(!showAxes); return ( { toggleXY, toggleXZ, toggleYZ, + showAxes, + toggleAxes, }} > {children} diff --git a/tools/Python/mcdisplay/webgl/Contexts/InstrumentContext.tsx b/tools/Python/mcdisplay/webgl/Contexts/InstrumentContext.tsx new file mode 100644 index 0000000000..2f9eea7d79 --- /dev/null +++ b/tools/Python/mcdisplay/webgl/Contexts/InstrumentContext.tsx @@ -0,0 +1,69 @@ +import React, { + ReactNode, + createContext, + useContext, + useState, + useEffect, +} from "react"; +import { fetchJSON } from "../utils/fetch"; +import { initializeInstrument } from "../data-utils/initInstrument"; +import { Instrument } from "../model/Instrument"; + +type InstrumentContextType = { + instrument: Instrument; + setInstrument: React.Dispatch>; +}; + +const InstrumentContext = createContext({ + instrument: { + name: "", + abspath: "", + params: [], + params_defaults: [], + params_values: [], + cmd: "", + components: [], + }, + setInstrument: () => {}, +}); + +interface InstrumentProviderProps { + children: ReactNode; +} + +export const InstrumentProvider: React.FC = ({ + children, +}) => { + const [instrument, _setInstrument] = useState({ + name: "", + abspath: "", + params: [], + params_defaults: [], + params_values: [], + cmd: "", + components: [], + }); + + useEffect(() => { + fetchJSON("../instrument.json").then((data) => { + if (data) { + const instrument = initializeInstrument(data); + _setInstrument(instrument); + } else { + console.warn("Instrument data is missing"); + } + }); + }, []); + + const setInstrument = (newInstrument: React.SetStateAction) => { + _setInstrument(newInstrument); + }; + + return ( + + {children} + + ); +}; + +export const useInstrumentContext = () => useContext(InstrumentContext); diff --git a/tools/Python/mcdisplay/webgl/Contexts/PlotRangeContext.tsx b/tools/Python/mcdisplay/webgl/Contexts/PlotRangeContext.tsx new file mode 100644 index 0000000000..7d36f2b5f1 --- /dev/null +++ b/tools/Python/mcdisplay/webgl/Contexts/PlotRangeContext.tsx @@ -0,0 +1,57 @@ +import React, { createContext, useContext, ReactNode, useState } from "react"; + +type AxisRange = [number, number]; + +type PlotRangeContextType = { + plotlyRanges: { + [key: string]: { + xaxis: AxisRange; + yaxis: AxisRange; + }; + }; + updatePlotlyRanges: ( + view: string, + newRanges: { xaxis: AxisRange; yaxis: AxisRange } + ) => void; +}; + +const PlotRangeContext = createContext({ + plotlyRanges: { + Top: { xaxis: [0, 100], yaxis: [-50, 50] }, + Side: { xaxis: [0, 100], yaxis: [-50, 50] }, + End: { xaxis: [0, 100], yaxis: [-50, 50] }, + }, + updatePlotlyRanges: () => {}, +}); + +interface PlotRangeProviderProps { + children: ReactNode; +} + +export const PlotRangeProvider: React.FC = ({ + children, +}) => { + const [plotlyRanges, setPlotlyRanges] = useState({ + Top: { xaxis: [0, 100] as AxisRange, yaxis: [-50, 50] as AxisRange }, + Side: { xaxis: [0, 100] as AxisRange, yaxis: [-50, 50] as AxisRange }, + End: { xaxis: [0, 100] as AxisRange, yaxis: [-50, 50] as AxisRange }, + }); + + const updatePlotlyRanges = ( + view: string, + newRanges: { xaxis: AxisRange; yaxis: AxisRange } + ) => { + setPlotlyRanges((prevRanges) => ({ + ...prevRanges, + [view]: newRanges, + })); + }; + + return ( + + {children} + + ); +}; + +export const usePlotRangeContext = () => useContext(PlotRangeContext); diff --git a/tools/Python/mcdisplay/webgl/Contexts/SceneContext.tsx b/tools/Python/mcdisplay/webgl/Contexts/SceneContext.tsx new file mode 100644 index 0000000000..f9dcae81b9 --- /dev/null +++ b/tools/Python/mcdisplay/webgl/Contexts/SceneContext.tsx @@ -0,0 +1,71 @@ +import React, { createContext, useContext, ReactNode, useRef } from "react"; +import * as THREE from "three"; +import { initializeScene } from "../components/scene/initializeScene"; + +type SceneContextType = { + gridsRef: React.MutableRefObject; + axesRef: React.MutableRefObject; + containerRef: React.MutableRefObject; + primaryCameraRef: React.MutableRefObject; + primaryControlsRef: React.MutableRefObject; + primaryViewRef: React.MutableRefObject; + TopView2DRef: React.MutableRefObject; + SideView2DRef: React.MutableRefObject; + BackView2DRef: React.MutableRefObject; + rendererRef: React.MutableRefObject; + sceneRef: React.MutableRefObject; +}; + +const SceneContext = createContext({ + gridsRef: { current: null }, + axesRef: { current: null }, + containerRef: { current: null }, + primaryCameraRef: { current: null }, + primaryControlsRef: { current: null }, + primaryViewRef: { current: null }, + TopView2DRef: { current: null }, + SideView2DRef: { current: null }, + BackView2DRef: { current: null }, + rendererRef: { current: null }, + sceneRef: { current: null }, +}); + +interface SceneProviderProps { + children: ReactNode; +} + +export const SceneProvider: React.FC = ({ children }) => { + const gridsRef = useRef({ gridXY: null, gridXZ: null, gridYZ: null }); + const axesRef = useRef({ x_axis: null, y_axis: null, z_axis: null }); + const containerRef = useRef(null); + const primaryCameraRef = useRef(null); + const primaryControlsRef = useRef(null); + const primaryViewRef = useRef(null); + const TopView2DRef = useRef(null); + const SideView2DRef = useRef(null); + const BackView2DRef = useRef(null); + const rendererRef = useRef(null); + const sceneRef = useRef(initializeScene()); + + return ( + + {children} + + ); +}; + +export const useSceneContext = () => useContext(SceneContext); diff --git a/tools/Python/mcdisplay/webgl/Contexts/addRays.ts b/tools/Python/mcdisplay/webgl/Contexts/addRays.ts index 1639a4ef70..14a88cdfa6 100644 --- a/tools/Python/mcdisplay/webgl/Contexts/addRays.ts +++ b/tools/Python/mcdisplay/webgl/Contexts/addRays.ts @@ -170,7 +170,7 @@ function createScatterPoints( scatterPoints.name = SCATTERPOINTS; vertices.forEach((vertex) => { - const geometry = new THREE.SphereGeometry(0.0035, 32, 32); + const geometry = new THREE.SphereGeometry(0.004, 4, 2); const material = new THREE.MeshBasicMaterial({ color }); const sphere = new THREE.Mesh(geometry, material); sphere.position.copy(vertex); diff --git a/tools/Python/mcdisplay/webgl/common.css b/tools/Python/mcdisplay/webgl/common.css index d21289e019..442029c7fe 100644 --- a/tools/Python/mcdisplay/webgl/common.css +++ b/tools/Python/mcdisplay/webgl/common.css @@ -58,7 +58,11 @@ li { isolation: isolate; } :root { - --gray-color: #7c7c7c; + --x-axis-color: #7f2020; + --y-axis-color: #207f20; + --z-axis-color: #20207f; + + --gray-color: #808080; /* Light mode colors */ --primary-color: #3498db; @@ -78,7 +82,7 @@ li { --primary-color-dark: #2980b9; --secondary-color-dark: #27ae60; --background-color-dark: #121212; - --text-color-dark: #000000; + --text-color-dark: #ffffff; --link-color-dark: #8ab4f8; --border-color-dark: #ffffff; --button-bg-color-dark: #2980b9; @@ -178,6 +182,9 @@ button.active { background-color: var(--background-color-dark); color: var(--text-color-dark); } + p { + color: var(--text-color-dark); + } a { color: var(--link-color-dark); diff --git a/tools/Python/mcdisplay/webgl/components/configure-scene/ConfigureSceneMenu.jsx b/tools/Python/mcdisplay/webgl/components/configure-scene/ConfigureSceneMenu.jsx index 46d5f13374..2a7e736c0c 100644 --- a/tools/Python/mcdisplay/webgl/components/configure-scene/ConfigureSceneMenu.jsx +++ b/tools/Python/mcdisplay/webgl/components/configure-scene/ConfigureSceneMenu.jsx @@ -5,6 +5,7 @@ import "./configure-scene-menu.css"; import ComponentStyler from "./component-styler/ComponentStyler"; import RaysMenu from "./rays-menu/RaysMenu"; import BackgroundColorButton from "./background-color-button/BackgroundColorButton"; +import AxesButton from "./axes-button/AxesButton"; const ConfigureSceneMenu = () => { return ( @@ -13,6 +14,7 @@ const ConfigureSceneMenu = () => { + ); diff --git a/tools/Python/mcdisplay/webgl/components/configure-scene/axes-button/AxesButton.tsx b/tools/Python/mcdisplay/webgl/components/configure-scene/axes-button/AxesButton.tsx new file mode 100644 index 0000000000..e39b65947a --- /dev/null +++ b/tools/Python/mcdisplay/webgl/components/configure-scene/axes-button/AxesButton.tsx @@ -0,0 +1,38 @@ +import React, { useEffect } from "react"; +import "../../../common.css"; +import "./axes-button.css"; +import { useGridContext } from "../../../Contexts/GridContext"; +import { useSceneContext } from "../../../Contexts/SceneContext"; + +const AxesButton = () => { + const { showAxes, toggleAxes } = useGridContext(); + const { axesRef } = useSceneContext(); + + useEffect(() => { + if (axesRef.current) { + if ( + axesRef.current.x_axis || + axesRef.current.y_axis || + axesRef.current.z_axis + ) { + axesRef.current.x_axis.visible = showAxes; + axesRef.current.y_axis.visible = showAxes; + axesRef.current.z_axis.visible = showAxes; + } + } + }, [showAxes]); + + return ( + + ); +}; + +export default AxesButton; diff --git a/tools/Python/mcdisplay/webgl/components/configure-scene/axes-button/axes-button.css b/tools/Python/mcdisplay/webgl/components/configure-scene/axes-button/axes-button.css new file mode 100644 index 0000000000..dee43cfc57 --- /dev/null +++ b/tools/Python/mcdisplay/webgl/components/configure-scene/axes-button/axes-button.css @@ -0,0 +1,14 @@ +#axes-button { +} + +#x { + color: var(--x-axis-color); +} + +#y { + color: var(--y-axis-color); +} + +#z { + color: var(--z-axis-color); +} diff --git a/tools/Python/mcdisplay/webgl/components/configure-scene/component-styler/ComponentStyler.tsx b/tools/Python/mcdisplay/webgl/components/configure-scene/component-styler/ComponentStyler.tsx index 97add2c287..05af5f42a7 100644 --- a/tools/Python/mcdisplay/webgl/components/configure-scene/component-styler/ComponentStyler.tsx +++ b/tools/Python/mcdisplay/webgl/components/configure-scene/component-styler/ComponentStyler.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import "../../../common.css"; import "./component-styler.css"; -import { useComponentsContext } from "../../../Contexts/ComponentsContext"; +import { useInstrumentContext } from "../../../Contexts/InstrumentContext"; import DropDown from "./dropdown/DropDown"; import ColorPicker from "./color-picker/ColorPicker"; import { DrawCall, Component } from "../../../model/Component"; @@ -15,18 +15,18 @@ const ComponentStyler = () => { color: "#1a73e8", transparency: 1, }; - const { components, setComponents } = useComponentsContext(); + const { instrument, setInstrument } = useInstrumentContext(); const [currentComponent, setCurrentComponent] = useState(fallBackComponent); useEffect(() => { if ( - components.length > 0 && + instrument.components.length > 0 && currentComponent.name === "Loading instrument" ) { - setCurrentComponent(components[1]); + setCurrentComponent(instrument.components[1]); } - }, [components, currentComponent]); + }, [instrument.components, currentComponent]); return (
@@ -34,12 +34,10 @@ const ComponentStyler = () => {
diff --git a/tools/Python/mcdisplay/webgl/components/configure-scene/component-styler/color-picker/ColorPicker.tsx b/tools/Python/mcdisplay/webgl/components/configure-scene/component-styler/color-picker/ColorPicker.tsx index 1dbea1511d..1c9f1b561c 100644 --- a/tools/Python/mcdisplay/webgl/components/configure-scene/component-styler/color-picker/ColorPicker.tsx +++ b/tools/Python/mcdisplay/webgl/components/configure-scene/component-styler/color-picker/ColorPicker.tsx @@ -4,20 +4,18 @@ import "./color-picker.css"; import Sketch from "@uiw/react-color-sketch"; import DropDownButton from "../dropdown-button/DropDownButton"; import { Component } from "../../../../model/Component"; +import { useInstrumentContext } from "../../../../Contexts/InstrumentContext"; interface ColorPickerProps { currentComponent: Component; setCurrentComponent: React.Dispatch>; - components: Component[]; - setComponents: React.Dispatch>; } const ColorPicker = ({ currentComponent, setCurrentComponent, - components, - setComponents, }: ColorPickerProps) => { + const { instrument, setInstrument } = useInstrumentContext(); const [open, setOpen] = useState(false); const [tempColor, setTempColor] = useState(currentComponent.color); const [tempTransparency, setTempTransparency] = useState( @@ -36,13 +34,16 @@ const ColorPicker = ({ const handleClickOutside = (event) => { if (sketchRef.current && !sketchRef.current.contains(event.target)) { - const newComponents = components.map((c) => { + const newComponents = instrument.components.map((c) => { if (c.id === currentComponent.id) { return { ...c, color: tempColor, transparency: tempTransparency }; } return c; }); - setComponents(newComponents); + setInstrument((prevInstrument) => ({ + ...prevInstrument, + components: newComponents, + })); setCurrentComponent((prevComponent) => ({ ...prevComponent, color: tempColor, diff --git a/tools/Python/mcdisplay/webgl/components/configure-scene/configure-scene-menu.css b/tools/Python/mcdisplay/webgl/components/configure-scene/configure-scene-menu.css index 78a518386c..4f256f5a9c 100644 --- a/tools/Python/mcdisplay/webgl/components/configure-scene/configure-scene-menu.css +++ b/tools/Python/mcdisplay/webgl/components/configure-scene/configure-scene-menu.css @@ -1,6 +1,5 @@ #configure-scene-menu { position: fixed; - background-color: rgba(255, 255, 255, 0.9); width: 100%; display: flex; flex-direction: row; diff --git a/tools/Python/mcdisplay/webgl/components/configure-scene/grid-buttons/GridButtons.jsx b/tools/Python/mcdisplay/webgl/components/configure-scene/grid-buttons/GridButtons.jsx index 3e30f9b419..6d536c54bd 100644 --- a/tools/Python/mcdisplay/webgl/components/configure-scene/grid-buttons/GridButtons.jsx +++ b/tools/Python/mcdisplay/webgl/components/configure-scene/grid-buttons/GridButtons.jsx @@ -1,10 +1,26 @@ -import React from "react"; +import React, { useEffect } from "react"; import "../../../common.css"; import { useGridContext } from "../../../Contexts/GridContext"; +import { useSceneContext } from "../../../Contexts/SceneContext"; const GridButtons = () => { - const { showXY, showXZ, showYZ, toggleXY, toggleXZ, toggleYZ } = + const { showXY, showXZ, showYZ, toggleXY, toggleXZ, toggleYZ, gridSize } = useGridContext(); + const { gridsRef, sceneRef } = useSceneContext(); + + useEffect(() => { + if (gridsRef.current) { + if (gridsRef.current.gridXY) { + gridsRef.current.gridXY.visible = showXY; + } + if (gridsRef.current.gridXZ) { + gridsRef.current.gridXZ.visible = showXZ; + } + if (gridsRef.current.gridYZ) { + gridsRef.current.gridYZ.visible = showYZ; + } + } + }, [showXY, showXZ, showYZ]); return (
diff --git a/tools/Python/mcdisplay/webgl/components/configure-scene/rays-menu/toggle-rays/ToggleRays.tsx b/tools/Python/mcdisplay/webgl/components/configure-scene/rays-menu/toggle-rays/ToggleRays.tsx index 3a57744fc2..b46ee0da8b 100644 --- a/tools/Python/mcdisplay/webgl/components/configure-scene/rays-menu/toggle-rays/ToggleRays.tsx +++ b/tools/Python/mcdisplay/webgl/components/configure-scene/rays-menu/toggle-rays/ToggleRays.tsx @@ -7,6 +7,9 @@ import RaysPlayback from "../rays-playback/RaysPlayback"; import ShowAllRays from "../show-all-rays/ShowAllRays"; import { fetchJSON } from "../../../../utils/fetch"; import { initializeRays } from "../../../../data-utils/initRays"; +import { useAppContext } from "../../../../Contexts/AppContext"; +import { setRaysInvisible, setRaysVisible } from "../../../../Contexts/addRays"; +import { useSceneContext } from "../../../../Contexts/SceneContext"; const ToggleRays = () => { const { @@ -16,8 +19,17 @@ const ToggleRays = () => { toggleShowAllRays, showRays, toggleRays, + play, + setPlay, + currentRayIndex, + setCurrentRayIndex, + showScatterPoints, + handleNextClick, } = useRaysContext(); + const { loading, setLoading } = useAppContext(); + const { sceneRef } = useSceneContext(); + const handleClick = () => { if (rays.rays.length === 0) { console.log("initialize rays"); @@ -33,6 +45,21 @@ const ToggleRays = () => { toggleRays(); }; + const handleShowRays = async () => { + setLoading(true); + if (!showRays || (showRays && !showAllRays)) { + setRaysInvisible(sceneRef.current); + } else if (showRays && showAllRays) { + setPlay(false); + setRaysVisible(sceneRef.current); + } + setLoading(false); + }; + + useEffect(() => { + handleShowRays(); + }, [showRays, showAllRays]); + return (
-
-

Top

-
{topView.y_label}[m]
-
{topView.x_label}[m]
-
-
-

Side

-
{sideView.y_label}[m]
-
{sideView.x_label}[m]
-
+ +
-
-

End

-
{backView.y_label}[m]
-
{backView.x_label}[m]
+ +
+
-
diff --git a/tools/Python/mcdisplay/webgl/components/scene/info-view/InfoView.tsx b/tools/Python/mcdisplay/webgl/components/scene/info-view/InfoView.tsx new file mode 100644 index 0000000000..b9d825989d --- /dev/null +++ b/tools/Python/mcdisplay/webgl/components/scene/info-view/InfoView.tsx @@ -0,0 +1,62 @@ +import React, { useContext } from "react"; +import "../../../common.css"; +import "./info-view.css"; +import { useInstrumentContext } from "../../../Contexts/InstrumentContext"; +import { useRaysContext } from "../../../Contexts/RaysContext"; +import ExportJSONButton from "./export-json-button/ExportJSONButton"; + +const InfoView = () => { + const { instrument, setInstrument } = useInstrumentContext(); + const { rays, setRays } = useRaysContext(); + + return ( +
+

{instrument.name}

+

+ Instrument file: + {instrument.abspath} +

+

+ Number of components: + {instrument.components.length} +

+

+ Command: + {instrument.cmd} +

+

+ Instrument parameters: + {instrument.params} +

+

+ Instrument parameter defaults: + {instrument.params_defaults} +

+

+ Instrument parameters values: + {instrument.params_values} +

+ {rays.numrays ? ( +

+ Number of rays: + {rays.numrays} +

+ ) : null} + {rays.vmax ? ( +

+ rays maximum velocity: + {rays.vmax} +

+ ) : null} + {rays.vmin ? ( +

+ rays minimum velocity: + {rays.vmin} +

+ ) : null} + +
+ ); +}; + +export default InfoView; diff --git a/tools/Python/mcdisplay/webgl/components/scene/info-view/export-json-button/ExportJSONButton.tsx b/tools/Python/mcdisplay/webgl/components/scene/info-view/export-json-button/ExportJSONButton.tsx new file mode 100644 index 0000000000..77ea3d801a --- /dev/null +++ b/tools/Python/mcdisplay/webgl/components/scene/info-view/export-json-button/ExportJSONButton.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import "../../../../common.css"; +import "./export-json-button.css"; +import { useInstrumentContext } from "../../../../Contexts/InstrumentContext"; + +const ExportJSONButton = () => { + const { instrument, setInstrument } = useInstrumentContext(); + + const handleButtonClick = () => { + const comps = instrument.components; + let element = document.createElement("a"); + element.setAttribute( + "href", + "data:text/plain;charset=utf-8," + + encodeURIComponent(JSON.stringify(comps, null, 2)) + ); + element.setAttribute("download", "components.json"); + element.style.display = "none"; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); + }; + + return ( +
+ +
+ ); +}; + +export default ExportJSONButton; diff --git a/tools/Python/mcdisplay/webgl/components/scene/info-view/export-json-button/export-json-button.css b/tools/Python/mcdisplay/webgl/components/scene/info-view/export-json-button/export-json-button.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/Python/mcdisplay/webgl/components/scene/info-view/info-view.css b/tools/Python/mcdisplay/webgl/components/scene/info-view/info-view.css new file mode 100644 index 0000000000..69589f4d0c --- /dev/null +++ b/tools/Python/mcdisplay/webgl/components/scene/info-view/info-view.css @@ -0,0 +1,3 @@ +#info-view { + padding: 10px; +} diff --git a/tools/Python/mcdisplay/webgl/components/scene/initializeScene.js b/tools/Python/mcdisplay/webgl/components/scene/initializeScene.js index 066a51c519..301e1e43a2 100644 --- a/tools/Python/mcdisplay/webgl/components/scene/initializeScene.js +++ b/tools/Python/mcdisplay/webgl/components/scene/initializeScene.js @@ -16,12 +16,16 @@ export const initializeCameras = ( primaryViewRef ) => { // Helper function to create an Orthographic Camera - const createOrthographicCamera = (width, height) => { - const left = width / -100; - const right = width / 100; - const top = height / 100; - const bottom = height / -100; - const size = 1; + const createOrthographicCamera = (width, height, size) => { + // Calculate the aspect ratio + const aspect = width / height; + + // Calculate the boundaries + const left = -size / 2; + const right = size / 2; + const top = size / 2 / aspect; + const bottom = -(size / 2) / aspect; + const near = 0.1; const far = 1000; return new THREE.OrthographicCamera(left, right, top, bottom, near, far); @@ -30,11 +34,11 @@ export const initializeCameras = ( // Helper function to assign the correct DOM element const getDomElement = (view) => { switch (view.view) { - case "back2D": + case "End": return backView2DRef; - case "top2D": + case "Top": return topView2DRef; - case "side2D": + case "Side": return sideView2DRef; default: return primaryViewRef; @@ -47,20 +51,39 @@ export const initializeCameras = ( const domElement = getDomElement(view); if (view.camera === "OrthographicCamera") { - camera = createOrthographicCamera(width, height); + camera = createOrthographicCamera(width, height, size); controls = new OrbitControls(camera, domElement); controls.enableRotate = false; const cameraHelper = new THREE.CameraHelper(camera); view.cameraHelper = cameraHelper; //scene.add(cameraHelper); } else { - camera = new THREE.PerspectiveCamera(view.fov, width / height, 0.1, 1000); + camera = new THREE.PerspectiveCamera( + view.fov, + width / height, + 0.01, + 1000 + ); controls = new OrbitControls(camera, primaryViewRef); } - + controls.mouseButtons = { + LEFT: THREE.MOUSE.PAN, + MIDDLE: THREE.MOUSE.DOLLY, + RIGHT: THREE.MOUSE.ROTATE, + }; const position = view.initialCamPos.map((element) => element * size); + console.log("position: ", position); + console.log("size: ", size); camera.position.fromArray(position); camera.up.fromArray(view.up); + + /*panning for orthographic cameras updates plotly axes + controls.addEventListener('change', () => { + const xRange = [camera.position.x - 5, camera.position.x + 5]; + const yRange = [camera.position.y - 5, camera.position.y + 5]; + updatePlotlyRanges(view.view, { xaxis: xRange, yaxis: yRange }); + }); + */ view.controls = controls; view.camera = camera; view.domElement = domElement; @@ -77,9 +100,51 @@ export const initializeRenderer = (width, height) => { return renderer; }; +export const addAxes = (scene, size) => { + const axes = {}; + + const center = new THREE.Vector3(0, 0, 0); + + /* arrow colors should match --x-axis-color, y-ax.. colors in common.css*/ + const x_axis = new THREE.ArrowHelper( + new THREE.Vector3(1, 0, 0), + center, + size, + 0x7f2020, + 1, + 0.5 + ); + const y_axis = new THREE.ArrowHelper( + new THREE.Vector3(0, 1, 0), + center, + size, + 0x207f20, + 1, + 0.5 + ); + const z_axis = new THREE.ArrowHelper( + new THREE.Vector3(0, 0, 1), + center, + size, + 0x20207f, + 1, + 0.5 + ); + + axes.x_axis = x_axis; + axes.y_axis = y_axis; + axes.z_axis = z_axis; + + scene.add(x_axis); + scene.add(y_axis); + scene.add(z_axis); + + return axes; +}; + export const addGrids = (scene, gridSize) => { /* - the constants + 20 and -10 are hacks for taking into account that 0,0,0 + the constants + 5 and -5 are hacks for taking into account that 0,0,0 is not the true start point of the instrument components may be centered there but can extend beyond it. */ @@ -90,6 +155,7 @@ export const addGrids = (scene, gridSize) => { const grids = {}; const gridXZ = new THREE.GridHelper(correctedGridSize, correctedGridSize); + console.log("correctedGridSize: ", correctedGridSize); gridXZ.position.set(0, 0, center); gridXZ.visible = true; gridXZ.name = "gridXZ"; @@ -103,7 +169,6 @@ export const addGrids = (scene, gridSize) => { gridXY.name = "gridXY"; scene.add(gridXY); grids.gridXY = gridXY; - const gridYZ = new THREE.GridHelper(correctedGridSize, correctedGridSize); gridYZ.position.set(0, 0, center); gridYZ.visible = false; @@ -115,12 +180,6 @@ export const addGrids = (scene, gridSize) => { return grids; }; -export const initializeControls = (camera, renderer) => { - const controls = new OrbitControls(camera, renderer.domElement); - controls.update(); - return controls; -}; - export const initializeDirectionalLight = (scene) => { const light = new THREE.DirectionalLight(0xffffff, 5); light.position.set(0, 10, 10); diff --git a/tools/Python/mcdisplay/webgl/components/scene/three-canvas.css b/tools/Python/mcdisplay/webgl/components/scene/three-canvas.css index fb9f06fd17..692a8084e2 100644 --- a/tools/Python/mcdisplay/webgl/components/scene/three-canvas.css +++ b/tools/Python/mcdisplay/webgl/components/scene/three-canvas.css @@ -29,22 +29,6 @@ color: var(--gray-color); } -.two-D { - display: flex; - justify-content: center; -} - -.y-axis { - position: absolute; - left: 0%; - top: 50%; -} - -.x-axis { - position: absolute; - left: 50%; - top: 92.5%; -} .view-name { position: absolute; left: 50%; diff --git a/tools/Python/mcdisplay/webgl/components/scene/two-d-view/TwoDView.tsx b/tools/Python/mcdisplay/webgl/components/scene/two-d-view/TwoDView.tsx new file mode 100644 index 0000000000..c39e2b3d9e --- /dev/null +++ b/tools/Python/mcdisplay/webgl/components/scene/two-d-view/TwoDView.tsx @@ -0,0 +1,39 @@ +import React, { useState } from "react"; +import "../../../common.css"; +import "./two-d-view.css"; +import Chart from "./chart/Chart"; + +interface TwoDViewProps { + viewRef: React.RefObject; + text: string; + x_label?: string; + y_label?: string; + unit?: string; +} + +const TwoDView = ({ + viewRef, + text, + x_label = "x", + y_label = "y", + unit = "m", +}: TwoDViewProps) => { + const [aspectRatio, setAspectRatio] = useState(1.0); + + viewRef.current?.addEventListener("resize", () => { + const view = viewRef.current; + if (view) { + console.log("aspectratio before: ", aspectRatio); + setAspectRatio(view.clientWidth / view.clientHeight); + console.log("aspectratio after: ", aspectRatio); + } + }); + + return ( +
+ +
+ ); +}; + +export default TwoDView; diff --git a/tools/Python/mcdisplay/webgl/components/scene/two-d-view/chart/Chart.tsx b/tools/Python/mcdisplay/webgl/components/scene/two-d-view/chart/Chart.tsx new file mode 100644 index 0000000000..6dac4c71ed --- /dev/null +++ b/tools/Python/mcdisplay/webgl/components/scene/two-d-view/chart/Chart.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import "../../../../common.css"; +import Plot from "react-plotly.js"; +import "./chart.css"; +import { usePlotRangeContext } from "../../../../Contexts/PlotRangeContext"; + +interface ChartProps { + chartTitle: string; + xAxisLabel: string; + yAxisLabel: string; +} + +const Chart: React.FC = ({ + chartTitle, + xAxisLabel, + yAxisLabel, +}) => { + const { plotlyRanges } = usePlotRangeContext(); + + const layout = { + title: chartTitle, + xaxis: { + title: xAxisLabel, + showgrid: false, + zeroline: false, + showline: false, + range: plotlyRanges[chartTitle]?.xaxis, + }, + yaxis: { + title: { + text: yAxisLabel, + standoff: 10, // Adjust this value to move the title + }, + showgrid: false, + zeroline: false, + showline: false, + range: plotlyRanges[chartTitle]?.yaxis, + }, + paper_bgcolor: "rgba(0,0,0,0)", + plot_bgcolor: "rgba(0,0,0,0)", + autosize: true, + margin: { + l: 30, // left margin + r: 15, // right margin + b: 15, // bottom margin + t: 15, // top margin + pad: 0, // padding + }, + dragmode: "pan", + }; + + const config = { + displayModeBar: true, + modeBarButtonsToRemove: [ + "toImage", + "zoom2d", + "select2d", + "lasso2d", + "zoomIn2d", + "zoomOut2d", + "autoScale2d", + "resetScale2d", + ], + displaylogo: false, + scrollZoom: false, + staticPlot: false, + }; + + return ( +
+ +
+ ); +}; + +export default Chart; diff --git a/tools/Python/mcdisplay/webgl/components/scene/two-d-view/chart/chart.css b/tools/Python/mcdisplay/webgl/components/scene/two-d-view/chart/chart.css new file mode 100644 index 0000000000..74e7b2f763 --- /dev/null +++ b/tools/Python/mcdisplay/webgl/components/scene/two-d-view/chart/chart.css @@ -0,0 +1,4 @@ +.chart { + width: 100%; + height: 100%; +} diff --git a/tools/Python/mcdisplay/webgl/components/scene/two-d-view/two-d-view.css b/tools/Python/mcdisplay/webgl/components/scene/two-d-view/two-d-view.css new file mode 100644 index 0000000000..0ec13c1eba --- /dev/null +++ b/tools/Python/mcdisplay/webgl/components/scene/two-d-view/two-d-view.css @@ -0,0 +1,31 @@ +.view { + position: relative; + border: solid 1.5px var(--gray-color); + width: 100%; + height: 100%; +} + +.gray-color { + color: var(--gray-color); +} + +.two-D { + display: flex; + justify-content: center; +} + +.y-axis { + position: absolute; + left: 0%; + top: 50%; +} + +.x-axis { + position: absolute; + left: 50%; + top: 92.5%; +} +.view-name { + position: absolute; + left: 50%; +} diff --git a/tools/Python/mcdisplay/webgl/components/scene/views.js b/tools/Python/mcdisplay/webgl/components/scene/views.js index cbd1dff3b7..b713f2bb9e 100644 --- a/tools/Python/mcdisplay/webgl/components/scene/views.js +++ b/tools/Python/mcdisplay/webgl/components/scene/views.js @@ -25,7 +25,8 @@ export const views = [ // Top view - XZ { camera: "OrthographicCamera", - view: "top2D", + zoom: 1.0, + view: "Top", initialCamPos: [0, 1, 0], up: [1, 0, 0], x_label: "z", @@ -42,12 +43,14 @@ export const views = [ // Back view - XY { camera: "OrthographicCamera", - view: "back2D", + zoom: 1.0, + view: "End", initialCamPos: [0, 0, 1], up: [0, 1, 0], x_label: "x", y_label: "y", updateCamera: function (camera, scene, mouseX) { + camera.position.x += mouseX * 0.05; camera.position.x = Math.max(Math.min(camera.position.x, 2000), -2000); camera.lookAt(scene.position); }, @@ -58,8 +61,9 @@ export const views = [ //Side view - YZ { camera: "OrthographicCamera", - view: "side2D", - initialCamPos: [1, 0, 0], + zoom: 1.0, + view: "Side", + initialCamPos: [1, 0, 1], up: [0, 1, 0], x_label: "z", y_label: "y", diff --git a/tools/Python/mcdisplay/webgl/main.tsx b/tools/Python/mcdisplay/webgl/main.tsx index 007d2f0e41..5d22502b24 100644 --- a/tools/Python/mcdisplay/webgl/main.tsx +++ b/tools/Python/mcdisplay/webgl/main.tsx @@ -2,21 +2,24 @@ import React from "react"; import "./common.css"; import { GridProvider } from "./Contexts/GridContext"; import { CameraProvider } from "./Contexts/CameraContext"; -import { ComponentsProvider } from "./Contexts/ComponentsContext"; +import { InstrumentProvider } from "./Contexts/InstrumentContext"; import { RaysProvider } from "./Contexts/RaysContext"; import { AppProvider } from "./Contexts/AppContext"; import App from "./App"; import ReactDOM from "react-dom/client"; +import { PlotRangeProvider } from "./Contexts/PlotRangeContext"; ReactDOM.createRoot(document.getElementById("root")!).render( - + - + + + - + diff --git a/tools/Python/mcdisplay/webgl/package.json b/tools/Python/mcdisplay/webgl/package.json index 9348860d54..3c7536ee60 100644 --- a/tools/Python/mcdisplay/webgl/package.json +++ b/tools/Python/mcdisplay/webgl/package.json @@ -5,8 +5,7 @@ "type": "module", "scripts": { "dev": "node start-vite.js", - "build": "vite build", - "serve": "http-server dist -p 8000" + "build": "vite build" }, "keywords": [], "author": "", @@ -22,13 +21,13 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-loader-spinner": "^6.1.6", + "react-plotly.js": "^2.6.0", "three": "^0.165.0", "three-lut": "^79.0.2", "three-orbitcontrols": "^2.110.3", "vite": "^5.2.13" }, "devDependencies": { - "http-server": "^14.1.1", "typescript": "^5.4.5", "vite-plugin-static-copy": "^1.0.6" } diff --git a/tools/Python/mcdisplay/webgl/start-vite.js b/tools/Python/mcdisplay/webgl/start-vite.js index 77b8b3dc8e..1d5334a7f5 100644 --- a/tools/Python/mcdisplay/webgl/start-vite.js +++ b/tools/Python/mcdisplay/webgl/start-vite.js @@ -1,14 +1,14 @@ // start-vite.js -import { createServer } from 'vite'; +import { createServer } from "vite"; async function start() { - try { - const server = await createServer(); - await server.listen(); - server.printUrls(); - } catch (error) { - console.error('Error starting Vite server:', error); - } + try { + const server = await createServer(); + await server.listen(); + server.printUrls(); + } catch (error) { + console.error("Error starting Vite server:", error); + } } -start(); \ No newline at end of file +start();