diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a5987f3d0..beef69eb1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -24,6 +24,9 @@ jobs: pip install build twine # Build client files. python -c "import viser; viser.ViserServer()" + - name: Strip unsupported tags in README + run: | + sed -i '//,//d' README.md - name: Build and publish env: PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} diff --git a/README.md b/README.md index ddd062538..7ff7c951d 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,55 @@ -

- viser -

- -**`pip install viser`**   •   **[ -[API Reference](https://nerfstudio-project.github.io/viser) ]** - -![pyright](https://github.com/nerfstudio-project/viser/workflows/pyright/badge.svg) -![mypy](https://github.com/nerfstudio-project/viser/workflows/mypy/badge.svg) -![typescript](https://github.com/nerfstudio-project/viser/workflows/typescript-compile/badge.svg) -[![pypi](https://img.shields.io/pypi/pyversions/viser)](https://pypi.org/project/viser) - ---- - -`viser` is a library for interactive 3D visualization + Python, inspired by +

+ + + + + + tyro logo + + + + +

+ +
+ +

+ pip install viser +   •   + Documentation +

+ +

+ pyright + mypy + typescript-compile + + codecov + +

+ +**`viser`** is a library for interactive 3D visualization + Python, inspired by tools like [Pangolin](https://github.com/stevenlovegrove/Pangolin), [rviz](https://wiki.ros.org/rviz/), [meshcat](https://github.com/rdeits/meshcat), and -[Gradio](https://github.com/gradio-app/gradio). +[Gradio](https://github.com/gradio-app/gradio). It's designed to support +applications in 3D vision and robotics. As a standalone visualization tool, `viser` features include: -- Web interface for easy use on remote machines. -- Python API for sending 3D primitives to the browser. -- Python-configurable inputs: buttons, checkboxes, text inputs, sliders, - dropdowns, gizmos. +- Python API for visualizing 3D primitives in a web browser. +- Python-configurable GUI elements: buttons, checkboxes, text inputs, sliders, + dropdowns, and more. - A [meshcat](https://github.com/rdeits/meshcat) and [tf](http://wiki.ros.org/tf2)-inspired coordinate frame tree. -The `viser.infra` backend can also be used to build custom web applications -(example: -[the original Nerfstudio viewer](https://github.com/nerfstudio-project/nerfstudio)). -It supports: +The `viser.infra` backend can also be used to build custom web applications. It +supports: - Websocket / HTTP server management, on a shared port. - Asynchronous server/client communication infrastructure. @@ -38,7 +57,6 @@ It supports: - Typed serialization; synchronization between Python dataclass and TypeScript interfaces. - ## Installation You can install `viser` with `pip`: @@ -66,8 +84,7 @@ python ./examples/02_gui.py After an example script is running, you can connect by navigating to the printed URL (default: `http://localhost:8080`). -See also: our [development docs](https://nerfstudio-project.github.io/viser/development/). - +See also: our [development docs](https://viser.studio/development/). ## Examples @@ -81,8 +98,10 @@ Source: `./examples/07_record3d_visualizer.py` https://github.com/nerfstudio-project/viser/assets/6992947/c51b4871-6cc8-4987-8751-2bf186bcb1ae -Source: [WangFeng18/3d-gaussian-splatting](https://github.com/WangFeng18/3d-gaussian-splatting) -and [heheyas/gaussian_splatting_3d](https://github.com/heheyas/gaussian_splatting_3d). +Source: +[WangFeng18/3d-gaussian-splatting](https://github.com/WangFeng18/3d-gaussian-splatting) +and +[heheyas/gaussian_splatting_3d](https://github.com/heheyas/gaussian_splatting_3d). **SMPLX visualizer** diff --git a/docs/source/_static/css/custom.css b/docs/source/_static/css/custom.css index 4fbb69843..64b29ec49 100644 --- a/docs/source/_static/css/custom.css +++ b/docs/source/_static/css/custom.css @@ -1,5 +1,5 @@ img.sidebar-logo { - width: 3em; + width: 100%; margin: 1em 0 0 0; } .sidebar-brand-text { diff --git a/docs/source/_static/viser_banner.svg b/docs/source/_static/viser_banner.svg new file mode 120000 index 000000000..3a24b033f --- /dev/null +++ b/docs/source/_static/viser_banner.svg @@ -0,0 +1 @@ +../../../src/viser/client/public/viser_banner.svg \ No newline at end of file diff --git a/docs/source/_static/viser_banner_dark.svg b/docs/source/_static/viser_banner_dark.svg new file mode 100644 index 000000000..cb2af9ad7 --- /dev/null +++ b/docs/source/_static/viser_banner_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 6e7f5fdd1..b6711322a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -72,8 +72,8 @@ "class": "", }, ], - "light_logo": "viser.svg", - "dark_logo": "viser.svg", + "light_logo": "viser_banner.svg", + "dark_logo": "viser_banner_dark.svg", } # Pull documentation types from hints diff --git a/docs/source/examples/08_smplx_visualizer.rst b/docs/source/examples/08_smplx_visualizer.rst index 65ff74f58..402b81b68 100644 --- a/docs/source/examples/08_smplx_visualizer.rst +++ b/docs/source/examples/08_smplx_visualizer.rst @@ -44,7 +44,7 @@ parameters to run this script: share: bool = False, ) -> None: server = viser.ViserServer(share=share) - server.configure_theme(control_layout="collapsible", dark_mode=True) + server.configure_theme(control_layout="collapsible") model = smplx.create( model_path=str(model_path), model_type=model_type, @@ -142,6 +142,7 @@ parameters to run this script: # GUI elements: mesh settings + visibility. with tab_group.add_tab("View", viser.Icon.VIEWFINDER): gui_rgb = server.add_gui_rgb("Color", initial_value=(90, 200, 255)) + gui_rgb_text = server.add_gui_text("Color", "") gui_wireframe = server.add_gui_checkbox("Wireframe", initial_value=False) gui_show_controls = server.add_gui_checkbox("Handles", initial_value=False) diff --git a/docs/source/examples/18_splines.rst b/docs/source/examples/18_splines.rst index b8653f35d..39d408cd5 100644 --- a/docs/source/examples/18_splines.rst +++ b/docs/source/examples/18_splines.rst @@ -22,7 +22,7 @@ Make a ball with some random splines. def main() -> None: server = viser.ViserServer() - for i in range(50): + for i in range(10): positions = onp.random.normal(size=(30, 3)) * 3.0 server.add_spline_catmull_rom( f"/catmull_{i}", @@ -30,6 +30,7 @@ Make a ball with some random splines. tension=0.5, line_width=3.0, color=onp.random.uniform(size=3), + segments=100, ) control_points = onp.random.normal(size=(30 * 2 - 2, 3)) * 3.0 @@ -39,6 +40,7 @@ Make a ball with some random splines. control_points, line_width=3.0, color=onp.random.uniform(size=3), + segments=100, ) while True: diff --git a/docs/source/extras.md b/docs/source/extras.md new file mode 100644 index 000000000..8dd2bf2c6 --- /dev/null +++ b/docs/source/extras.md @@ -0,0 +1,7 @@ +# Extras + + + +.. automodule:: viser.extras + + diff --git a/docs/source/index.md b/docs/source/index.md index 8f5482bc1..9aec1638a 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -2,22 +2,22 @@ |mypy| |nbsp| |pyright| |nbsp| |typescript| |nbsp| |versions| -`viser` is a library for interactive 3D visualization + Python, inspired by +**viser** is a library for interactive 3D visualization + Python, inspired by tools like [Pangolin](https://github.com/stevenlovegrove/Pangolin), -[rviz](https://wiki.ros.org/rviz/), and -[meshcat](https://github.com/rdeits/meshcat). +[rviz](https://wiki.ros.org/rviz/), +[meshcat](https://github.com/rdeits/meshcat), and +[Gradio](https://github.com/gradio-app/gradio). It's designed to support +applications in 3D vision and robotics. As a standalone visualization tool, `viser` features include: -- Web interface for easy use on remote machines. -- Python API for sending 3D primitives to the browser. -- Python-configurable inputs: buttons, checkboxes, text inputs, sliders, - dropdowns, gizmos. -- Support for multiple panels and view-synchronized connections. +- Python API for visualizing 3D primitives in a web browser. +- Python-configurable GUI elements: buttons, checkboxes, text inputs, sliders, + dropdowns, and more. +- A [meshcat](https://github.com/rdeits/meshcat) and + [tf](http://wiki.ros.org/tf2)-inspired coordinate frame tree. -The `viser.infra` backend can also be used to build custom web applications -(example: -[the Nerfstudio viewer](https://github.com/nerfstudio-project/nerfstudio)). It +The `viser.infra` backend can also be used to build custom web applications. It supports: - Websocket / HTTP server management, on a shared port. @@ -39,7 +39,7 @@ pip install -e . # Run an example. pip install -e .[examples] -python ./examples/4_gui.py +python ./examples/02_gui.py ``` After an example script is running, you can connect by navigating to the printed @@ -78,6 +78,7 @@ URL (default: `http://localhost:8080`). ./infrastructure.md ./transforms.md + ./extras.md .. toctree:: :caption: Examples diff --git a/examples/08_smplx_visualizer.py b/examples/08_smplx_visualizer.py index afd3c6550..93b2dc028 100644 --- a/examples/08_smplx_visualizer.py +++ b/examples/08_smplx_visualizer.py @@ -37,7 +37,7 @@ def main( share: bool = False, ) -> None: server = viser.ViserServer(share=share) - server.configure_theme(control_layout="collapsible", dark_mode=True) + server.configure_theme(control_layout="collapsible") model = smplx.create( model_path=str(model_path), model_type=model_type, diff --git a/src/viser/client/public/Inter-VariableFont_slnt,wght.ttf b/src/viser/client/public/Inter-VariableFont_slnt,wght.ttf new file mode 100644 index 000000000..e72470871 Binary files /dev/null and b/src/viser/client/public/Inter-VariableFont_slnt,wght.ttf differ diff --git a/src/viser/client/public/viser_banner.svg b/src/viser/client/public/viser_banner.svg new file mode 100644 index 000000000..18d2e540f --- /dev/null +++ b/src/viser/client/public/viser_banner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/viser/client/public/viser_banner_dark.svg b/src/viser/client/public/viser_banner_dark.svg new file mode 100644 index 000000000..cb2af9ad7 --- /dev/null +++ b/src/viser/client/public/viser_banner_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/viser/client/src/App.tsx b/src/viser/client/src/App.tsx index cb91fe1bb..8dd837b18 100644 --- a/src/viser/client/src/App.tsx +++ b/src/viser/client/src/App.tsx @@ -15,7 +15,15 @@ import { import { BlendFunction, KernelSize } from "postprocessing"; import { SynchronizedCameraControls } from "./CameraControls"; -import { Box, Image, MantineProvider, MediaQuery } from "@mantine/core"; +import { + Anchor, + Box, + Image, + MantineProvider, + MediaQuery, + Modal, + useMantineTheme, +} from "@mantine/core"; import React from "react"; import { SceneNodeThreeObject, UseSceneTree } from "./SceneTree"; @@ -38,6 +46,7 @@ import { ViserModal } from "./Modal"; import { useSceneTreeState } from "./SceneTreeState"; import { GetRenderRequestMessage, Message } from "./WebsocketMessages"; import { makeThrottledMessageSender } from "./WebsocketFunctions"; +import { useDisclosure } from "@mantine/hooks"; export type ViewerContextContents = { // Zustand hooks. @@ -124,6 +133,8 @@ function ViewerRoot() { function ViewerContents() { const viewer = React.useContext(ViewerContext)!; const control_layout = viewer.useGui((state) => state.theme.control_layout); + const [aboutModelOpened, { open: openAbout, close: closeAbout }] = + useDisclosure(false); return ( {viewer.useGui((state) => state.theme.show_logo) ? ( - - - + <> + + + + + + + ) : null} @@ -413,3 +436,40 @@ export function Root() { ); } + +function AboutViser() { + return ( + + + + Viser is a 3D visualization toolkit developed at UC Berkeley. + +

+ + GitHub + +   •   + + Documentation + +

+
+ ); +} diff --git a/src/viser/client/src/ControlPanel/ControlPanel.tsx b/src/viser/client/src/ControlPanel/ControlPanel.tsx index 6d5cdbb4e..e2223be51 100644 --- a/src/viser/client/src/ControlPanel/ControlPanel.tsx +++ b/src/viser/client/src/ControlPanel/ControlPanel.tsx @@ -76,7 +76,7 @@ export default function ControlPanel(props: { const panelContents = ( <> - + @@ -136,15 +136,15 @@ function ConnectionStatus() { return ( <> -
{/* Spacer. */} +
{/* Spacer. */} {(styles) => ( @@ -160,7 +160,7 @@ function ConnectionStatus() { /> )} - + {label !== "" ? label : connected ? "Connected" : "Connecting..."} diff --git a/src/viser/client/src/ControlPanel/FloatingPanel.tsx b/src/viser/client/src/ControlPanel/FloatingPanel.tsx index f60920774..2a832ee8e 100644 --- a/src/viser/client/src/ControlPanel/FloatingPanel.tsx +++ b/src/viser/client/src/ControlPanel/FloatingPanel.tsx @@ -213,8 +213,8 @@ export default function FloatingPanel({ }} > ({ + sx={{ borderRadius: "0.2em 0.2em 0 0", - backgroundColor: - theme.colorScheme === "dark" - ? theme.colors.dark[5] - : theme.colors.gray[1], lineHeight: "1.5em", cursor: "pointer", position: "relative", @@ -259,9 +255,9 @@ FloatingPanel.Handle = function FloatingPanelHandle({ userSelect: "none", display: "flex", alignItems: "center", - padding: "0 0.8em", - height: "2.5em", - })} + padding: "0 0.75em", + height: "2.75em", + }} onClick={() => { const state = panelContext.dragInfo.current; if (state.dragging) { @@ -295,6 +291,13 @@ FloatingPanel.Contents = function FloatingPanelContents({ /* Prevent internals from getting too wide. Needs to match the * width of the wrapper element above. */ w={context.width} + sx={(theme) => ({ + borderTop: "1px solid", + borderColor: + theme.colorScheme == "dark" + ? theme.colors.dark[4] + : theme.colors.gray[3], + })} > {children} diff --git a/src/viser/client/src/ControlPanel/Generated.tsx b/src/viser/client/src/ControlPanel/Generated.tsx index 71961d8b8..d078e9592 100644 --- a/src/viser/client/src/ControlPanel/Generated.tsx +++ b/src/viser/client/src/ControlPanel/Generated.tsx @@ -5,7 +5,7 @@ import { } from "../WebsocketMessages"; import { ViewerContext, ViewerContextContents } from "../App"; import { makeThrottledMessageSender } from "../WebsocketFunctions"; -import { GuiConfig } from "./GuiState"; +import { GuiConfig, computeRelativeLuminance } from "./GuiState"; import { Collapse, Image, @@ -39,9 +39,11 @@ export default function GeneratedGuiContainer({ // We need to take viewer as input in drei's elements, where contexts break. containerId, viewer, + folderDepth, }: { containerId: string; viewer?: ViewerContextContents; + folderDepth?: number; }) { if (viewer === undefined) viewer = React.useContext(ViewerContext)!; @@ -53,23 +55,18 @@ export default function GeneratedGuiContainer({ // Render each GUI element in this container. const out = guiIdSet === undefined ? null : ( - + {[...guiIdSet] .map((id) => guiConfigFromId[id]) .sort((a, b) => a.order - b.order) - .map((conf, index) => { + .map((conf) => { return ( - <> - {conf.type === "GuiAddFolderMessage" && index > 0 ? ( - - ) : null} - - {conf.type === "GuiAddFolderMessage" && - index < guiIdSet.size - 1 ? ( - // Add some whitespace after folders. - - ) : null} - + ); })} @@ -82,16 +79,18 @@ export default function GeneratedGuiContainer({ function GeneratedInput({ conf, viewer, + folderDepth, }: { conf: GuiConfig; viewer?: ViewerContextContents; + folderDepth: number; }) { // Handle GUI input types. if (viewer === undefined) viewer = React.useContext(ViewerContext)!; // Handle nested containers. if (conf.type == "GuiAddFolderMessage") - return ; + return ; if (conf.type == "GuiAddTabGroupMessage") return ; if (conf.type == "GuiAddMarkdownMessage") { @@ -100,7 +99,7 @@ function GeneratedInput({ visible = visible ?? true; if (!visible) return <>; return ( - + Markdown Failed to Render} > @@ -131,7 +130,7 @@ function GeneratedInput({ if (!visible) return <>; let inputColor = - new ColorTranslator(theme.fn.primaryColor()).L > 55.0 + computeRelativeLuminance(theme.fn.primaryColor()) > 50.0 ? theme.colors.gray[9] : theme.white; @@ -142,8 +141,9 @@ function GeneratedInput({ labeled = false; if (conf.color !== null) { inputColor = - new ColorTranslator(theme.colors[conf.color][theme.fn.primaryShade()]) - .L > 55.0 + computeRelativeLuminance( + theme.colors[conf.color][theme.fn.primaryShade()], + ) > 50.0 ? theme.colors.gray[9] : theme.white; } @@ -195,7 +195,16 @@ function GeneratedInput({ ({ + thumb: { + background: theme.fn.primaryColor(), + borderRadius: "0.1em", + height: "0.75em", + width: "0.625em", + }, + })} pt="0.3rem" showLabelOnHover={false} min={conf.min} @@ -207,11 +216,11 @@ function GeneratedInput({ marks={[{ value: conf.min }, { value: conf.max }]} disabled={disabled} /> - - + + {parseInt(conf.min.toFixed(6))} - + {parseInt(conf.max.toFixed(6))} @@ -228,8 +237,15 @@ function GeneratedInput({ hideControls step={conf.step ?? undefined} precision={conf.precision} - sx={{ width: "3rem", height: "1rem", minHeight: "1rem" }} - styles={{ input: { padding: "0.3rem" } }} + sx={{ width: "3rem" }} + styles={{ + input: { + padding: "0.3rem", + letterSpacing: "-0.5px", + minHeight: "1.5rem", + height: "1.5rem", + }, + }} ml="xs" /> @@ -249,6 +265,12 @@ function GeneratedInput({ // Ignore empty values. newValue !== "" && updateValue(newValue); }} + styles={{ + input: { + minHeight: "1.625rem", + height: "1.625rem", + }, + }} disabled={disabled} stepHoldDelay={500} stepHoldInterval={(t) => Math.max(1000 / t ** 2, 25)} @@ -263,6 +285,13 @@ function GeneratedInput({ onChange={(value) => { updateValue(value.target.value); }} + styles={{ + input: { + minHeight: "1.625rem", + height: "1.625rem", + padding: "0 0.5em", + }, + }} disabled={disabled} /> ); @@ -319,11 +348,21 @@ function GeneratedInput({ input = (