From 2f4bf852ed1128326d162de477682a38c488eb5a Mon Sep 17 00:00:00 2001 From: Nicolas Echezarreta Date: Tue, 12 Nov 2024 13:25:32 -0300 Subject: [PATCH 1/6] set gizmo free movement as default --- .../inspector/src/lib/sdk/operations/update-selected-entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@dcl/inspector/src/lib/sdk/operations/update-selected-entity.ts b/packages/@dcl/inspector/src/lib/sdk/operations/update-selected-entity.ts index 460cde85c..40fee3476 100644 --- a/packages/@dcl/inspector/src/lib/sdk/operations/update-selected-entity.ts +++ b/packages/@dcl/inspector/src/lib/sdk/operations/update-selected-entity.ts @@ -21,7 +21,7 @@ function isAncestorOf(ancestorId: Entity, targetId: Entity, nodes: Node[]): bool export function updateSelectedEntity(engine: IEngine) { return function updateSelectedEntity(entity: Entity, multiple: boolean = false) { - let gizmo = GizmoType.POSITION + let gizmo = GizmoType.FREE let deletedSelection = false // clear selection From be588f8cc10949633f524b421993bf3a1a064951 Mon Sep 17 00:00:00 2001 From: Nicolas Echezarreta Date: Wed, 13 Nov 2024 12:13:35 -0300 Subject: [PATCH 2/6] split to SingleEntityInspector & MultipleEntityInspector --- .../EntityHeader/EntityHeader.tsx | 2 +- .../EntityInspector/EntityInspector.tsx | 55 ++++++++++++++++--- .../TransformInspector/TransformInspector.tsx | 3 +- .../TransformInspector/types.ts | 2 +- .../src/hooks/sdk/useComponentInput.ts | 4 +- .../src/hooks/sdk/useComponentValue.ts | 9 +-- 6 files changed, 53 insertions(+), 22 deletions(-) diff --git a/packages/@dcl/inspector/src/components/EntityInspector/EntityHeader/EntityHeader.tsx b/packages/@dcl/inspector/src/components/EntityInspector/EntityHeader/EntityHeader.tsx index 90abcc9dc..58ef574d1 100644 --- a/packages/@dcl/inspector/src/components/EntityInspector/EntityHeader/EntityHeader.tsx +++ b/packages/@dcl/inspector/src/components/EntityInspector/EntityHeader/EntityHeader.tsx @@ -32,7 +32,7 @@ interface ModalState { cb?: () => void } -const getLabel = (sdk: SdkContextValue, entity: Entity) => { +export const getLabel = (sdk: SdkContextValue, entity: Entity) => { const nameComponent = sdk.components.Name.getOrNull(entity) switch (entity) { case ROOT: diff --git a/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx b/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx index 79fb78f24..1fd8129f1 100644 --- a/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx +++ b/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx @@ -1,10 +1,12 @@ +import { Entity } from '@dcl/ecs' import { useEffect, useMemo, useState } from 'react' import { withSdk } from '../../hoc/withSdk' import { useChange } from '../../hooks/sdk/useChange' -import { useSelectedEntity } from '../../hooks/sdk/useSelectedEntity' +import { useEntitiesWith } from '../../hooks/sdk/useEntitiesWith' import { useAppSelector } from '../../redux/hooks' import { getHiddenComponents } from '../../redux/ui' +import { EDITOR_ENTITIES } from '../../lib/sdk/tree' import { GltfInspector } from './GltfInspector' import { ActionInspector } from './ActionInspector' @@ -32,8 +34,42 @@ import { SmartItemBasicView } from './SmartItemBasicView' import './EntityInspector.css' -export const EntityInspector = withSdk(({ sdk }) => { - const entity = useSelectedEntity() +export function EntityInspector() { + const selectedEntities = useEntitiesWith((components) => components.Selection) + const ownedEntities = useMemo( + () => selectedEntities.filter((entity) => !EDITOR_ENTITIES.includes(entity)), + [selectedEntities] + ) + const entity = useMemo(() => (selectedEntities.length > 0 ? selectedEntities[0] : null), [selectedEntities]) + + if (ownedEntities.length > 1) { + return + } + + return +} + +const MultiEntityInspector = withSdk<{ entities: Entity[] }>(({ sdk, entities }) => { + const hiddenComponents = useAppSelector(getHiddenComponents) + const inspectors = useMemo( + () => [{ name: sdk.components.Transform.componentName, component: TransformInspector }], + [sdk] + ) + + return ( +
+
+
{entities.length} entities selected
+
+ {inspectors.map( + ({ name, component: Inspector }, index) => + !hiddenComponents[name] && + )} +
+ ) +}) + +const SingleEntityInspector = withSdk<{ entity: Entity | null }>(({ sdk, entity }) => { const hiddenComponents = useAppSelector(getHiddenComponents) const [isBasicViewEnabled, setIsBasicViewEnabled] = useState(false) @@ -123,13 +159,13 @@ export const EntityInspector = withSdk(({ sdk }) => { ) return ( -
+
{entity !== null ? ( <> {inspectors.map( ({ name, component: Inspector }, index) => - !hiddenComponents[name] && + !hiddenComponents[name] && )} {isBasicViewEnabled ? ( @@ -140,9 +176,10 @@ export const EntityInspector = withSdk(({ sdk }) => { ) )} - ) : null} -
- ) -}) + ) : null} +
+ ) + } +) export default EntityInspector diff --git a/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx b/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx index c5b073f9d..86284925d 100644 --- a/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx +++ b/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx @@ -13,8 +13,9 @@ import { Link, Props as LinkProps } from './Link' import './TransformInspector.css' -export default withSdk(({ sdk, entity }) => { +export default withSdk(({ sdk, entities }) => { const { Transform, TransformConfig } = sdk.components + const entity = entities.find((entity) => Transform.has(entity)) || entities[0] const hasTransform = useHasComponent(entity, Transform) const transform = Transform.getOrNull(entity) ?? undefined diff --git a/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/types.ts b/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/types.ts index 037115990..d0aead046 100644 --- a/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/types.ts +++ b/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/types.ts @@ -1,7 +1,7 @@ import { Entity } from '@dcl/ecs' export interface Props { - entity: Entity + entities: Entity[] } export type TransformInput = { diff --git a/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts b/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts index 2b80b7d1f..03edb7616 100644 --- a/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts +++ b/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts @@ -70,10 +70,8 @@ export const useComponentInput = (entity: Entity, component: // sync state -> engine useEffect(() => { - if (value === null) return - const isEqualValue = !recursiveCheck(getComponentValue(entity, component), value, 2) - - if (isEqualValue) { - return - } + if (value === null || isComponentEqual(value)) return if (isLastWriteWinComponent(component) && sdk) { sdk.operations.updateValue(component, entity, value!) void sdk.operations.dispatch() @@ -48,7 +43,7 @@ export const useComponentValue = (entity: Entity, component: (event) => { if (entity === event.entity && component.componentId === event.component?.componentId && !!event.value) { if (event.operation === CrdtMessageType.PUT_COMPONENT) { - // TODO: This setValue is generating a isEqual comparission. + // TODO: This setValue is generating an isEqual comparission in previous effect. // Maybe we have to use two pure functions instead of an effect. // Same happens with the input & componentValue. setValue(event.value) From 212b957aee1a95d4eff4301eda646932740c7bc6 Mon Sep 17 00:00:00 2001 From: Nicolas Echezarreta Date: Wed, 13 Nov 2024 12:48:04 -0300 Subject: [PATCH 3/6] magic stuff for multiple entity Transform --- .../EntityInspector/EntityInspector.tsx | 9 +- .../TransformInspector/TransformInspector.tsx | 6 +- .../src/hooks/sdk/useComponentInput.ts | 137 ++++++++++++++++++ 3 files changed, 144 insertions(+), 8 deletions(-) diff --git a/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx b/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx index 1fd8129f1..3e5ecd6d9 100644 --- a/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx +++ b/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx @@ -176,10 +176,9 @@ const SingleEntityInspector = withSdk<{ entity: Entity | null }>(({ sdk, entity ) )} - ) : null} - - ) - } -) + ) : null} + + ) +}) export default EntityInspector diff --git a/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx b/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx index 86284925d..2ca684c10 100644 --- a/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx +++ b/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { isValidNumericInput, useComponentInput } from '../../../hooks/sdk/useComponentInput' +import { isValidNumericInput, useComponentInput, useComponentInput2 } from '../../../hooks/sdk/useComponentInput' import { useHasComponent } from '../../../hooks/sdk/useHasComponent' import { withSdk } from '../../../hoc/withSdk' @@ -20,8 +20,8 @@ export default withSdk(({ sdk, entities }) => { const hasTransform = useHasComponent(entity, Transform) const transform = Transform.getOrNull(entity) ?? undefined const config = TransformConfig.getOrNull(entity) ?? undefined - const { getInputProps } = useComponentInput( - entity, + const { getInputProps } = useComponentInput2( + entities, Transform, fromTransform, toTransform(transform, config), diff --git a/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts b/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts index 03edb7616..e709d993b 100644 --- a/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts +++ b/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts @@ -113,3 +113,140 @@ export const useComponentInput = ( + values: ComponentValueType[], + fromComponentValueToInput: (componentValue: ComponentValueType) => InputType +): InputType => { + // Transform all component values to input format + const inputs = values.map(fromComponentValueToInput) + + // Get first input as reference + const firstInput = inputs[0] + + // Create result object with same shape as first input + const result = {} as InputType + + // For each key in first input + for (const key in firstInput) { + const firstValue = firstInput[key] + + // Check if all inputs have same value for this key + const allSame = inputs.every((input) => { + const value = input[key] + if (typeof value === 'object' && value !== null) { + // For objects, compare stringified versions + return JSON.stringify(value) === JSON.stringify(firstValue) + } + return value === firstValue + }) + + // Set result value based on whether all inputs match + result[key] = allSame ? firstValue : ('--' as any) + } + + return result +} + +export const useComponentInput2 = ( + entities: Entity[], + component: Component, + fromComponentValueToInput: (componentValue: ComponentValueType) => InputType, + fromInputToComponentValue: (input: InputType) => ComponentValueType, + validateInput: (input: InputType) => boolean = () => true, + deps: unknown[] = [] +) => { + const componentValues = entities.map((entity) => useComponentValue(entity, component)) + const [componentValue, _, isEqual] = componentValues[0] + const asd = mergeComponentValues( + componentValues.map(([value]) => value), + fromComponentValueToInput + ) + const [input, setInput] = useState( + componentValue === null ? null : fromComponentValueToInput(componentValue) + ) + const [focusedOn, setFocusedOn] = useState(null) + const skipSyncRef = useRef(false) + const [isValid, setIsValid] = useState(true) + + const updateInputs = useCallback((value: InputType | null, skipSync = false) => { + skipSyncRef.current = skipSync + setInput(value) + }, []) + + const handleUpdate = + (path: NestedKey, getter: (event: React.ChangeEvent) => any = (e) => e.target.value) => + (event: React.ChangeEvent) => { + if (input === null) return + const newInputs = setValue(input, path, getter(event)) + updateInputs(newInputs) + } + + const handleFocus = useCallback( + (path: NestedKey) => () => { + setFocusedOn(path) + }, + [] + ) + + const handleBlur = useCallback(() => { + if (componentValue === null) return + setFocusedOn(null) + updateInputs(fromComponentValueToInput(componentValue)) + }, [componentValue]) + + const validate = useCallback( + (input: InputType | null): input is InputType => input !== null && validateInput(input), + [input, ...deps] + ) + + // sync inputs -> engine + useEffect(() => { + if (skipSyncRef.current) return + if (validate(input)) { + const newComponentValue = { ...componentValue, ...fromInputToComponentValue(input) } + if (isEqual(newComponentValue)) return + + for (const [_, setComponentValue] of componentValues) { + setComponentValue(newComponentValue) + } + } + }, [input]) + + // sync engine -> inputs + useEffect(() => { + if (componentValue === null) return + + let newInputs = fromComponentValueToInput(componentValue) as any + if (focusedOn) { + // skip sync from state while editing, to avoid overriding the user input + const current = getValue(input, focusedOn) + newInputs = setValue(newInputs, focusedOn, current) + } + // set "skipSync" to avoid cyclic component value change + updateInputs(newInputs, true) + }, [componentValue, ...deps]) + + useEffect(() => { + setIsValid(validate(input)) + }, [input, ...deps]) + + const getProps = useCallback( + ( + path: NestedKey, + getter?: (event: React.ChangeEvent) => any + ): Pick, 'value' | 'onChange' | 'onFocus' | 'onBlur'> => { + const value = (getValue(asd, path) || '').toString() + + return { + value, + onChange: handleUpdate(path, getter), + onFocus: handleFocus(path), + onBlur: handleBlur + } + }, + [handleUpdate, handleFocus, handleBlur, input] + ) + + return { getInputProps: getProps, isValid } +} From 80f52a85ca72c218740d01cd887b8ecb5a6d23da Mon Sep 17 00:00:00 2001 From: Nicolas Echezarreta Date: Thu, 14 Nov 2024 16:03:51 -0300 Subject: [PATCH 4/6] magic stuff for multiple entity Transform #2 --- .../EntityInspector/EntityInspector.tsx | 6 +- .../TransformInspector/TransformInspector.tsx | 5 +- .../src/hooks/sdk/useComponentInput.ts | 216 ++++++++++-------- 3 files changed, 130 insertions(+), 97 deletions(-) diff --git a/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx b/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx index 3e5ecd6d9..528cfbfa8 100644 --- a/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx +++ b/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx @@ -63,7 +63,7 @@ const MultiEntityInspector = withSdk<{ entities: Entity[] }>(({ sdk, entities }) {inspectors.map( ({ name, component: Inspector }, index) => - !hiddenComponents[name] && + !hiddenComponents[name] && )} ) @@ -165,14 +165,14 @@ const SingleEntityInspector = withSdk<{ entity: Entity | null }>(({ sdk, entity {inspectors.map( ({ name, component: Inspector }, index) => - !hiddenComponents[name] && + !hiddenComponents[name] && )} {isBasicViewEnabled ? ( ) : ( advancedInspectorComponents.map( ({ name, component: Inspector }, index) => - !hiddenComponents[name] && + !hiddenComponents[name] && ) )} diff --git a/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx b/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx index 2ca684c10..ca02c2fe4 100644 --- a/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx +++ b/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx @@ -1,6 +1,7 @@ +import { Entity } from '@dcl/ecs' import { useEffect } from 'react' -import { isValidNumericInput, useComponentInput, useComponentInput2 } from '../../../hooks/sdk/useComponentInput' +import { isValidNumericInput, useComponentInput, useMultiComponentInput } from '../../../hooks/sdk/useComponentInput' import { useHasComponent } from '../../../hooks/sdk/useHasComponent' import { withSdk } from '../../../hoc/withSdk' @@ -20,7 +21,7 @@ export default withSdk(({ sdk, entities }) => { const hasTransform = useHasComponent(entity, Transform) const transform = Transform.getOrNull(entity) ?? undefined const config = TransformConfig.getOrNull(entity) ?? undefined - const { getInputProps } = useComponentInput2( + const { getInputProps } = useMultiComponentInput( entities, Transform, fromTransform, diff --git a/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts b/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts index e709d993b..52fc7918a 100644 --- a/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts +++ b/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts @@ -1,8 +1,11 @@ -import { InputHTMLAttributes, useCallback, useEffect, useRef, useState } from 'react' -import { Entity } from '@dcl/ecs' +import { InputHTMLAttributes, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { CrdtMessageType, Entity } from '@dcl/ecs' +import { recursiveCheck as hasDiff } from 'jest-matcher-deep-close-to/lib/recursiveCheck' import { getValue, NestedKey, setValue } from '../../lib/logic/get-set-value' import { Component } from '../../lib/sdk/components' -import { useComponentValue } from './useComponentValue' +import { getComponentValue, isLastWriteWinComponent, useComponentValue } from './useComponentValue' +import { useSdk } from './useSdk' +import { useChange } from './useChange' type Input = { [key: string]: boolean | string | string[] | any[] | Record @@ -15,6 +18,9 @@ export function isValidNumericInput(input: Input[keyof Input]): boolean { if (typeof input === 'boolean') { return !!input } + if (typeof input === 'number') { + return !isNaN(input) + } return input.length > 0 && !isNaN(Number(input)) } @@ -114,6 +120,28 @@ export const useComponentInput = { + // Base case - if any value is not an object, compare directly + if (!values.every(val => val && typeof val === 'object')) { + return values.every(val => val === values[0]) ? values[0] : '--' // symbol? + } + + // Get all keys from all objects + const allKeys = [...new Set(values.flatMap(Object.keys))] + + // Create result object + const result: any = {} + + // For each key, recursively merge values + for (const key of allKeys) { + const valuesForKey = values.map(obj => obj[key]) + result[key] = mergeValues(valuesForKey) + } + + return result +} + const mergeComponentValues = ( values: ComponentValueType[], fromComponentValueToInput: (componentValue: ComponentValueType) => InputType @@ -129,124 +157,128 @@ const mergeComponentValues = { - const value = input[key] - if (typeof value === 'object' && value !== null) { - // For objects, compare stringified versions - return JSON.stringify(value) === JSON.stringify(firstValue) - } - return value === firstValue - }) - - // Set result value based on whether all inputs match - result[key] = allSame ? firstValue : ('--' as any) + const valuesForKey = inputs.map(input => input[key]) + result[key] = mergeValues(valuesForKey) } return result } -export const useComponentInput2 = ( +const getEntityAndComponentValue = (entities: Entity[], component: Component): [Entity, ComponentValueType][] => { + return entities.map((entity) => [entity, getComponentValue(entity, component) as ComponentValueType]) +} + +export const useMultiComponentInput = ( entities: Entity[], component: Component, fromComponentValueToInput: (componentValue: ComponentValueType) => InputType, fromInputToComponentValue: (input: InputType) => ComponentValueType, validateInput: (input: InputType) => boolean = () => true, - deps: unknown[] = [] ) => { - const componentValues = entities.map((entity) => useComponentValue(entity, component)) - const [componentValue, _, isEqual] = componentValues[0] - const asd = mergeComponentValues( - componentValues.map(([value]) => value), - fromComponentValueToInput - ) - const [input, setInput] = useState( - componentValue === null ? null : fromComponentValueToInput(componentValue) + // If there's only one entity, use the single entity version just to be safe for now + if (entities.length === 1) { + return useComponentInput( + entities[0], + component, + fromComponentValueToInput, + fromInputToComponentValue, + validateInput + ) + } + const sdk = useSdk() + + // Get initial merged value from all entities + const initialEntityValues = getEntityAndComponentValue(entities, component) + const initialMergedValue = useMemo(() => + mergeComponentValues( + initialEntityValues.map(([_, component]) => component), + fromComponentValueToInput + ), + [] // only compute on mount ) - const [focusedOn, setFocusedOn] = useState(null) - const skipSyncRef = useRef(false) - const [isValid, setIsValid] = useState(true) - const updateInputs = useCallback((value: InputType | null, skipSync = false) => { - skipSyncRef.current = skipSync - setInput(value) - }, []) + const [value, setMergeValue] = useState(initialMergedValue) + const [isValid, setIsValid] = useState(true) + const [isFocused, setIsFocused] = useState(false) - const handleUpdate = + // Handle input updates + const handleUpdate = useCallback( (path: NestedKey, getter: (event: React.ChangeEvent) => any = (e) => e.target.value) => (event: React.ChangeEvent) => { - if (input === null) return - const newInputs = setValue(input, path, getter(event)) - updateInputs(newInputs) - } + if (!value) return - const handleFocus = useCallback( - (path: NestedKey) => () => { - setFocusedOn(path) - }, - [] - ) + const newValue = setValue(value, path, getter(event)) + if (!hasDiff(value, newValue, 2)) return - const handleBlur = useCallback(() => { - if (componentValue === null) return - setFocusedOn(null) - updateInputs(fromComponentValueToInput(componentValue)) - }, [componentValue]) + // Only update if component is last-write-win and SDK exists + if (!isLastWriteWinComponent(component) || !sdk) { + setMergeValue(newValue) + return + } - const validate = useCallback( - (input: InputType | null): input is InputType => input !== null && validateInput(input), - [input, ...deps] + // Validate and update all entities + const entityUpdates = getEntityAndComponentValue(entities, component).map(([entity, componentValue]) => { + const updatedInput = setValue(fromComponentValueToInput(componentValue as any), path, getter(event)) + const newComponentValue = fromInputToComponentValue(updatedInput) + return { + entity, + value: newComponentValue, + isValid: validateInput(updatedInput) + } + }) + + const allUpdatesValid = entityUpdates.every(({ isValid }) => isValid) + + if (allUpdatesValid) { + entityUpdates.forEach(({ entity, value }) => { + sdk.operations.updateValue(component, entity, value) + }) + void sdk.operations.dispatch() + } + + setMergeValue(newValue) + setIsValid(allUpdatesValid) + }, + [value, sdk, component, entities, fromInputToComponentValue, fromComponentValueToInput, validateInput] ) - // sync inputs -> engine - useEffect(() => { - if (skipSyncRef.current) return - if (validate(input)) { - const newComponentValue = { ...componentValue, ...fromInputToComponentValue(input) } - if (isEqual(newComponentValue)) return + // Sync with engine changes + useChange( + (event) => { + const isRelevantUpdate = + entities.includes(event.entity) && + component.componentId === event.component?.componentId && + event.value && + event.operation === CrdtMessageType.PUT_COMPONENT - for (const [_, setComponentValue] of componentValues) { - setComponentValue(newComponentValue) - } - } - }, [input]) + if (!isRelevantUpdate) return - // sync engine -> inputs - useEffect(() => { - if (componentValue === null) return + const updatedEntityValues = getEntityAndComponentValue(entities, component) + const newMergedValue = mergeComponentValues( + updatedEntityValues.map(([_, component]) => component), + fromComponentValueToInput + ) - let newInputs = fromComponentValueToInput(componentValue) as any - if (focusedOn) { - // skip sync from state while editing, to avoid overriding the user input - const current = getValue(input, focusedOn) - newInputs = setValue(newInputs, focusedOn, current) - } - // set "skipSync" to avoid cyclic component value change - updateInputs(newInputs, true) - }, [componentValue, ...deps]) + if (!hasDiff(value, newMergedValue, 2) || isFocused) return - useEffect(() => { - setIsValid(validate(input)) - }, [input, ...deps]) + setMergeValue(newMergedValue) + }, + [entities, component, fromComponentValueToInput, value, isFocused] + ) - const getProps = useCallback( + // Input props getter + const getInputProps = useCallback( ( path: NestedKey, getter?: (event: React.ChangeEvent) => any - ): Pick, 'value' | 'onChange' | 'onFocus' | 'onBlur'> => { - const value = (getValue(asd, path) || '').toString() - - return { - value, - onChange: handleUpdate(path, getter), - onFocus: handleFocus(path), - onBlur: handleBlur - } - }, - [handleUpdate, handleFocus, handleBlur, input] + ): Pick, 'value' | 'onChange' | 'onFocus' | 'onBlur'> => ({ + value: (getValue(value, path) || '').toString(), + onChange: handleUpdate(path, getter), + onFocus: () => setIsFocused(true), + onBlur: () => setIsFocused(false) + }), + [value, handleUpdate] ) - return { getInputProps: getProps, isValid } + return { getInputProps, isValid } } From da65ecc5d25ceeff8702d31535a5356de396f885 Mon Sep 17 00:00:00 2001 From: Nicolas Echezarreta Date: Thu, 14 Nov 2024 20:06:35 -0300 Subject: [PATCH 5/6] add '--' for inputs --- .../TransformInspector/TransformInspector.tsx | 1 - .../src/components/ui/TextField/TextField.tsx | 2 +- .../src/hooks/sdk/useComponentInput.ts | 84 ++++++++++--------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx b/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx index ca02c2fe4..3217ca8bb 100644 --- a/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx +++ b/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx @@ -1,4 +1,3 @@ -import { Entity } from '@dcl/ecs' import { useEffect } from 'react' import { isValidNumericInput, useComponentInput, useMultiComponentInput } from '../../../hooks/sdk/useComponentInput' diff --git a/packages/@dcl/inspector/src/components/ui/TextField/TextField.tsx b/packages/@dcl/inspector/src/components/ui/TextField/TextField.tsx index 27df8483d..09c98dfff 100644 --- a/packages/@dcl/inspector/src/components/ui/TextField/TextField.tsx +++ b/packages/@dcl/inspector/src/components/ui/TextField/TextField.tsx @@ -123,7 +123,7 @@ const TextField = React.forwardRef((props, ref) => { { // Base case - if any value is not an object, compare directly - if (!values.every(val => val && typeof val === 'object')) { - return values.every(val => val === values[0]) ? values[0] : '--' // symbol? + if (!values.every((val) => val && typeof val === 'object')) { + return values.every((val) => val === values[0]) ? values[0] : '--' } // Get all keys from all objects @@ -135,7 +135,7 @@ const mergeValues = (values: any[]): any => { // For each key, recursively merge values for (const key of allKeys) { - const valuesForKey = values.map(obj => obj[key]) + const valuesForKey = values.map((obj) => obj[key]) result[key] = mergeValues(valuesForKey) } @@ -157,14 +157,17 @@ const mergeComponentValues = input[key]) + const valuesForKey = inputs.map((input) => input[key]) result[key] = mergeValues(valuesForKey) } return result } -const getEntityAndComponentValue = (entities: Entity[], component: Component): [Entity, ComponentValueType][] => { +const getEntityAndComponentValue = ( + entities: Entity[], + component: Component +): [Entity, ComponentValueType][] => { return entities.map((entity) => [entity, getComponentValue(entity, component) as ComponentValueType]) } @@ -173,7 +176,7 @@ export const useMultiComponentInput = , fromComponentValueToInput: (componentValue: ComponentValueType) => InputType, fromInputToComponentValue: (input: InputType) => ComponentValueType, - validateInput: (input: InputType) => boolean = () => true, + validateInput: (input: InputType) => boolean = () => true ) => { // If there's only one entity, use the single entity version just to be safe for now if (entities.length === 1) { @@ -189,11 +192,12 @@ export const useMultiComponentInput = - mergeComponentValues( - initialEntityValues.map(([_, component]) => component), - fromComponentValueToInput - ), + const initialMergedValue = useMemo( + () => + mergeComponentValues( + initialEntityValues.map(([_, component]) => component), + fromComponentValueToInput + ), [] // only compute on mount ) @@ -204,41 +208,41 @@ export const useMultiComponentInput = , getter: (event: React.ChangeEvent) => any = (e) => e.target.value) => - (event: React.ChangeEvent) => { - if (!value) return + (event: React.ChangeEvent) => { + if (!value) return - const newValue = setValue(value, path, getter(event)) - if (!hasDiff(value, newValue, 2)) return - - // Only update if component is last-write-win and SDK exists - if (!isLastWriteWinComponent(component) || !sdk) { - setMergeValue(newValue) - return - } + const newValue = setValue(value, path, getter(event)) + if (!hasDiff(value, newValue, 2)) return - // Validate and update all entities - const entityUpdates = getEntityAndComponentValue(entities, component).map(([entity, componentValue]) => { - const updatedInput = setValue(fromComponentValueToInput(componentValue as any), path, getter(event)) - const newComponentValue = fromInputToComponentValue(updatedInput) - return { - entity, - value: newComponentValue, - isValid: validateInput(updatedInput) + // Only update if component is last-write-win and SDK exists + if (!isLastWriteWinComponent(component) || !sdk) { + setMergeValue(newValue) + return } - }) - const allUpdatesValid = entityUpdates.every(({ isValid }) => isValid) - - if (allUpdatesValid) { - entityUpdates.forEach(({ entity, value }) => { - sdk.operations.updateValue(component, entity, value) + // Validate and update all entities + const entityUpdates = getEntityAndComponentValue(entities, component).map(([entity, componentValue]) => { + const updatedInput = setValue(fromComponentValueToInput(componentValue as any), path, getter(event)) + const newComponentValue = fromInputToComponentValue(updatedInput) + return { + entity, + value: newComponentValue, + isValid: validateInput(updatedInput) + } }) - void sdk.operations.dispatch() - } - setMergeValue(newValue) - setIsValid(allUpdatesValid) - }, + const allUpdatesValid = entityUpdates.every(({ isValid }) => isValid) + + if (allUpdatesValid) { + entityUpdates.forEach(({ entity, value }) => { + sdk.operations.updateValue(component, entity, value) + }) + void sdk.operations.dispatch() + } + + setMergeValue(newValue) + setIsValid(allUpdatesValid) + }, [value, sdk, component, entities, fromInputToComponentValue, fromComponentValueToInput, validateInput] ) From ed02515432846c6005b665a87d4dfbaad79db68a Mon Sep 17 00:00:00 2001 From: Nicolas Echezarreta Date: Tue, 19 Nov 2024 16:48:18 -0300 Subject: [PATCH 6/6] refactor gizmo manager --- .../editorComponents/selection.ts | 18 +- .../decentraland/gizmo-manager.spec.ts | 11 +- .../lib/babylon/decentraland/gizmo-manager.ts | 236 +++++++----------- .../decentraland/sdkComponents/transform.ts | 8 +- 4 files changed, 108 insertions(+), 165 deletions(-) diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/selection.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/selection.ts index 5ef0293db..3e98be78d 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/selection.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/selection.ts @@ -53,28 +53,14 @@ export const setGizmoManager = (entity: EcsEntity, value: { gizmo: number }) => toggleSelection(entity, true) - const selectedEntities = Array.from(context.engine.getEntitiesWith(context.editorComponents.Selection)) const types = context.gizmos.getGizmoTypes() const type = types[value?.gizmo || 0] context.gizmos.setGizmoType(type) - - if (selectedEntities.length === 1) { - context.gizmos.setEntity(entity) - } else if (selectedEntities.length > 1) { - context.gizmos.repositionGizmoOnCentroid() - } + context.gizmos.addEntity(entity) } export const unsetGizmoManager = (entity: EcsEntity) => { const context = entity.context.deref()! - const selectedEntities = Array.from(context.engine.getEntitiesWith(context.editorComponents.Selection)) - const currentEntity = context.gizmos.getEntity() - toggleSelection(entity, false) - - if (currentEntity?.entityId === entity.entityId || selectedEntities.length === 0) { - context.gizmos.unsetEntity() - } else { - context.gizmos.repositionGizmoOnCentroid() - } + context.gizmos.removeEntity(entity) } diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.spec.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.spec.ts index 3fe518c54..ffa0e56bf 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.spec.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.spec.ts @@ -65,14 +65,14 @@ describe('GizmoManager', () => { babylonEntity.rotationQuaternion = new Quaternion(0, 0, 0, 1) handler = jest.fn() gizmos.onChange(handler) - gizmos.setEntity(babylonEntity) + gizmos.addEntity(babylonEntity) entities = [dclEntity] nodes.push({ entity: dclEntity, children: [] }) }) afterEach(() => { babylonEntity.dispose() context.engine.removeEntity(dclEntity) - gizmos.unsetEntity() + gizmos.removeEntity(context.getOrCreateEntity(dclEntity)) entities = [] nodes = nodes.filter(($) => $.entity !== dclEntity) }) @@ -86,16 +86,11 @@ describe('GizmoManager', () => { it('should skip setting the entity', () => { const handler = jest.fn() gizmos.onChange(handler) - gizmos.setEntity(babylonEntity) + gizmos.addEntity(babylonEntity) expect(handler).not.toHaveBeenCalled() }) }) describe('and dragging a gizmo', () => { - it('should not execute SDK operations if transform was not changed', () => { - gizmos.gizmoManager.gizmos.positionGizmo?.onDragEndObservable.notifyObservers({} as any) - expect(context.operations.updateValue).toBeCalledTimes(0) - expect(context.operations.dispatch).toBeCalledTimes(0) - }) it('should execute SDK operations if transform was changed', () => { babylonEntity.position = new Vector3(10, 10, 10) gizmos.gizmoManager.gizmos.positionGizmo?.onDragEndObservable.notifyObservers({} as any) diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.ts index 8da5fd707..5501a8225 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/gizmo-manager.ts @@ -3,7 +3,6 @@ import { IAxisDragGizmo, PickingInfo, Quaternion, - Node, Vector3, PointerDragBehavior, AbstractMesh, @@ -37,13 +36,8 @@ function areProportional(a: number, b: number) { return Math.abs(a - b) < 1e-5 } -// should be moved to ecs-math -function areQuaternionsEqual(a: DclQuaternion, b: DclQuaternion) { - return a.x === b.x && a.y === b.y && a.z === b.z && a.w === b.w -} - function calculateCenter(positions: Vector3[]): Vector3 { - if (positions.length === 0) throw new Error('No positions provided to calculate center') + if (positions.length === 0) new Vector3(0, 0, 0) const sum = positions.reduce((acc, pos) => { acc.x += pos.x @@ -71,21 +65,12 @@ export function createGizmoManager(context: SceneContext) { gizmoManager.gizmos.positionGizmo!.updateGizmoRotationToMatchAttachedMesh = false gizmoManager.gizmos.rotationGizmo!.updateGizmoRotationToMatchAttachedMesh = true - let lastEntity: EcsEntity | null = null + let selectedEntities: EcsEntity[] = [] let rotationGizmoAlignmentDisabled = false let positionGizmoAlignmentDisabled = false let shouldRestorRotationGizmoAlignment = false let shouldRestorPositionGizmoAlignment = false let isEnabled = true - const parentMapper: Map = new Map() - - function getSelectedEntities() { - return context.operations.getSelectedEntities() - } - - function areMultipleEntitiesSelected() { - return getSelectedEntities().length > 1 - } function fixRotationGizmoAlignment(value: TransformType) { const isProportional = @@ -117,23 +102,36 @@ export function createGizmoManager(context: SceneContext) { } } - function getTransform(entity?: EcsEntity): TransformType { - const _entity = entity ?? lastEntity - if (_entity) { - const parent = context.Transform.getOrNull(_entity.entityId)?.parent || (0 as Entity) - const value = { - position: gizmoManager.positionGizmoEnabled ? snapPosition(_entity.position) : _entity.position, - scale: gizmoManager.scaleGizmoEnabled ? snapScale(_entity.scaling) : _entity.scaling, - rotation: gizmoManager.rotationGizmoEnabled - ? _entity.rotationQuaternion - ? snapRotation(_entity.rotationQuaternion) - : Quaternion.Zero() - : _entity.rotationQuaternion ?? Quaternion.Zero(), - parent - } - return value - } else { - throw new Error('No entity selected') + function getFirstEntity() { + return selectedEntities[0] + } + + function getParent(entity: EcsEntity) { + return context.Transform.getOrNull(entity.entityId)?.parent || (0 as Entity) + } + + function computeWorldTransform(entity: EcsEntity): TransformType { + const { positionGizmoEnabled, scaleGizmoEnabled, rotationGizmoEnabled } = gizmoManager + // Compute the updated transform based on the current node position + const worldMatrix = entity.computeWorldMatrix(true) + const position = new Vector3() + const scale = new Vector3() + const rotation = new Quaternion() + worldMatrix.decompose(scale, rotation, position) + + return { + position: positionGizmoEnabled ? snapPosition(position) : position, + scale: scaleGizmoEnabled ? snapScale(scale) : scale, + rotation: rotationGizmoEnabled ? snapRotation(rotation) : rotation + } + } + + function getTransform(entity: EcsEntity): TransformType { + return { + position: entity.position, + scale: entity.scaling, + rotation: entity.rotationQuaternion ?? Quaternion.Zero(), + parent: getParent(entity) } } @@ -143,61 +141,39 @@ export function createGizmoManager(context: SceneContext) { position: DclVector3.create(position.x, position.y, position.z), rotation: DclQuaternion.create(rotation.x, rotation.y, rotation.z, rotation.w), scale: DclVector3.create(scale.x, scale.y, scale.z), - parent: parent + parent }) - void context.operations.dispatch() } + /** + * Updates the transform of all selected entities after a gizmo operation + * + * 1. Fixes rotation gizmo alignment based on the first selected entity's transform + * 2. For each selected entity: + * - Gets the original parent and resolves it to a valid entity or root node + * - Temporarily sets the entity's parent to handle transform calculations + * - Updates the entity's transform: + * - If parent is root node: Uses world space transform with snapping + * - Otherwise: Uses local space transform + * - Preserves the original parent relationship + * 3. Dispatches the transform updates to persist changes + */ function updateTransform() { - if (lastEntity === null) return - const oldTransform = context.Transform.get(lastEntity.entityId) - const newTransform = getTransform() - fixRotationGizmoAlignment(newTransform) - - // Remap all selected entities to the original parent - parentMapper.forEach((value, key, map) => { - if (key === lastEntity!.entityId) return - const entity = context.getEntityOrNull(key) - if (entity) { - entity.setParent(value) - map.delete(key) - } - }) + fixRotationGizmoAlignment(getTransform(getFirstEntity())) + for (const entity of selectedEntities) { + const originalParent = getParent(entity) + const parent = context.getEntityOrNull(originalParent ?? context.rootNode.entityId) - if ( - DclVector3.equals(newTransform.position, oldTransform.position) && - DclVector3.equals(newTransform.scale, oldTransform.scale) && - areQuaternionsEqual(newTransform.rotation, oldTransform.rotation) - ) - return - // Update last selected entity transform - updateEntityTransform(lastEntity.entityId, newTransform) - - // Update entity transform for all the selected entities - if (areMultipleEntitiesSelected()) { - for (const entityId of getSelectedEntities()) { - if (entityId === lastEntity.entityId) continue - const entity = context.getEntityOrNull(entityId)! - const transform = getTransform(entity) - updateEntityTransform(entityId, transform) - } - } - } + entity.setParent(parent) - function initTransform() { - if (lastEntity === null) return - if (areMultipleEntitiesSelected()) { - for (const entityId of getSelectedEntities()) { - if (entityId === lastEntity.entityId) continue - const entity = context.getEntityOrNull(entityId)! - parentMapper.set(entityId, entity.parent!) - entity.setParent(lastEntity) - } + updateEntityTransform(entity.entityId, { + ...(parent === context.rootNode ? computeWorldTransform(entity) : getTransform(entity)), + parent: originalParent + }) } - } - // Map to store the original parent of each entity - const originalParents = new Map() + void context.operations.dispatch() + } // Check if a transform node for the gizmo already exists, or create one function getDummyNode(): TransformNode { @@ -206,10 +182,17 @@ export function createGizmoManager(context: SceneContext) { return dummyNode } + function restoreParents() { + for (const entity of selectedEntities) { + const originalParent = getParent(entity) + const parent = context.getEntityOrNull(originalParent ?? context.rootNode.entityId) + entity.setParent(parent) + } + } + function repositionGizmoOnCentroid() { - const selectedEntities = getSelectedEntities().map((entityId) => context.getEntityOrNull(entityId)!) const positions = selectedEntities.map((entity) => { - const { x, y, z } = getTransform(entity).position + const { x, y, z } = computeWorldTransform(entity).position return new Vector3(x, y, z) }) const centroidPosition = calculateCenter(positions) @@ -219,34 +202,13 @@ export function createGizmoManager(context: SceneContext) { // so everything aligns to the right position afterwards. dummyNode.position = centroidPosition - // Store the original parents and set the dummy node as parent for each selected entity - selectedEntities.forEach((entity) => { - const parent = entity.parent as TransformNode | null - originalParents.set(entity.entityId, parent) + for (const entity of selectedEntities) { entity.setParent(dummyNode) - }) + } - // Attach the gizmo to the dummy node gizmoManager.attachToNode(dummyNode) } - function restoreOriginalParents() { - originalParents.forEach((parent, entity) => { - const ecsEntity = context.getEntityOrNull(entity)! - ecsEntity.setParent(parent) - }) - - // Clear the stored parents as they're now restored - originalParents.clear() - - // Detach the gizmo from the dummy node if needed - gizmoManager.attachToNode(null) - } - - gizmoManager.gizmos.scaleGizmo?.onDragStartObservable.add(initTransform) - gizmoManager.gizmos.positionGizmo?.onDragStartObservable.add(initTransform) - gizmoManager.gizmos.rotationGizmo?.onDragStartObservable.add(initTransform) - gizmoManager.gizmos.scaleGizmo?.onDragEndObservable.add(updateTransform) gizmoManager.gizmos.positionGizmo?.onDragEndObservable.add(updateTransform) gizmoManager.gizmos.rotationGizmo?.onDragEndObservable.add(updateTransform) @@ -300,9 +262,8 @@ export function createGizmoManager(context: SceneContext) { return () => events.off('change', cb) } - function unsetEntity() { - lastEntity = null - gizmoManager.attachToNode(lastEntity) + function removeGizmos() { + gizmoManager.attachToNode(null) gizmoManager.positionGizmoEnabled = false gizmoManager.rotationGizmoEnabled = false gizmoManager.scaleGizmoEnabled = false @@ -312,8 +273,9 @@ export function createGizmoManager(context: SceneContext) { function setEnabled(value: boolean) { if (value !== isEnabled) { isEnabled = value - if (!isEnabled && lastEntity) { - unsetEntity() + if (!isEnabled && selectedEntities.length > 0) { + restoreParents() + removeGizmos() } } } @@ -335,24 +297,23 @@ export function createGizmoManager(context: SceneContext) { }) context.scene.onPointerDown = function (_e, pickResult) { + const firstEntity = getFirstEntity() if ( - lastEntity === null || + !firstEntity || pickResult.pickedMesh === null || !gizmoManager.freeGizmoEnabled || - !context.Transform.getOrNull(lastEntity.entityId) + !context.Transform.getOrNull(firstEntity.entityId) ) return - const selectedEntities = getSelectedEntities().map((entityId) => context.getEntityOrNull(entityId)!) if (selectedEntities.some((entity) => pickResult.pickedMesh!.isDescendantOf(entity))) { - initTransform() - meshPointerDragBehavior.attach(lastEntity as unknown as AbstractMesh) + meshPointerDragBehavior.attach(firstEntity as unknown as AbstractMesh) } } context.scene.onPointerUp = function () { - if (lastEntity === null || !gizmoManager.freeGizmoEnabled || !context.Transform.getOrNull(lastEntity.entityId)) - return - updateTransform() + const firstEntity = getFirstEntity() + if (!firstEntity || !gizmoManager.freeGizmoEnabled || !context.Transform.getOrNull(firstEntity.entityId)) return + void updateTransform() meshPointerDragBehavior.detach() } @@ -372,9 +333,11 @@ export function createGizmoManager(context: SceneContext) { return isEnabled }, setEnabled, - setEntity(entity: EcsEntity | null): void { + restoreParents, + repositionGizmoOnCentroid, + addEntity(entity: EcsEntity): void { if ( - entity === lastEntity || + selectedEntities.includes(entity) || !isEnabled || entity?.isHidden() || entity?.isLocked() || @@ -382,28 +345,23 @@ export function createGizmoManager(context: SceneContext) { ) { return } - restoreOriginalParents() - if (areMultipleEntitiesSelected()) { - return repositionGizmoOnCentroid() - } else { - gizmoManager.attachToNode(entity) - lastEntity = entity - // fix gizmo rotation/position if necessary - const transform = getTransform() - fixRotationGizmoAlignment(transform) - fixPositionGizmoAlignment(transform) - } + restoreParents() + selectedEntities.push(entity) + repositionGizmoOnCentroid() + const transform = getTransform(entity) + // fix gizmo rotation/position if necessary + fixRotationGizmoAlignment(transform) + fixPositionGizmoAlignment(transform) events.emit('change') }, - repositionGizmoOnCentroid() { - restoreOriginalParents() - return repositionGizmoOnCentroid() - }, getEntity() { - return lastEntity + return getFirstEntity() }, - unsetEntity() { - unsetEntity() + removeEntity(entity: EcsEntity) { + restoreParents() + selectedEntities = selectedEntities.filter((e) => e.entityId !== entity.entityId) + if (selectedEntities.length === 0) removeGizmos() + else repositionGizmoOnCentroid() }, getGizmoTypes() { return [GizmoType.POSITION, GizmoType.ROTATION, GizmoType.SCALE, GizmoType.FREE] as const diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/sdkComponents/transform.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/sdkComponents/transform.ts index 672f54c8c..26c3c0149 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/sdkComponents/transform.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/sdkComponents/transform.ts @@ -7,6 +7,8 @@ import { getRoot } from '../../../sdk/nodes' export const putTransformComponent: ComponentOperation = (entity, component) => { if (component.componentType === ComponentType.LastWriteWinElementSet) { + const gizmos = entity.context.deref()!.gizmos + gizmos.restoreParents() const newValue = component.getOrNull(entity.entityId) as TransformType | null const currentValue = entity.ecsComponentValues.transform entity.ecsComponentValues.transform = newValue || undefined @@ -46,6 +48,8 @@ export const putTransformComponent: ComponentOperation = (entity, component) => } if (needsReparenting) reparentEntity(entity) + + gizmos.repositionGizmoOnCentroid() } } @@ -87,10 +91,10 @@ function reparentEntity(entity: EcsEntity) { const isSceneRoot = newRoot === ROOT if (!isSceneRoot) { entity.setVisibility(false) - entity.context.deref()?.gizmos.unsetEntity() + entity.context.deref()?.gizmos.removeEntity(entity) } else { entity.setVisibility(true) - entity.context.deref()?.gizmos.setEntity(entity) + entity.context.deref()?.gizmos.addEntity(entity) } } }