Compare commits

...

27 Commits

Author SHA1 Message Date
d9b2feac01 chore: focus enabled and the outline is none 2025-06-09 14:10:27 -05:00
a278596233 fix: root target for state transition 2025-06-09 11:22:43 -05:00
5fda3093bb fix: added a workflow to properly disconnect the mouse when the driver stops 2025-06-06 15:00:40 -05:00
e6c31a8aea fix: typescript errors 2025-06-06 14:46:53 -05:00
743d126492 chore: typescript, error crashing. 2025-06-06 13:58:31 -05:00
917e63fd15 chore: adding a bunch of comments 2025-06-06 13:11:48 -05:00
91e959a5fd fix: initial connection and spamming events works on windowsgaga 2025-06-06 11:28:29 -05:00
55be1e5785 chore: saving off broken progress this software is unusable 2025-06-05 15:11:24 -05:00
031dc5c54f chore: some more debugging? 2025-06-04 11:59:05 -05:00
1f728fdfe5 chore: I removed the .connect() call, ope 2025-06-04 11:34:32 -05:00
1dfdb87de6 chore: implementing a mouse retry manual connection 2025-06-04 11:04:42 -05:00
fc47bc36ed chore: error message prefix for all 3dconnexion error handling 2025-06-04 10:44:45 -05:00
8c868e147d chore: implementing a retry system for 3 attempts after it fails once automatically 2025-06-04 10:36:15 -05:00
598cf46f9e chore: writing more skeleton logic for initializing the mouse and error handling properly for multiple cycles 2025-06-03 14:27:15 -05:00
d191c8ba1d chore: more skeleton for initializing the mouse 2025-06-03 14:03:16 -05:00
4386e31ecb chore: getting a skeleon for an xstate machine to connect the 3dconnexion mouse 2025-06-03 13:36:20 -05:00
af4c7990bd fix: some PR clean up 2025-06-02 16:45:14 -05:00
1aca603ecf fix: more cleanup 2025-06-02 12:51:15 -05:00
eabef0a7f3 fix: cleaning up code 2025-06-02 12:48:58 -05:00
9049c368fa fix: auto fmt 2025-06-02 11:56:51 -05:00
3d22fc8138 fix: saving off a ton of progress, need to do a big cleanup. Rotation logic is borked 2025-06-02 11:51:34 -05:00
666312d475 fix: saving off progress 2025-05-30 16:26:21 -05:00
c2a3dcdd28 huh 2025-05-30 14:57:17 -05:00
24e4ec55e6 fix: saving off 2025-05-30 09:13:35 -05:00
9ccd216aaf fix:test 2025-05-30 09:13:24 -05:00
28058e2e3c fix: external mouse init 2025-05-29 14:36:02 -05:00
7c3842b5df fix: saving off some init 2025-05-29 14:35:27 -05:00
17 changed files with 1920 additions and 4 deletions

View File

@ -16,6 +16,7 @@
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="./inter/inter.css" />
<script type="text/javascript" src="/vendor/3dconnexion.min.js"></script>
<script
defer
data-domain="app.zoo.dev"

72
public/vendor/3dconnexion.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -8,6 +8,7 @@ import {
useNavigate,
useSearchParams,
} from 'react-router-dom'
import * as THREE from 'three'
import { AppHeader } from '@src/components/AppHeader'
import { EngineStream } from '@src/components/EngineStream'
@ -57,6 +58,7 @@ import { VITE_KC_SITE_BASE_URL } from '@src/env'
// CYCLIC REF
sceneInfra.camControls.engineStreamActor = engineStreamActor
window.THREE = THREE
maybeWriteToDisk()
.then(() => {})
.catch(() => {})

View File

