From 1539e6cf9cab1716eeaa67f8dab14527152aa3e6 Mon Sep 17 00:00:00 2001 From: jb0gie Date: Fri, 31 Jan 2025 12:32:05 +0000 Subject: [PATCH 1/2] feat: add app freezing and transform controls - Implemented app freezing functionality to prevent movement, rotation, and scaling - Added copy/paste controls for transform fields (position, rotation, scale) - Introduced draggable number inputs with Ctrl+drag and Shift fine control - Enhanced Fields component to support Vector3 and Euler transform types - Added reset transform button and tooltip for transform controls --- src/client/components/InspectPane.js | 635 ++++++++++++++++++++++++++- src/core/entities/App.js | 53 ++- 2 files changed, 665 insertions(+), 23 deletions(-) diff --git a/src/client/components/InspectPane.js b/src/client/components/InspectPane.js index aaa5bf57..96611774 100644 --- a/src/client/components/InspectPane.js +++ b/src/client/components/InspectPane.js @@ -1,5 +1,5 @@ import { css } from '@firebolt-dev/css' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState, useCallback } from 'react' import { BoxIcon, CircleCheckIcon, @@ -9,13 +9,23 @@ import { LoaderIcon, PackageCheckIcon, XIcon, + LockIcon, + UnlockIcon, + CopyIcon, + ClipboardIcon } from 'lucide-react' +import * as THREE from 'three' import { hashFile } from '../../core/utils-client' import { usePane } from './usePane' import { useUpdate } from './useUpdate' import { cls } from './cls' +// Add a global store for copied values +const copiedValues = { + current: null +} + export function InspectPane({ world, entity }) { if (entity.isApp) { return @@ -34,6 +44,7 @@ export function AppPane({ world, app }) { const paneRef = useRef() const headRef = useRef() const [blueprint, setBlueprint] = useState(app.blueprint) + const [frozen, setFrozen] = useState(app.frozen || false) usePane('inspect', paneRef, headRef) useEffect(() => { window.app = app @@ -48,6 +59,13 @@ export function AppPane({ world, app }) { world.blueprints.off('modify', onModify) } }, []) + useEffect(() => { + const onUpdate = () => { + setFrozen(app.frozen) + } + app.on('update', onUpdate) + return () => app.off('update', onUpdate) + }, [app]) const changeModel = async e => { const file = e.target.files[0] if (!file) return @@ -70,12 +88,21 @@ export function AppPane({ world, app }) { // broadcast blueprint change to server + other clients world.network.send('blueprintModified', { id: blueprint.id, version, model: url }) } - const togglePreload = async () => { + const togglePreload = () => { const preload = !blueprint.preload const version = blueprint.version + 1 world.blueprints.modify({ id: blueprint.id, version, preload }) world.network.send('blueprintModified', { id: blueprint.id, version, preload }) } + const toggleLock = () => { + const newValue = !frozen + app.frozen = newValue + world.network.send('entityModified', { + id: app.data.id, + frozen: newValue + }) + setFrozen(newValue) + } return (
Preload
-
+
+ {frozen ? : } + Lock +
@@ -282,25 +312,158 @@ function PlayerPane({ world, player }) { function Fields({ app, blueprint }) { const world = app.world const [fields, setFields] = useState(app.getConfig?.() || []) - const config = blueprint.config + const [position, setPosition] = useState(app.root?.position || new THREE.Vector3()) + const [rotation, setRotation] = useState(app.root?.rotation || new THREE.Euler()) + const [scale, setScale] = useState(app.root?.scale || new THREE.Vector3(1, 1, 1)) + const [frozen, setFrozen] = useState(app.frozen || false) + const config = blueprint.config || {} + + // Update state when app changes + useEffect(() => { + const onUpdate = () => { + if (app.root) { + setPosition(app.root.position.clone()) + setRotation(app.root.rotation.clone()) + setScale(app.root.scale.clone()) + } + setFrozen(app.frozen) + } + + // Initial update + onUpdate() + + // Subscribe to updates + app.on('update', onUpdate) + return () => app.off('update', onUpdate) + }, [app]) + + // Add transform fields + const transformFields = [ + { + type: 'section', + key: 'transform', + label: 'Transform', + }, + { + type: 'vector3', + key: 'position', + label: 'Position', + value: position, + }, + { + type: 'euler', + key: 'rotation', + label: 'Rotation', + value: rotation, + }, + { + type: 'vector3', + key: 'scale', + label: 'Scale', + value: scale, + }, + ...fields + ] + useEffect(() => { app.onConfigure = fn => setFields(fn?.() || []) return () => { app.onConfigure = null } }, []) + const modify = (key, value) => { if (config[key] === value) return + + // Handle transform updates + if (key === 'position' && app.root) { + app.root.position.copy(value) + app.data.position = value.toArray() + world.network.send('entityModified', { + id: app.data.id, + position: app.data.position + }) + if (app.networkPos) { + app.networkPos.pushArray(app.data.position) + } + setPosition(value.clone()) + return + } + + if (key === 'rotation' && app.root) { + // Ensure we maintain the same rotation order + const euler = value.clone() + euler.order = 'YXZ' // Match the app's rotation order + + app.root.rotation.copy(euler) + const quaternion = new THREE.Quaternion().setFromEuler(euler) + app.data.quaternion = quaternion.toArray() + + world.network.send('entityModified', { + id: app.data.id, + quaternion: app.data.quaternion + }) + + if (app.networkQuat) { + app.networkQuat.pushArray(app.data.quaternion) + } + + setRotation(euler) + return + } + + if (key === 'scale' && app.root) { + // Don't allow scale changes if frozen + if (app.frozen) return + + app.root.scale.copy(value) + app.data.scale = value.toArray() + world.network.send('entityModified', { + id: app.data.id, + scale: app.data.scale + }) + setScale(value.clone()) + return + } + + if (key === 'frozen') { + app.frozen = value + world.network.send('entityModified', { + id: app.data.id, + frozen: value + }) + setFrozen(value) + return + } + + // Handle TOD switch + if (key === 'tod') { + config[key] = value + + // update blueprint locally (also rebuilds apps) + const id = blueprint.id + const version = blueprint.version + 1 + world.blueprints.modify({ id, version, config }) + + // broadcast blueprint change to server + other clients + world.network.send('blueprintModified', { id, version, config }) + return + } + + // Update config for other fields config[key] = value + // update blueprint locally (also rebuilds apps) const id = blueprint.id const version = blueprint.version + 1 world.blueprints.modify({ id, version, config }) + // broadcast blueprint change to server + other clients world.network.send('blueprintModified', { id, version, config }) } - return fields.map(field => ( - + + return transformFields.map(field => ( + )) } @@ -310,6 +473,8 @@ const fieldTypes = { textarea: FieldTextArea, file: FieldFile, switch: FieldSwitch, + vector3: FieldVector3, + euler: FieldEuler, empty: () => null, } @@ -325,31 +490,127 @@ function Field({ world, config, field, value, modify }) { return } -function FieldWithLabel({ label, children }) { +function FieldWithLabel({ label, field, value, modify, children }) { + const [hasCopiedValues, setHasCopiedValues] = useState(false) + const isTransformField = field?.type === 'vector3' || field?.type === 'euler' + + const handleCopy = () => { + if (!isTransformField || !value) return + copiedValues.current = { + type: field.type, + value: value.toArray() + } + setHasCopiedValues(true) + } + + const handlePaste = () => { + if (!isTransformField || !copiedValues.current) return + if (copiedValues.current.type !== field.type) return + + const newValue = field.type === 'euler' + ? new THREE.Euler().fromArray(copiedValues.current.value) + : new THREE.Vector3().fromArray(copiedValues.current.value) + + modify(field.key, newValue) + } + + useEffect(() => { + setHasCopiedValues(!!copiedValues.current && copiedValues.current.type === field.type) + }, [field?.type]) + return (
-
{label}
-
{children}
+
+
+ {label} + {isTransformField && ( +
+
+ +
+ {hasCopiedValues && ( +
+ +
+ )} +
+ )} +
+
+
+ {children} +
) } function FieldSection({ world, field, value, modify }) { + const handleReset = () => { + if (field.label === 'Transform') { + modify('position', new THREE.Vector3(0, 0, 0)) + modify('rotation', new THREE.Euler(0, 0, 0)) + modify('scale', new THREE.Vector3(1, 1, 1)) + } + } + return (
-
{field.label}
+
+
+ {field.label} + {field.label === 'Transform' && ( +
+ Hold Ctrl + drag to adjust values.
Hold Shift for fine control. +
+ )} +
+ {field.label === 'Transform' && ( +
+ Reset +
+ )} +
) } @@ -375,7 +719,7 @@ function FieldText({ world, field, value, modify }) { if (localValue !== value) setLocalValue(value) }, [value]) return ( - +