Skip to content

Commit

Permalink
Merge branch 'main' into fix/multiplayer-clone
Browse files Browse the repository at this point in the history
  • Loading branch information
gonpombo8 authored Nov 25, 2024
2 parents 9caa3b9 + cf41aed commit d50fc5f
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 192 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 <MultiEntityInspector entities={ownedEntities} />
}

return <SingleEntityInspector entity={entity} />
}

const MultiEntityInspector = withSdk<{ entities: Entity[] }>(({ sdk, entities }) => {
const hiddenComponents = useAppSelector(getHiddenComponents)
const inspectors = useMemo(
() => [{ name: sdk.components.Transform.componentName, component: TransformInspector }],
[sdk]
)

return (
<div className="EntityInspector">
<div className="EntityHeader">
<div className="title">{entities.length} entities selected</div>
</div>
{inspectors.map(
({ name, component: Inspector }, index) =>
!hiddenComponents[name] && <Inspector key={`${index}-${entities.join(',')}`} entities={entities} />
)}
</div>
)
})

const SingleEntityInspector = withSdk<{ entity: Entity | null }>(({ sdk, entity }) => {
const hiddenComponents = useAppSelector(getHiddenComponents)
const [isBasicViewEnabled, setIsBasicViewEnabled] = useState(false)

Expand Down Expand Up @@ -123,20 +159,20 @@ export const EntityInspector = withSdk(({ sdk }) => {
)

return (
<div className="EntityInspector" key={entity}>
<div className="EntityInspector">
{entity !== null ? (
<>
<EntityHeader entity={entity} />
{inspectors.map(
({ name, component: Inspector }, index) =>
!hiddenComponents[name] && <Inspector key={index} entity={entity} />
!hiddenComponents[name] && <Inspector key={`${index}-${entity}`} entities={[entity]} />
)}
{isBasicViewEnabled ? (
<SmartItemBasicView entity={entity} />
) : (
advancedInspectorComponents.map(
({ name, component: Inspector }, index) =>
!hiddenComponents[name] && <Inspector key={index} entity={entity} />
!hiddenComponents[name] && <Inspector key={`${index}-${entity}`} entity={entity} />
)
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect } from 'react'

import { isValidNumericInput, useComponentInput } from '../../../hooks/sdk/useComponentInput'
import { isValidNumericInput, useComponentInput, useMultiComponentInput } from '../../../hooks/sdk/useComponentInput'
import { useHasComponent } from '../../../hooks/sdk/useHasComponent'
import { withSdk } from '../../../hoc/withSdk'

Expand All @@ -13,14 +13,15 @@ import { Link, Props as LinkProps } from './Link'

import './TransformInspector.css'

export default withSdk<Props>(({ sdk, entity }) => {
export default withSdk<Props>(({ 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
const config = TransformConfig.getOrNull(entity) ?? undefined
const { getInputProps } = useComponentInput(
entity,
const { getInputProps } = useMultiComponentInput(
entities,
Transform,
fromTransform,
toTransform(transform, config),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Entity } from '@dcl/ecs'

export interface Props {
entity: Entity
entities: Entity[]
}

export type TransformInput = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const TextField = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
<input
className="input"
ref={ref}
type={type}
type={inputValue === '--' ? 'text' : type}
value={inputValue}
onChange={handleInputChange}
onFocus={handleInputFocus}
Expand Down
183 changes: 177 additions & 6 deletions packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts
Original file line number Diff line number Diff line change
@@ -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<string, boolean | string | string[] | any[] | Input>
Expand All @@ -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))
}

Expand Down Expand Up @@ -70,10 +76,8 @@ export const useComponentInput = <ComponentValueType extends object, InputType e
if (skipSyncRef.current) return
if (validate(input)) {
const newComponentValue = { ...componentValue, ...fromInputToComponentValue(input) }
if (isEqual(newComponentValue)) return

if (isEqual(newComponentValue)) {
return
}
setComponentValue(newComponentValue)
}
}, [input])
Expand Down Expand Up @@ -115,3 +119,170 @@ export const useComponentInput = <ComponentValueType extends object, InputType e

return { getInputProps: getProps, isValid }
}

// Helper function to recursively merge values
const mergeValues = (values: any[]): any => {
// 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] : '--'
}

// 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 = <ComponentValueType extends object, InputType extends Input>(
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 valuesForKey = inputs.map((input) => input[key])
result[key] = mergeValues(valuesForKey)
}

return result
}

const getEntityAndComponentValue = <ComponentValueType extends object>(
entities: Entity[],
component: Component<ComponentValueType>
): [Entity, ComponentValueType][] => {
return entities.map((entity) => [entity, getComponentValue(entity, component) as ComponentValueType])
}

export const useMultiComponentInput = <ComponentValueType extends object, InputType extends Input>(
entities: Entity[],
component: Component<ComponentValueType>,
fromComponentValueToInput: (componentValue: ComponentValueType) => InputType,
fromInputToComponentValue: (input: InputType) => ComponentValueType,
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) {
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 [value, setMergeValue] = useState(initialMergedValue)
const [isValid, setIsValid] = useState(true)
const [isFocused, setIsFocused] = useState(false)

// Handle input updates
const handleUpdate = useCallback(
(path: NestedKey<InputType>, getter: (event: React.ChangeEvent<HTMLInputElement>) => any = (e) => e.target.value) =>
(event: React.ChangeEvent<HTMLInputElement>) => {
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
}

// 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 with engine changes
useChange(
(event) => {
const isRelevantUpdate =
entities.includes(event.entity) &&
component.componentId === event.component?.componentId &&
event.value &&
event.operation === CrdtMessageType.PUT_COMPONENT

if (!isRelevantUpdate) return

const updatedEntityValues = getEntityAndComponentValue(entities, component)
const newMergedValue = mergeComponentValues(
updatedEntityValues.map(([_, component]) => component),
fromComponentValueToInput
)

if (!hasDiff(value, newMergedValue, 2) || isFocused) return

setMergeValue(newMergedValue)
},
[entities, component, fromComponentValueToInput, value, isFocused]
)

// Input props getter
const getInputProps = useCallback(
(
path: NestedKey<InputType>,
getter?: (event: React.ChangeEvent<HTMLInputElement>) => any
): Pick<InputHTMLAttributes<HTMLElement>, 'value' | 'onChange' | 'onFocus' | 'onBlur'> => ({
value: (getValue(value, path) || '').toString(),
onChange: handleUpdate(path, getter),
onFocus: () => setIsFocused(true),
onBlur: () => setIsFocused(false)
}),
[value, handleUpdate]
)

return { getInputProps, isValid }
}
9 changes: 2 additions & 7 deletions packages/@dcl/inspector/src/hooks/sdk/useComponentValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,7 @@ export const useComponentValue = <ComponentValueType>(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()
Expand All @@ -48,7 +43,7 @@ export const useComponentValue = <ComponentValueType>(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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading

0 comments on commit d50fc5f

Please sign in to comment.