@ -1332,7 +1332,7 @@ function calculateNearFarFromFOV(fov: number) {
return { z_near: 0.01, z_far: 1000 }
}
function convertThreeCamValuesToEngineCam({
export function convertThreeCamValuesToEngineCam({
target,
position,
quaternion,

View File

@ -281,6 +281,18 @@ export class SceneInfra {
this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
/**
* Required for 3dconnexion mouse navigation library API
* Gotcha: Canvas elements are not focusable by default
* Set an id to easily access the canvas
* tabIndex=0 allows for focus
* autofocus is helpful? It seemed to act poorly withou it.
*/
this.renderer.domElement.id = 'client-side-scene-canvas'
this.renderer.domElement.tabIndex = 0
this.renderer.domElement.autofocus = true
this.renderer.domElement.style.outline = 'none'
// LABEL RENDERER
this.labelRenderer = new CSS2DRenderer()
this.labelRenderer.setSize(window.innerWidth, window.innerHeight)

View File

@ -36,8 +36,9 @@ import { useRouteLoaderData } from 'react-router-dom'
import { createThumbnailPNGOnDesktop } from '@src/lib/screenshot'
import type { SettingsViaQueryString } from '@src/lib/settings/settingsTypes'
import { resetCameraPosition } from '@src/lib/resetCameraPosition'
import { _3DMouseThreeJS } from '@src/lib/externalMouse/external-mouse-threejs'
const TIME_1_SECOND = 1000
let once = false
export const EngineStream = (props: {
pool: string | null
@ -190,6 +191,20 @@ export const EngineStream = (props: {
projectDirectoryWithoutEndingSlash: project.path,
})
}
if (!once) {
/* const the3DMouse = new _3DMouseThreeJS({
* // Name needs to be registered in the python proxy server!
* name: 'zoo-design-studio',
* debug: true,
* canvasId: 'client-side-scene-canvas',
* camera: sceneInfra.camControls.camera.clone(),
* })
* window.the3DMouse = the3DMouse
* the3DMouse.init3DMouse() */
once = true
}
})
.catch(trap)
}

View File

@ -0,0 +1,70 @@
import { Popover } from '@headlessui/react'
import { useSelector } from '@xstate/react'
import { CustomIcon } from '@src/components/CustomIcon'
import { _3dMouseActor, sceneInfra } from '@src/lib/singletons'
import { ActionButton } from '@src/components/ActionButton'
import {
_3DMouseMachineEvents,
_3DMouseMachineStates,
} from '@src/machines/_3dMouse/utils'
export const ExternalMouseIndicator = ({
className,
}: {
className?: string
}) => {
const useMouseState = useSelector(_3dMouseActor, (state) => {
return state.value
})
return (
<Popover className="relative">
<Popover.Button
className={
'flex items-center p-0 border-none bg-transparent dark:bg-transparent relative ' +
(className || '')
}
data-testid="network-machine-toggle"
>
<CustomIcon name="keyboard" className="w-5 h-5" />
</Popover.Button>
<Popover.Panel
className="absolute right-0 left-auto bottom-full mb-1 w-64 flex flex-col gap-1 align-stretch bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm"
data-testid="network-popover"
>
<div className="flex items-center justify-between p-2 rounded-t-sm bg-chalkboard-20 dark:bg-chalkboard-80">
<h2 className="text-sm font-sans font-normal">
External mouse state
</h2>
<p
data-testid="network"
className="font-bold text-xs uppercase px-2 py-1 rounded-sm"
>
{useMouseState}
</p>
</div>
<ActionButton
Element="button"
disabled={useMouseState !== _3DMouseMachineStates.waitingToConnect}
onClick={() =>
_3dMouseActor.send({
type: _3DMouseMachineEvents.connect,
data: {
name: 'zoo-design-studio',
debug: false,
canvasId: 'client-side-scene-canvas',
camera: sceneInfra.camControls.camera,
},
})
}
className={
'flex items-center p-2 gap-2 leading-tight border-transparent dark:border-transparent enabled:dark:border-transparent enabled:hover:border-primary/50 enabled:dark:hover:border-inherit active:border-primary dark:bg-transparent hover:bg-transparent'
}
data-testid="home-new-file"
>
Connect mouse
</ActionButton>
</Popover.Panel>
</Popover>
)
}

View File

@ -20,6 +20,7 @@ import { ActionButton } from '@src/components/ActionButton'
import { isDesktop } from '@src/lib/isDesktop'
import { VITE_KC_SITE_BASE_URL } from '@src/env'
import { APP_DOWNLOAD_PATH } from '@src/lib/constants'
import { ExternalMouseIndicator } from '@src/components/ExternalMouseIndicator'
export function LowerRightControls({
children,
@ -117,6 +118,9 @@ export function LowerRightControls({
</Tooltip>
</Link>
<NetworkMachineIndicator className={linkOverrideClassName} />
{!location.pathname.startsWith(PATHS.HOME) && (
<ExternalMouseIndicator />
)}
{!location.pathname.startsWith(PATHS.HOME) && (
<NetworkHealthIndicator />
)}

View File

@ -2019,6 +2019,7 @@ export class EngineCommandManager extends EventTarget {
const cmd = command.cmd
if (
(cmd.type === 'camera_drag_move' ||
cmd.type === 'default_camera_look_at' ||
cmd.type === 'handle_mouse_drag_move' ||
cmd.type === 'default_camera_zoom' ||
cmd.type === ('default_camera_perspective_settings' as any)) &&

View File

@ -217,3 +217,6 @@ export const POOL_QUERY_PARAM = 'pool'
* @deprecated: supporting old share links with this. For new command URLs, use "cmd"
*/
export const CREATE_FILE_URL_PARAM = 'create-file'
/** Error prefix for console.errors when working with the 3dconnexion mouse connection*/
export const EXTERNAL_MOUSE_ERROR_PREFIX = '[3dconnexion]'

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,9 @@ import {
engineViewIsometricWithoutGeometryPresent,
engineViewIsometricWithGeometryPresent,
} from '@src/lib/utils'
import { AxisNames } from '@src/lib/constants'
import { v4 } from 'uuid'
const uuidv4 = v4
/**
* Reset the camera position to a baseline, which is isometric for
@ -31,6 +34,21 @@ export async function resetCameraPosition() {
const cameraProjection =
settingsActor.getSnapshot().context.modeling.cameraProjection.current
// TODO: Kevin remove after debugging
// sceneInfra.camControls.updateCameraToAxis(AxisNames.NEG_Y)
// /**
// * HACK: We need to update the gizmo, the command above doesn't trigger gizmo
// * to render which makes the axis point in an old direction.
// */
// await engineCommandManager.sendSceneCommand({
// type: 'modeling_cmd_req',
// cmd_id: uuidv4(),
// cmd: {
// type: 'default_camera_get_settings',
// },
// })
// return
// We need to keep the users projection setting when resetting their camera
if (cameraProjection === 'perspective') {
await sceneInfra.camControls.usePerspectiveCamera()

View File

@ -34,6 +34,7 @@ import type { AppMachineContext } from '@src/lib/types'
import { createAuthCommands } from '@src/lib/commandBarConfigs/authCommandConfig'
import { commandBarMachine } from '@src/machines/commandBarMachine'
import { createProjectCommands } from '@src/lib/commandBarConfigs/projectsCommandConfig'
import { _3DMouseMachine } from '@src/machines/_3dMouse/_3dMouseMachine'
export const codeManager = new CodeManager()
export const engineCommandManager = new EngineCommandManager()
@ -117,8 +118,15 @@ if (typeof window !== 'undefined') {
},
})
}
const { AUTH, SETTINGS, SYSTEM_IO, ENGINE_STREAM, COMMAND_BAR, BILLING } =
ACTOR_IDS
const {
AUTH,
SETTINGS,
SYSTEM_IO,
ENGINE_STREAM,
COMMAND_BAR,
BILLING,
_3DMOUSE,
} = ACTOR_IDS
const appMachineActors = {
[AUTH]: authMachine,
[SETTINGS]: settingsMachine,
@ -126,6 +134,7 @@ const appMachineActors = {
[ENGINE_STREAM]: engineStreamMachine,
[COMMAND_BAR]: commandBarMachine,
[BILLING]: billingMachine,
[_3DMOUSE]: _3DMouseMachine,
} as const
const appMachine = setup({
@ -173,6 +182,9 @@ const appMachine = setup({
urlUserService: VITE_KC_API_BASE_URL,
},
}),
spawnChild(appMachineActors[_3DMOUSE], {
systemId: _3DMOUSE,
}),
],
})
@ -240,6 +252,12 @@ export const useCommandBarState = () => {
return useSelector(commandBarActor, cmdBarStateSelector)
}
export const _3dMouseActor = appActor.system.get(_3DMOUSE) as ActorRefFrom<
(typeof appMachineActors)[typeof _3DMOUSE]
>
window.dog = _3dMouseActor
// Initialize global commands
commandBarActor.send({
type: 'Add commands',

View File

@ -0,0 +1,266 @@
import { assertEvent, assign, fromPromise, setup } from 'xstate'
import { OrthographicCamera, PerspectiveCamera } from 'three'
import {
_3DMouseThreeJS,
_3DMouseThreeJSWindows,
} from '@src/lib/externalMouse/external-mouse-threejs'
import { EXTERNAL_MOUSE_ERROR_PREFIX } from '@src/lib/constants'
import {
_3DMouseContext,
_3DMouseMachineStates,
_3DMouseMachineEvents,
_3DMouseMachineActors,
} from '@src/machines/_3dMouse/utils'
function logError(message: string) {
console.error(`${EXTERNAL_MOUSE_ERROR_PREFIX} ${message}`)
}
export const _3DMouseMachine = setup({
types: {
context: {} as _3DMouseContext,
events: {} as
| {
type: _3DMouseMachineEvents.connect
data: {
name: string
debug: boolean
canvasId: string
/** Allow null because of internal retry, it will fail if this is null, we cannot have a default camera*/
camera: PerspectiveCamera | OrthographicCamera | null
onDisconnect: () => void
}
}
| {
type: _3DMouseMachineEvents.done_connect
output: _3DMouseThreeJSWindows
}
| {
type: _3DMouseMachineEvents.error_connect
}
| {
type: _3DMouseMachineEvents.disconnect
},
},
actions: {},
actors: {
[_3DMouseMachineActors.connect]: fromPromise(
async ({
input,
}: {
input: {
context: _3DMouseContext
name: string
debug: boolean
canvasId: string
camera: PerspectiveCamera | OrthographicCamera | null
onDisconnect: () => void
}
}): Promise<_3DMouseThreeJSWindows> => {
console.error('I AM CONNECTING TOO MANY TIMES')
/** * Global variable reference from html script tag loading */
if (!_3Dconnexion) {
const message = '_3Dconnexion library missing.'
logError(message)
throw message
}
/** Check if the canvas is present, it checks in _3DMouseThreeJS as well */
const canvas: HTMLCanvasElement | null = document.querySelector(
'#' + input.canvasId
)
if (!canvas) {
const message = `Unable to find canvas with id: ${input.canvasId}`
logError(message)
throw message
}
/** Make sure the camera is available */
if (!input.camera) {
const message = `Unable to find initial client scene camera`
logError(message)
throw message
}
// const the3DMouse = new _3DMouseThreeJS({
// // Name needs to be registered in the python proxy server!
// name: input.name,
// debug: input.debug,
// canvasId: input.canvasId,
// camera: input.camera.clone(),
// })
// delete old mouse before creating a new one!
// This is important when someone disconnects and we reconnect
if (input.context._3dMouse) {
input.context._3dMouse.destroy()
}
const the3DMouse = new _3DMouseThreeJSWindows({
// Name needs to be registered in the python proxy server!
appName: input.name,
debug: input.debug,
canvasId: input.canvasId,
camera: input.camera.clone(),
TRACE_MESSAGES: true,
disconnectCallback: input.onDisconnect,
})
/**
* The mouse class has a bug and we cannot properly await the async xmlHttpRequest
* we have to poll and hope it connects
*/
const response = await the3DMouse.init3DMouse(1000 * 2)
console.log('RESPONSE', response)
if (response.value === false) {
logError(response.message)
the3DMouse.destroy()
throw response.message
}
return the3DMouse
}
),
},
}).createMachine({
initial: _3DMouseMachineStates.waitingToConnect,
context: () => ({
_3dMouse: null,
lastConfigurationForConnection: null,
retries: 0,
/** retry 3 times before the user needs to manually click a connect button to retry */
maxRetries: 3,
}),
on: {
[_3DMouseMachineEvents.disconnect]: {
// root state
target: '.' + _3DMouseMachineStates.waitingToConnect,
},
},
states: {
[_3DMouseMachineStates.waitingToConnect]: {
on: {
[_3DMouseMachineEvents.connect]: {
target: _3DMouseMachineStates.connecting,
actions: [
assign({
lastConfigurationForConnection: ({ event }) => {
const { name, debug, canvasId, camera } = event.data
return { name, debug, canvasId, camera }
},
}),
],
},
},
},
[_3DMouseMachineStates.connecting]: {
invoke: {
id: _3DMouseMachineActors.connect,
src: _3DMouseMachineActors.connect,
input: ({ context, event, self }) => {
assertEvent(event, _3DMouseMachineEvents.connect)
const onDisconnectHelperFunction = () => {
self.send({ type: _3DMouseMachineEvents.disconnect })
}
return {
context,
name: event.data.name,
debug: event.data.debug,
canvasId: event.data.canvasId,
camera: event.data.camera,
onDisconnect: onDisconnectHelperFunction,
}
},
onDone: {
target: _3DMouseMachineStates.connected,
actions: [
assign({
_3dMouse: ({ event }) => {
assertEvent(event, _3DMouseMachineEvents.done_connect)
return event.output
},
}),
],
},
onError: {
target: _3DMouseMachineStates.failedToConnect,
},
},
},
[_3DMouseMachineStates.connected]: {
on: {},
},
[_3DMouseMachineStates.failedToConnect]: {
target: _3DMouseMachineStates.waitingToConnect,
always: [
{
guard: ({ context }) =>
context.retries < context.maxRetries &&
context.lastConfigurationForConnection !== null,
target: _3DMouseMachineStates.retryConnection,
actions: assign({ retries: ({ context }) => context.retries + 1 }),
},
{
target: _3DMouseMachineStates.waitingToConnect,
/**
* After we fail 3 times, go back to the initial state and wait for a connect event that is externally triggered via .send()
* force clear the lastConfigurationForConnection
*/
actions: [
assign({ retries: () => 0 }),
assign({ lastConfigurationForConnection: () => null }),
],
},
],
},
[_3DMouseMachineStates.retryConnection]: {
invoke: {
id: _3DMouseMachineActors.connect,
src: _3DMouseMachineActors.connect,
input: ({ context, event, self }) => {
assertEvent(event, _3DMouseMachineEvents.error_connect)
const onDisconnectHelperFunction = () => {
self.send({ type: _3DMouseMachineEvents.disconnect })
}
let { name, debug, canvasId, camera } =
context.lastConfigurationForConnection || {
name: '',
debug: false,
canvasId: '',
camera: null,
}
logError(
`retrying connection automatically, retry:${context.retries} out of ${context.maxRetries}`
)
return {
context,
name,
debug,
canvasId,
camera,
onDisconnect: onDisconnectHelperFunction,
}
},
onDone: {
target: _3DMouseMachineStates.connected,
actions: [
assign({
_3dMouse: ({ event }) => {
assertEvent(event, _3DMouseMachineEvents.done_connect)
return event.output
},
}),
],
},
onError: {
target: _3DMouseMachineStates.failedToConnect,
},
},
},
},
})

View File

@ -0,0 +1,40 @@
import { OrthographicCamera, PerspectiveCamera } from 'three'
import { _3DMouseThreeJSWindows } from '@src/lib/externalMouse/external-mouse-threejs'
const donePrefix = 'xstate.done.actor.'
const errorPrefix = 'xstate.error.actor.'
export type _3DMouseContext = {
_3dMouse: _3DMouseThreeJSWindows | null
/** Use this object for internal retries on behalf of the user */
lastConfigurationForConnection: {
name: string
debug: boolean
canvasId: string
camera: PerspectiveCamera | OrthographicCamera | null
} | null
retries: number
maxRetries: number
}
export enum _3DMouseMachineStates {
waitingToConnect = 'waiting to connect',
connecting = 'connecting',
connected = 'connected',
failedToConnect = 'failed to connect',
/** Automatic reconnection, this will internally transistion.
* I do not recommend making an event to transition here from a .send() call
*/
retryConnection = 'retry connection',
}
export enum _3DMouseMachineEvents {
connect = 'connect',
done_connect = donePrefix + 'connect',
error_connect = errorPrefix + 'connect',
disconnect = 'disconnect',
}
export enum _3DMouseMachineActors {
connect = 'connect',
}

View File

@ -5,4 +5,5 @@ export const ACTOR_IDS = {
ENGINE_STREAM: 'engine_stream',
COMMAND_BAR: 'command_bar',
BILLING: 'billing',
_3DMOUSE: '3dmouse',
} as const

View File

@ -79,6 +79,17 @@ process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??=
console.log('Environment vars', process.env)
console.log('Parsed CLI args', args)
// SSL/TSL: this is the self signed certificate support
app.on(
'certificate-error',
(event, webContents, url, error, certificate, callback) => {
// On certificate error we disable default behaviour (stop loading the page)
// and we then say "it is all fine - true" to the callback
event.preventDefault()
callback(true)
}
)
/// Register our application to handle all "zoo-studio:" protocols.
const singleInstanceLock = app.requestSingleInstanceLock()
if (process.defaultApp) {