@viamrobotics/motion-tools 1.21.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +18 -100
  2. package/dist/FrameConfigUpdater.svelte.d.ts +0 -1
  3. package/dist/FrameConfigUpdater.svelte.js +6 -24
  4. package/dist/components/App.svelte +3 -3
  5. package/dist/components/App.svelte.d.ts +1 -1
  6. package/dist/components/CameraControls.svelte +6 -6
  7. package/dist/components/Entities/Pose.svelte +18 -13
  8. package/dist/components/FileDrop/useFileDrop.svelte.js +16 -2
  9. package/dist/components/{KeyboardControls.svelte → InputBindings.svelte} +50 -77
  10. package/dist/components/InputBindings.svelte.d.ts +7 -0
  11. package/dist/components/PointerMissBox.svelte +1 -1
  12. package/dist/components/Scene.svelte +2 -0
  13. package/dist/components/SceneProviders.svelte +2 -0
  14. package/dist/components/SelectedTransformControls.svelte +227 -0
  15. package/dist/components/SelectedTransformControls.svelte.d.ts +3 -0
  16. package/dist/components/StaticGeometries.svelte +3 -56
  17. package/dist/components/overlay/Details.svelte +82 -54
  18. package/dist/components/overlay/dashboard/Button.svelte +4 -2
  19. package/dist/components/overlay/dashboard/Button.svelte.d.ts +1 -1
  20. package/dist/components/overlay/dashboard/Dashboard.svelte +43 -33
  21. package/dist/ecs/traits.d.ts +15 -0
  22. package/dist/ecs/traits.js +7 -0
  23. package/dist/editing/FrameEditSession.d.ts +37 -0
  24. package/dist/editing/FrameEditSession.js +178 -0
  25. package/dist/hooks/useEnvironment.svelte.d.ts +1 -0
  26. package/dist/hooks/useEnvironment.svelte.js +1 -0
  27. package/dist/hooks/useFrameEditSession.svelte.d.ts +15 -0
  28. package/dist/hooks/useFrameEditSession.svelte.js +36 -0
  29. package/dist/hooks/useFrames.svelte.js +45 -5
  30. package/dist/hooks/usePartConfig.svelte.js +10 -0
  31. package/dist/hooks/useSettings.svelte.d.ts +1 -3
  32. package/dist/hooks/useSettings.svelte.js +1 -3
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +2 -0
  35. package/dist/transform.js +13 -0
  36. package/package.json +8 -6
  37. package/dist/components/KeyboardControls.svelte.d.ts +0 -7
package/README.md CHANGED
@@ -1,119 +1,37 @@
1
- # motion-tools
1
+ # Viam Visualization
2
2
 
3
- `motion-tools` aims to provide a visualization interface for any spatial information using Viam's APIs. This typically means motion-related monitoring, testing, and debugging.
3
+ 3D visualization interface for spatial data using Viam's APIs frame systems, geometries, point clouds, drawings — for motion-related monitoring, testing, and debugging.
4
4
 
5
- ## Getting started
5
+ ## Documentation
6
6
 
