Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
97c383a
Update CameraControls.tsx
mehdikhfifi Aug 22, 2024
6b13c62
Update CameraControls.tsx
mehdikhfifi Aug 22, 2024
731436d
Merge branch 'main' into mehdi/togglable_camcontrols
brentyi Aug 23, 2024
6da0bf5
`1.0.27`
brentyi May 5, 2026
b9a8bab
Merge branch 'main' into mehdi/togglable_camcontrols
oliver-batchelor-work May 5, 2026
6777227
.
oliver-batchelor-work May 5, 2026
fd7591b
.
oliver-batchelor-work May 5, 2026
797ec07
Fix mouse view
oliver-batchelor-work May 5, 2026
16de3cb
bump
oliver-batchelor-work May 5, 2026
b2fba7c
Merge remote-tracking branch 'upstream/main'
oliver-batchelor-work May 5, 2026
6d13a77
Un-invert X drag for FPS
oliver-batchelor-work May 5, 2026
0e35d2b
Inverted Y toggle
oliver-batchelor-work May 5, 2026
bce4b5f
Respect camera up in first-person controls
oliver-batchelor-work May 24, 2026
4e8918e
Make camera mode indicator toggleable
oliver-batchelor-work May 24, 2026
b4b6f76
Fix first-person mouse yaw direction
oliver-batchelor-work May 24, 2026
079465f
Bump version to 1.0.30
oliver-batchelor-work May 24, 2026
50ca878
Build client during package builds
oliver-batchelor-work May 24, 2026
6db3bf5
Fix first-person key and pointer lock state
oliver-batchelor-work May 25, 2026
1f8589f
Fix first-person arrow key yaw direction
oliver-batchelor-work May 25, 2026
682ca59
Preserve orbit state when exiting first-person
oliver-batchelor-work Jun 4, 2026
891d88b
Centralize first-person mode transitions
oliver-batchelor-work Jun 4, 2026
18fb1a2
Stop orbit controls updating during first-person
oliver-batchelor-work Jun 4, 2026
d625e6f
Build client outside source tree
oliver-batchelor-work Jun 8, 2026
d3ce0e3
Fix first-person arrow key yaw direction
oliver-batchelor-work Jun 13, 2026
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
93 changes: 93 additions & 0 deletions hatch_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

from hatchling.builders.hooks.plugin.interface import BuildHookInterface


class CustomBuildHook(BuildHookInterface):
generated_build_dir: Path | None = None

def initialize(self, version: str, build_data: dict[str, object]) -> None:
if os.environ.get("VISER_SKIP_CLIENT_BUILD") == "1":
return

root = Path(self.root)
client_dir = root / "src" / "viser" / "client"
if not (client_dir / "package.json").exists():
return

with tempfile.TemporaryDirectory(prefix="viser-client-build-") as temp_dir:
temp_client_dir = Path(temp_dir) / "client"
shutil.copytree(
client_dir,
temp_client_dir,
ignore=shutil.ignore_patterns(".nodeenv", "node_modules", "build"),
)

build_client(temp_client_dir)

build_dir = client_dir / "build"
shutil.rmtree(build_dir, ignore_errors=True)
shutil.copytree(temp_client_dir / "build", build_dir)
self.generated_build_dir = build_dir

def finalize(
self, version: str, build_data: dict[str, object], artifact_path: str
) -> None:
if self.generated_build_dir is not None:
shutil.rmtree(self.generated_build_dir, ignore_errors=True)


def build_client(client_dir: Path) -> None:
node_bin_dir = install_sandboxed_node(client_dir)
npm_path = node_bin_dir / "npm"
if sys.platform == "win32":
npm_path = npm_path.with_suffix(".cmd")

env = os.environ.copy()
env["NODE_VIRTUAL_ENV"] = str(node_bin_dir.parent)
env["PATH"] = (
str(node_bin_dir) + (";" if sys.platform == "win32" else ":") + env["PATH"]
)

subprocess.run([str(npm_path), "ci"], cwd=client_dir, env=env, check=True)
subprocess.run(
[str(npm_path), "run", "build"],
cwd=client_dir,
env=env,
check=True,
)


def install_sandboxed_node(client_dir: Path) -> Path:
env_dir = client_dir / ".nodeenv"

def get_node_bin_dir() -> Path:
node_bin_dir = env_dir / "bin"
if not node_bin_dir.exists():
node_bin_dir = env_dir / "Scripts"
return node_bin_dir

node_bin_dir = get_node_bin_dir()

npx_path = node_bin_dir / "npx"
if sys.platform == "win32":
npx_path = npx_path.with_suffix(".cmd")
if npx_path.exists():
return node_bin_dir

subprocess.run(
[sys.executable, "-m", "nodeenv", "--node=24.12.0", str(env_dir)],
check=True,
)
node_bin_dir = get_node_bin_dir()
npx_path = node_bin_dir / "npx"
if sys.platform == "win32":
npx_path = npx_path.with_suffix(".cmd")
if not npx_path.exists():
raise RuntimeError(f"nodeenv did not create {npx_path}")
return node_bin_dir
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
[build-system]
requires = ["hatchling"]
requires = ["hatchling", "nodeenv>=1.9.1,<2.0.0"]
build-backend = "hatchling.build"