7
- To run the app, you will need to set your project up by installing the app dependencies and ensuring you can connect
8
- to your machines if required.
7
+ 📚 **[viamrobotics.github.io/visualization](https://viamrobotics.github.io/visualization/)** is the canonical guide. It covers:
9
8
 
10
- ### Project setup
9
+ - [Running locally](https://viamrobotics.github.io/visualization/guides/local-usage/) — set up the app and drive it from Go via `client/api`.
10
+ - [Embedding `<MotionTools />`](https://viamrobotics.github.io/visualization/guides/embedding/) — drop the visualizer into your own Svelte app.
11
+ - [Implementing WorldStateStoreService](https://viamrobotics.github.io/visualization/guides/worldstatestore/) — produce `Transform`s for a Viam WSS module with `draw`.
12
+ - API reference for [`client/api`](https://viamrobotics.github.io/visualization/api/client-api/) and [`draw`](https://viamrobotics.github.io/visualization/api/draw/).
13
+ - [v1 → v2 migration guide](https://viamrobotics.github.io/visualization/migration/v1-to-v2/).
14
+ - A live [playground](https://viamrobotics.github.io/visualization/playground/) rendering a baked snapshot.
11
15
 
12
- The easiest way to get started is using our automated setup script:
16
+ ## Quick start
13
17
 
14
18
  ```bash
15
- make setup
19
+ make setup # one-time: install Node 22, pnpm, bun, Go, buf, project deps
20
+ make up # http://localhost:5173
16
21
  ```
17
22
 
18
- This single command will:
23
+ For manual setup, machine configs, multiple instances, and troubleshooting see the [local-usage guide](https://viamrobotics.github.io/visualization/guides/local-usage/).
19
24
 
20
- 1. Install **fnm** (Fast Node Manager) and **Node.js 22**
21
- 2. Install **pnpm** package manager
22
- 3. Install **bun** runtime
23
- 4. Install **Go** and **buf** (for protobuf generation)
24
- 5. Install all project dependencies
25
- 6. Generate protobuf code
25
+ ## Contributing
26
26
 
27
- After setup completes, add the shell configuration it prints to your shell config file (`~/.zshrc` or `~/.bashrc`), then restart your terminal.
28
-
29
- #### Manual setup
30
-
31
- If the above does not work for you, or if you prefer to install dependencies manually:
32
-
33
- 1. [Install fnm](https://github.com/Schniz/fnm#installation): `curl -fsSL https://fnm.vercel.app/install | bash`
34
- 2. Install Node.js: `fnm install 22 && fnm use 22`
35
- 3. [Install pnpm](https://pnpm.io/installation): `curl -fsSL https://get.pnpm.io/install.sh | sh -`
36
- 4. [Install bun](https://bun.sh/docs/installation): `curl -fsSL https://bun.sh/install | bash`
37
- 5. [Install Go](https://go.dev/doc/install)
38
- 6. [Install buf](https://buf.build/docs/installation): download from GitHub releases
39
- 7. Install Go tools: `go install google.golang.org/protobuf/cmd/protoc-gen-go@latest`
40
- 8. Install dependencies: `pnpm install`
41
- 9. Generate protobufs: `make proto`
42
-
43
- ### Env files for machine configs
44
-
45
- To add a list of connection configs in an `.env.local` file, use the following format:
46
-
47
- ```
48
- VITE_CONFIGS='
49
- {
50
- "fleet-rover-01": {
51
- "host": "fleet-rover-01-main.ve4ba7w5qr.viam.cloud",
52
- "partId": "myPartID",
53
- "apiKeyId": "myApiKeyId",
54
- "apiKeyValue": "MyApiKeyValue",
55
- "signalingAddress": "https://app.viam.com:443"
56
- }
57
- }
58
- '
59
- ```
60
-
61
- ### Running the app locally
62
-
63
- After setup completes, you can start a local app server with:
27
+ Run the dev server with HMR:
64
28
 
65
29
  ```bash
66
- make up
67
- ```
68
-
69
- This starts the app as a static site. The build part of the process will only run if you have not built the app yet as a part of `make up`, or your build is out of date.
70
-
71
- #### Running multiple apps
72
-
73
- If you want to be able to run multiple versions of the app, you can configure how the servers run. The `make up` command can accept two options:
74
-
75
- 1. `STATIC_PORT` is the port for the static file server, and defaults to `5173`
76
- 2. `WS_PORT` is the port for the websocket server used to communicate with the draw client API
77
-
78
- > [!NOTE]
79
- > The `WS_PORT` is not fully configurable at the moment, so passing it will only affect where the frontend listens for the websocket server, but calls with the draw client API are currently hardcoded to point to `"http://localhost:3000/"`. If this is a feature you require, please submit a request to the viz team!
80
-
81
- To run two apps using the same web socket server, run:
82
-
83
- ```
84
- # in one terminal
85
- make up
86
-
87
- # in another terminal
88
- make up STATIC_PORT=5174
89
- ```
90
-
91
- The apps should be available on `http://localhost:5173/` and `http://localhost:5174/`, and calls to the draw client API should render in both.
92
-
93
- ### Local development
94
-
95
- If you are contributing to `motion-tools`, you should just run the development web server with:
96
-
97
- ```
98
30
  pnpm dev
99
31
  ```
100
32
 
101
- ## Running the visualizer
102
-
103
- To visit the visualizer, go to `http://localhost:5173/`
104
-
105
- Open the machine config page (bottom right) and enter in connection details to visualize a specific machine. You can also add machine configs from an env file (see below).
106
-
107
- ## Executing drawing commands
108
-
109
- The visualizer includes a golang package that allows executing commands to the visualizer.
110
-
111
- The list of available commands [can be found here](https://pkg.go.dev/github.com/viam-labs/motion-tools@v0.9.0/client/client).
33
+ See [CLAUDE.md](CLAUDE.md) for contributor conventions.
112
34
 
113
35
  ## Programmatic camera control
114
36
 
115
- It is possible to programmatically move the viewer camera and even modify the camera settings during runtime.
116
-
117
- To do this, open the Javascript console while using the visualizer and call methods or set properties on the `cameraControls` object.
118
-
119
- The following APIs are available: https://github.com/yomotsu/camera-controls?tab=readme-ov-file#properties
37
+ The visualizer exposes a `cameraControls` object on `window`. Open the browser console and call methods on it to move the camera or tweak its settings at runtime. Full API: <https://github.com/yomotsu/camera-controls#properties>.
@@ -23,6 +23,5 @@ export declare class FrameConfigUpdater {
23
23
  setFrameParent: (entity: Entity, parentName: string) => void;
24
24
  deleteFrame: (entity: Entity) => void;
25
25
  setGeometryType: (entity: Entity, type: "none" | "box" | "sphere" | "capsule") => void;
26
- private sanitizeFloatValue;
27
26
  }
28
27
  export {};
@@ -7,9 +7,7 @@ export class FrameConfigUpdater {
7
7
  this.removeFrame = removeFrame;
8
8
  }
9
9
  updateLocalPosition = (entity, position) => {
10
- const x = this.sanitizeFloatValue(position.x);
11
- const y = this.sanitizeFloatValue(position.y);
12
- const z = this.sanitizeFloatValue(position.z);
10
+ const { x, y, z } = position;
13
11
  if (x === undefined && y === undefined && z === undefined)
14
12
  return;
15
13
  const change = {};
@@ -28,10 +26,7 @@ export class FrameConfigUpdater {
28
26
  }
29
27
  };
30
28
  updateLocalOrientation = (entity, orientation) => {
31
- const oX = this.sanitizeFloatValue(orientation.oX);
32
- const oY = this.sanitizeFloatValue(orientation.oY);
33
- const oZ = this.sanitizeFloatValue(orientation.oZ);
34
- const theta = this.sanitizeFloatValue(orientation.theta);
29
+ const { oX, oY, oZ, theta } = orientation;
35
30
  if (oX === undefined && oY === undefined && oZ === undefined && theta === undefined) {
36
31
  return;
37
32
  }
@@ -57,9 +52,7 @@ export class FrameConfigUpdater {
57
52
  const parent = entity.get(traits.Parent) ?? 'world';
58
53
  const pose = entity.get(traits.EditedPose);
59
54
  if (geometry?.type === 'box') {
60
- const x = this.sanitizeFloatValue(geometry.x);
61
- const y = this.sanitizeFloatValue(geometry.y);
62
- const z = this.sanitizeFloatValue(geometry.z);
55
+ const { x, y, z } = geometry;
63
56
  if (x === undefined && y === undefined && z === undefined)
64
57
  return;
65
58
  const change = {};
@@ -76,7 +69,7 @@ export class FrameConfigUpdater {
76
69
  }
77
70
  }
78
71
  else if (geometry?.type === 'sphere') {
79
- const r = this.sanitizeFloatValue(geometry.r);
72
+ const { r } = geometry;
80
73
  if (r === undefined)
81
74
  return;
82
75
  entity.set(traits.Sphere, { r });
@@ -86,8 +79,7 @@ export class FrameConfigUpdater {
86
79
  }
87
80
  }
88
81
  else if (geometry?.type === 'capsule') {
89
- const r = this.sanitizeFloatValue(geometry.r);
90
- const l = this.sanitizeFloatValue(geometry.l);
82
+ const { r, l } = geometry;
91
83
  if (r === undefined && l === undefined)
92
84
  return;
93
85
  const change = {};
@@ -95,7 +87,7 @@ export class FrameConfigUpdater {
95
87
  change.r = r;
96
88
  if (l !== undefined)
97
89
  change.l = l;
98
- entity.set(traits.Capsule, { r, l });
90
+ entity.set(traits.Capsule, change);
99
91
  const capsule = entity.get(traits.Capsule);
100
92
  if (name && capsule && pose) {
101
93
  this.updateFrame(name, parent, pose, { type: 'capsule', ...capsule });
@@ -134,14 +126,4 @@ export class FrameConfigUpdater {
134
126
  this.updateFrame(name, parent, pose, { type: 'capsule', r: 20, l: 100 });
135
127
  }
136
128
  };
137
- sanitizeFloatValue = (value) => {
138
- if (value === undefined) {
139
- return undefined;
140
- }
141
- const num = Number.parseFloat(value.toFixed(2));
142
- if (Number.isNaN(num)) {
143
- return undefined;
144
- }
145
- return value;
146
- };
147
129
  }
@@ -47,7 +47,7 @@
47
47
 
48
48
  interface Props {
49
49
  partID?: string
50
- enableKeybindings?: boolean
50
+ inputBindingsEnabled?: boolean
51
51
  localConfigProps?: LocalConfigProps
52
52
  drawConnectionConfig?: DrawConnectionConfig
53
53
 
@@ -74,7 +74,7 @@
74
74
 
75
75
  let {
76
76
  partID = '',
77
- enableKeybindings = true,
77
+ inputBindingsEnabled = true,
78
78
  localConfigProps,
79
79
  cameraPose,
80
80
  drawConnectionConfig,
@@ -91,7 +91,7 @@
91
91
  const { isPresenting } = useXR()
92
92
 
93
93
  $effect(() => {
94
- settings.current.enableKeybindings = enableKeybindings
94
+ environment.current.inputBindingsEnabled = inputBindingsEnabled
95
95
  })
96
96
 
97
97
  createPartIDContext(() => partID)
@@ -11,7 +11,7 @@ interface LocalConfigProps {
11
11
  }
12
12
  interface Props {
13
13
  partID?: string;
14
- enableKeybindings?: boolean;
14
+ inputBindingsEnabled?: boolean;
15
15
  localConfigProps?: LocalConfigProps;
16
16
  drawConnectionConfig?: DrawConnectionConfig;
17
17
  /**
@@ -4,15 +4,15 @@
4
4
 
5
5
  import Button from './overlay/dashboard/Button.svelte'
6
6
  import { useCameraControls, useTransformControls } from '../hooks/useControls.svelte'
7
- import { useSettings } from '../hooks/useSettings.svelte'
7
+ import { useEnvironment } from '../hooks/useEnvironment.svelte'
8
8
 
9
- import KeyboardControls from './KeyboardControls.svelte'
9
+ import InputBindings from './InputBindings.svelte'
10
10
 
11
11
  const cameraControls = useCameraControls()
12
- const settings = useSettings()
12
+ const environment = useEnvironment()
13
13
  const transformControls = useTransformControls()
14
14
 
15
- const enableKeybindings = $derived(settings.current.enableKeybindings)
15
+ const inputBindingsEnabled = $derived(environment.current.inputBindingsEnabled)
16
16
  </script>
17
17
 
18
18
  <Portal id="dashboard">
@@ -37,8 +37,8 @@
37
37
  }}
38
38
  >
39
39
  {#snippet children({ ref }: { ref: CameraControlsRef })}
40
- {#if enableKeybindings}
41
- <KeyboardControls cameraControls={ref} />
40
+ {#if inputBindingsEnabled}
41
+ <InputBindings cameraControls={ref} />
42
42
  {/if}
43
43
  <Gizmo placement="bottom-right" />
44
44
  {/snippet}
@@ -6,7 +6,7 @@
6
6
  import { traits, useTrait } from '../../ecs'
7
7
  import { usePartConfig } from '../../hooks/usePartConfig.svelte'
8
8
  import { usePose } from '../../hooks/usePose.svelte'
9
- import { matrixToPose, poseToMatrix } from '../../transform'
9
+ import { composeRenderedPose } from '../../transform'
10
10
 
11
11
  interface Props {
12
12
  entity: Entity
@@ -25,22 +25,27 @@
25
25
  () => parent.current
26
26
  )
27
27
 
28
- const resolvedPose = $derived.by(() => {
29
- if (pose.current === undefined || partConfig.hasPendingSave) {
30
- return editedPose.current
31
- }
28
+ $effect.pre(() => {
29
+ if (pose.current === undefined) return
32
30
 
33
- if (!entityPose.current || !editedPose.current) {
34
- return
31
+ if (entity.has(traits.LivePose)) {
32
+ entity.set(traits.LivePose, pose.current)
33
+ } else {
34
+ entity.add(traits.LivePose(pose.current))
35
35
  }
36
+ })
36
37
 
37
- const poseNetwork = poseToMatrix(entityPose.current)
38
- const poseUsePose = poseToMatrix(pose.current)
39
- const poseLocalEditedPose = poseToMatrix(editedPose.current)
38
+ // Always render through the live blend: live × network⁻¹ × edited. With
39
+ // `edited === network` (no edits) this collapses to `live`, so the rendered
40
+ // pose tracks the robot's kinematics-resolved position. With edits, the
41
+ // formula composes the staged delta on top of live. Input handlers that
42
+ // drive edits (gizmo onChange, Details panel) compute `edited` such that
43
+ // the blend renders to the user's intent.
44
+ const resolvedPose = $derived.by(() => {
45
+ if (pose.current === undefined || partConfig.hasPendingSave) return editedPose.current
46
+ if (!entityPose.current || !editedPose.current) return undefined
40
47
 
41
- const poseNetworkInverse = poseNetwork.invert()
42
- const resultMatrix = poseUsePose.multiply(poseNetworkInverse).multiply(poseLocalEditedPose)
43
- return matrixToPose(resultMatrix)
48
+ return composeRenderedPose(pose.current, entityPose.current, editedPose.current)
44
49
  })
45
50
  </script>
46
51
 
@@ -2,6 +2,9 @@ import { Extensions, parseFileName, Prefixes, readFile } from './file-names';
2
2
  import { pcdDropper } from './pcd-dropper';
3
3
  import { plyDropper } from './ply-dropper';
4
4
  import { snapshotDropper } from './snapshot-dropper';
5
+ const hasDraggedFiles = (dataTransfer) => {
6
+ return dataTransfer?.types?.includes('Files') ?? false;
7
+ };
5
8
  const createFileDropper = (extension, prefix) => {
6
9
  switch (prefix) {
7
10
  case Prefixes.Snapshot: {
@@ -22,11 +25,15 @@ export const useFileDrop = (onSuccess, onError) => {
22
25
  let dropState = $state('inactive');
23
26
  // prevent default to allow drop
24
27
  const ondragenter = (event) => {
28
+ if (!hasDraggedFiles(event.dataTransfer))
29
+ return;
25
30
  event.preventDefault();
26
31
  dropState = 'hovering';
27
32
  };
28
33
  // prevent default to allow drop
29
34
  const ondragover = (event) => {
35
+ if (!hasDraggedFiles(event.dataTransfer))
36
+ return;
30
37
  event.preventDefault();
31
38
  };
32
39
  const ondragleave = (event) => {
@@ -40,10 +47,17 @@ export const useFileDrop = (onSuccess, onError) => {
40
47
  dropState = 'inactive';
41
48
  };
42
49
  const ondrop = (event) => {
50
+ const { dataTransfer } = event;
51
+ if (dataTransfer === null || !hasDraggedFiles(dataTransfer)) {
52
+ dropState = 'inactive';
53
+ return;
54
+ }
43
55
  event.preventDefault();
44
- if (event.dataTransfer === null)
56
+ const { files } = dataTransfer;
57
+ if (files.length === 0) {
58
+ dropState = 'inactive';
45
59
  return;
46
- const { files } = event.dataTransfer;
60
+ }
47
61
  let completed = 0;
48
62
  for (const file of files) {
49
63
  const fileName = parseFileName(file.name);
@@ -2,6 +2,7 @@
2
2
  import type { CameraControlsRef } from '@threlte/extras'
3
3
 
4
4
  import { isInstanceOf, useTask } from '@threlte/core'
5
+ import { useGamepad, useInputMap, useKeyboard } from '@threlte/extras'
5
6
  import { PressedKeys } from 'runed'
6
7
  import { MathUtils, Vector3 } from 'three'
7
8
 
@@ -22,19 +23,33 @@
22
23
 
23
24
  const settings = useSettings()
24
25
 
25
- const keys = new PressedKeys()
26
- const meta = $derived(keys.has('meta'))
27
- const w = $derived(keys.has('w'))
28
- const s = $derived(keys.has('s'))
29
- const a = $derived(keys.has('a'))
30
- const d = $derived(keys.has('d'))
31
- const r = $derived(keys.has('r'))
32
- const f = $derived(keys.has('f'))
33
- const up = $derived(keys.has('arrowup'))
34
- const left = $derived(keys.has('arrowleft'))
35
- const down = $derived(keys.has('arrowdown'))
36
- const right = $derived(keys.has('arrowright'))
37
- const anyKeysPressed = $derived(w || s || a || d || r || f || up || left || down || right)
26
+ const keyboard = useKeyboard()
27
+ const gamepad = useGamepad()
28
+ const input = useInputMap(
29
+ ({ key, gamepadAxis, gamepadButton }) => ({
30
+ truckLeft: [key('a'), gamepadAxis('leftStick', 'x', -1)],
31
+ truckRight: [key('d'), gamepadAxis('leftStick', 'x', 1)],
32
+ forward: [key('w'), gamepadAxis('leftStick', 'y', -1)],
33
+ backward: [key('s'), gamepadAxis('leftStick', 'y', 1)],
34
+ dollyIn: [key('r'), gamepadButton('rightBumper')],
35
+ dollyOut: [key('f'), gamepadButton('leftBumper')],
36
+ rotateLeft: [key('arrowleft'), gamepadAxis('rightStick', 'x', -1)],
37
+ rotateRight: [key('arrowright'), gamepadAxis('rightStick', 'x', 1)],
38
+ tiltUp: [key('arrowup'), gamepadAxis('rightStick', 'y', 1)],
39
+ tiltDown: [key('arrowdown'), gamepadAxis('rightStick', 'y', -1)],
40
+ }),
41
+ { keyboard, gamepad }
42
+ )
43
+
44
+ const truckAxis = $derived(input.axis('truckLeft', 'truckRight'))
45
+ const forwardAxis = $derived(input.axis('backward', 'forward'))
46
+ const dollyAxis = $derived(input.axis('dollyOut', 'dollyIn'))
47
+ const yawAxis = $derived(input.axis('rotateLeft', 'rotateRight'))
48
+ const pitchAxis = $derived(input.axis('tiltUp', 'tiltDown'))
49
+
50
+ const anyKeysPressed = $derived(
51
+ truckAxis !== 0 || forwardAxis !== 0 || dollyAxis !== 0 || yawAxis !== 0 || pitchAxis !== 0
52
+ )
38
53
 
39
54
  const target = new Vector3()
40
55
 
@@ -70,7 +85,7 @@
70
85
  const dt = delta * 1000
71
86
 
72
87
  // Disallow keyboard navigation if the user is holding down the meta key
73
- if (meta) {
88
+ if (keyboard.key('meta').pressed) {
74
89
  return
75
90
  }
76
91
 
@@ -80,64 +95,41 @@
80
95
  const dollySpeed = 0.005 * dt
81
96
  const zoomSpeed = 0.5 * dt
82
97
 
83
- if (a) {
84
- cameraControls.truck(-moveSpeed * dt, 0, true)
85
- }
86
-
87
- if (d) {
88
- cameraControls.truck(moveSpeed * dt, 0, true)
89
- }
90
-
91
- if (w) {
92
- cameraControls.forward(moveSpeed * dt, true)
93
- }
94
-
95
- if (s) {
96
- cameraControls.forward(-moveSpeed * dt, true)
98
+ if (truckAxis !== 0) {
99
+ cameraControls.truck(truckAxis * moveSpeed * dt, 0, true)
97
100
  }
98
101
 
99
- if (r) {
100
- if (isInstanceOf(cameraControls.camera, 'PerspectiveCamera')) {
101
- cameraControls.dolly(dollySpeed, true)
102
- } else {
103
- cameraControls.zoom(zoomSpeed, true)
104
- }
102
+ if (forwardAxis !== 0) {
103
+ cameraControls.forward(forwardAxis * moveSpeed * dt, true)
105
104
  }
106
105
 
107
- if (f) {
106
+ if (dollyAxis !== 0) {
108
107
  if (isInstanceOf(cameraControls.camera, 'PerspectiveCamera')) {
109
- cameraControls.dolly(-dollySpeed, true)
108
+ cameraControls.dolly(dollyAxis * dollySpeed, true)
110
109
  } else {
111
- cameraControls.zoom(-zoomSpeed, true)
110
+ cameraControls.zoom(dollyAxis * zoomSpeed, true)
112
111
  }
113
112
  }
114
113
 
115
- if (left) {
116
- cameraControls.rotate(-rotateSpeed, 0, true)
117
- }
118
-
119
- if (right) {
120
- cameraControls.rotate(rotateSpeed, 0, true)
114
+ if (yawAxis !== 0) {
115
+ cameraControls.rotate(yawAxis * rotateSpeed, 0, true)
121
116
  }
122
117
 
123
- if (up) {
124
- cameraControls.rotate(0, -tiltSpeed, true)
125
- }
126
-
127
- if (down) {
128
- cameraControls.rotate(0, tiltSpeed, true)
118
+ if (pitchAxis !== 0) {
119
+ cameraControls.rotate(0, pitchAxis * tiltSpeed, true)
129
120
  }
130
121
  },
131
122
  {
123
+ after: input.task,
132
124
  running: () => anyKeysPressed,
133
125
  autoInvalidate: false,
134
126
  }
135
127
  )
136
128
 
129
+ const keys = new PressedKeys()
130
+
137
131
  keys.onKeys('escape', () => {
138
- if (keys.has('escape')) {
139
- focusedEntity.set()
140
- }
132
+ focusedEntity.set()
141
133
  })
142
134
 
143
135
  keys.onKeys('c', () => {
@@ -157,30 +149,11 @@
157
149
  settings.current.transformMode = 'scale'
158
150
  })
159
151
 
160
- keys.onKeys('x', () => {
161
- settings.current.enableXR = !settings.current.enableXR
162
- })
163
-
164
- /**
165
- * Handler for any keybindings that need to access the event object
166
- */
167
- const onkeydown = (event: KeyboardEvent) => {
168
- const key = event.key.toLowerCase()
169
-
170
- if (key === 'h') {
171
- if (!entity) return
172
-
173
- event.stopImmediatePropagation()
174
-
175
- if (entity.has(traits.Invisible)) {
176
- entity.remove(traits.Invisible)
177
- } else {
178
- entity.add(traits.Invisible)
179
- }
180
-
181
- return
152
+ keys.onKeys('h', () => {
153
+ if (entity?.has(traits.Invisible)) {
154
+ entity.remove(traits.Invisible)
155
+ } else {
156
+ entity?.add(traits.Invisible)
182
157
  }
183
- }
158
+ })
184
159
  </script>
185
-
186
- <svelte:window {onkeydown} />
@@ -0,0 +1,7 @@
1
+ import type { CameraControlsRef } from '@threlte/extras';
2
+ interface Props {
3
+ cameraControls: CameraControlsRef;
4
+ }
5
+ declare const InputBindings: import("svelte").Component<Props, {}, "">;
6
+ type InputBindings = ReturnType<typeof InputBindings>;
7
+ export default InputBindings;
@@ -23,7 +23,7 @@
23
23
  onpointerdown={() => {
24
24
  cameraDown.copy(camera.current.position)
25
25
  }}
26
- onpointerup={() => {
26
+ onclick={() => {
27
27
  if (transformControls.active) {
28
28
  return
29
29
  }
@@ -10,6 +10,7 @@
10
10
  import Entities from './Entities/Entities.svelte'
11
11
  import Focus from './Focus.svelte'
12
12
  import Selected from './Selected.svelte'
13
+ import SelectedTransformControls from './SelectedTransformControls.svelte'
13
14
  import StaticGeometries from './StaticGeometries.svelte'
14
15
  import { useFocusedObject3d } from '../hooks/useSelection.svelte'
15
16
  import { useSettings } from '../hooks/useSettings.svelte'
@@ -77,6 +78,7 @@
77
78
 
78
79
  <StaticGeometries />
79
80
  <Selected />
81
+ <SelectedTransformControls />
80
82
 
81
83
  {#if !$isPresenting && settings.current.grid}
82
84
  <Grid
@@ -12,6 +12,7 @@
12
12
  } from '../hooks/useControls.svelte'
13
13
  import { provideDrawAPI } from '../hooks/useDrawAPI.svelte'
14
14
  import { provideDrawService } from '../hooks/useDrawService.svelte'
15
+ import { provideFrameEditSession } from '../hooks/useFrameEditSession.svelte'
15
16
  import { provideFramelessComponents } from '../hooks/useFramelessComponents.svelte'
16
17
  import { provideFrames } from '../hooks/useFrames.svelte'
17
18
  import { provideGeometries } from '../hooks/useGeometries.svelte'
@@ -47,6 +48,7 @@
47
48
 
48
49
  provideResourceByName(() => partID.current)
49
50
  provideConfigFrames()
51
+ provideFrameEditSession(() => partID.current)
50
52
  provideFrames(() => partID.current)
51
53
  provideGeometries(() => partID.current)
52
54
  provide3DModels(() => partID.current)