[tool.hatch.build]
exclude = ["src/viser/client/.nodeenv", "src/viser/client/node_modules", "**/__pycache__/**"]
# Client build is in the gitignore, but we still want it in the distribution.
ignore-vcs = true

[tool.hatch.build.hooks.custom]

[tool.hatch.version]
path = "src/viser/__init__.py"

Expand Down
2 changes: 1 addition & 1 deletion src/viser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,4 @@
if not _TYPE_CHECKING:
from ._scene_handles import ScenePointerEvent as ScenePointerEvent

__version__ = "1.0.26"
__version__ = "1.0.36"
6 changes: 3 additions & 3 deletions src/viser/_client_autobuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,10 @@ def _build_viser_client(out_dir: Path, cached: bool = True) -> None:
npm_path = npm_path.with_suffix(".cmd")

subprocess.run(
args=[str(npm_path), "install"],
args=[str(npm_path), "ci"],
env=subprocess_env,
cwd=client_dir,
check=False,
check=True,
)
subprocess.run(
args=[
Expand All @@ -154,7 +154,7 @@ def _build_viser_client(out_dir: Path, cached: bool = True) -> None:
],
env=subprocess_env,
cwd=client_dir,
check=False,
check=True,
)


Expand Down
52 changes: 50 additions & 2 deletions src/viser/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React, { useEffect, useMemo } from "react";
import { ViewerMutable } from "./ViewerContext";
import {
Anchor,
ActionIcon,
Box,
Divider,
Image,
Expand All @@ -24,6 +25,7 @@ import {
useMantineColorScheme,
useMantineTheme,
} from "@mantine/core";
import { IconCamera, IconView360 } from "@tabler/icons-react";
import { useDisclosure } from "@mantine/hooks";

// Local imports.
Expand Down Expand Up @@ -202,6 +204,7 @@ function ViewerRoot() {
: () => null,
sendCamera: null,
resetCameraPose: null,
setFirstPersonMode: () => null,

// DOM/Three.js references.
canvas: null,
Expand Down Expand Up @@ -455,6 +458,46 @@ function NotificationsPanel() {
);
}

/** Viewport camera-mode control. */
function CameraModeIndicator() {
const viewer = React.useContext(ViewerContext)!;
const firstPerson = viewer.useGui((state) => state.firstPersonCamera);
const dark = viewer.useGui((state) => state.theme.dark_mode);
const label = firstPerson ? "First person mode" : "Orbit mode";
const nextLabel = firstPerson ? "orbit mode" : "first-person mode";
const Icon = firstPerson ? IconCamera : IconView360;

const toggleMode = () => {
viewer.mutable.current.setFirstPersonMode(!firstPerson);
};

return (
<Tooltip
label={`${label}. Click to switch to ${nextLabel}, or press P.`}
position="left"
withArrow
>
<ActionIcon
aria-label={`${label}. Switch to ${nextLabel}.`}
onClick={toggleMode}
variant="subtle"
color="gray"
style={{
position: "absolute",
bottom: "0.6rem",
right: "0.6rem",
zIndex: 2,
backgroundColor: dark ? "rgba(0,0,0,0.55)" : "rgba(255,255,255,0.85)",
color: dark ? "#e9ecef" : "#212529",
boxShadow: "0 1px 4px rgba(0,0,0,0.15)",
}}
>
<Icon size="1rem" />
</ActionIcon>
</Tooltip>
);
}

/**
* Main 3D canvas component.
*/
Expand Down Expand Up @@ -505,7 +548,9 @@ function ViewerCanvas({ children }: { children: React.ReactNode }) {
pointerInfo.modifierAtDown = modifier;
pointerInfo.activeEventTypes = activeEventTypes;
pointerInfo.isDragging = true;
mutable.current.cameraControl!.enabled = false;
if (!viewer.useGui.get().firstPersonCamera) {
mutable.current.cameraControl!.enabled = false;
}

const ctx = mutable.current.canvas2d!.getContext("2d")!;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
Expand Down Expand Up @@ -562,12 +607,14 @@ function ViewerCanvas({ children }: { children: React.ReactNode }) {
// Reset gesture state and erase the rectangle overlay before any
// early return -- otherwise a server callback removed mid-gesture
// can leave stale ``isDragging`` or a drawn rectangle behind.
mutable.current.cameraControl!.enabled = true;
pointerInfo.isDragging = false;
const activeEventTypes = pointerInfo.activeEventTypes;
pointerInfo.activeEventTypes = new Set();
const ctx = mutable.current.canvas2d!.getContext("2d")!;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
if (!viewer.useGui.get().firstPersonCamera) {
mutable.current.cameraControl!.enabled = true;
}
if (!wasDragging || activeEventTypes.size === 0) return;

const modifier = pointerInfo.modifierAtDown;
Expand Down Expand Up @@ -629,6 +676,7 @@ function ViewerCanvas({ children }: { children: React.ReactNode }) {
{!inView && <DisableRender />}
{sceneContents}
</Canvas>
<CameraModeIndicator />
</div>
);
}
Expand Down
Loading