Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5d5688f
feat: add components to change skybox configs
alejandralevy Jul 24, 2025
8e08ee1
tmp
nicoecheza Jul 29, 2025
c634e19
feat: add properties to v1
alejandralevy Jul 30, 2025
93be272
feat: merge migration
alejandralevy Jul 30, 2025
ecab779
feat: Fix prop name and initial value
alejandralevy Jul 30, 2025
949a05d
feat: Fix issue with range selector
alejandralevy Jul 30, 2025
4e70308
feat: Add hour selector
alejandralevy Jul 30, 2025
520ba10
feat: Add gradient to hour range field
alejandralevy Jul 30, 2025
00508d4
feat: Remove import and comment
alejandralevy Jul 31, 2025
0f9284b
feat: fix test and remove seconds value
alejandralevy Jul 31, 2025
1d027eb
feat: fix test value
alejandralevy Jul 31, 2025
0f6c8c3
feat: add skybox config on root level
alejandralevy Jul 31, 2025
5ee129f
feat: add direction to skybox config
alejandralevy Aug 1, 2025
2df97c7
feat: fix type errror
alejandralevy Aug 1, 2025
85507f9
feat: remove old components definitions
alejandralevy Aug 1, 2025
bae6765
fix: wrong parameter on function and enum values
alejandralevy Aug 1, 2025
e4acf71
feat: handle transition mode change with getInputProps hoook
alejandralevy Aug 1, 2025
4b21fd0
feat: fix transition mode not being sent
alejandralevy Aug 4, 2025
ae9919d
feat: refactor range hour component
alejandralevy Aug 4, 2025
06af810
feat: remove unnecessary useEffect
alejandralevy Aug 4, 2025
4287992
feat: add useEffect to update state
alejandralevy Aug 4, 2025
237cd90
feat: fix breaking test
alejandralevy Aug 4, 2025
1474bb5
Merge branch 'main' into feat/skybox-time-settings
alejandralevy Aug 4, 2025
5f40711
feat: fix css issue
alejandralevy Aug 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,23 @@ import { TextField } from '../../ui/TextField'
import { FileUploadField } from '../../ui/FileUploadField'
import { ACCEPTED_FILE_TYPES } from '../../ui/FileUploadField/types'
import { Props } from './types'
import { fromScene, toScene, isValidInput, isImage, fromSceneSpawnPoint, toSceneSpawnPoint } from './utils'
import {
fromScene,
toScene,
isValidInput,
isImage,
fromSceneSpawnPoint,
toSceneSpawnPoint,
MIDDAY_SECONDS
} from './utils'

import './SceneInspector.css'
import { EditorComponentsTypes, SceneAgeRating, SceneCategory, SceneSpawnPoint } from '../../../lib/sdk/components'
import { Dropdown } from '../../ui/Dropdown'
import { TextArea } from '../../ui'
import { Tabs } from '../Tabs'
import { CheckboxField } from '../../ui/CheckboxField'
import RangeHourField from '../../ui/RangeHourField/RangeHourField'
import { useComponentValue } from '../../../hooks/sdk/useComponentValue'
import { useArrayState } from '../../../hooks/useArrayState'
import { AddButton } from '../AddButton'
Expand All @@ -30,6 +39,7 @@ import { Tab } from '../Tab'
import { transformBinaryToBase64Resource } from '../../../lib/data-layer/host/fs-utils'
import { selectThumbnails } from '../../../redux/app'
import { Layout } from './Layout'
import { TransitionMode } from '../../../lib/sdk/components/SceneMetadata'

const AGE_RATING_OPTIONS = [
{
Expand Down Expand Up @@ -101,6 +111,7 @@ export default withSdk<Props>(({ sdk, entity }) => {
const categoriesProps = getInputProps('categories')
const authorProps = getInputProps('author')
const emailProps = getInputProps('email')
const transitionModeProps = getInputProps('skyboxConfig.transitionMode')
const silenceVoiceChatProps = getInputProps('silenceVoiceChat', (e) => e.target.checked)
const disablePortableExperiencesProps = getInputProps('disablePortableExperiences', (e) => e.target.checked)

Expand All @@ -109,6 +120,38 @@ export default withSdk<Props>(({ sdk, entity }) => {
Scene
)

const handleSkyboxAutoChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const isAuto = e.target.checked
const newValue = {
...componentValue,
skyboxConfig: {
...componentValue.skyboxConfig,
fixedTime: isAuto ? undefined : MIDDAY_SECONDS
}
}

setComponentValue(newValue)
},
[sdk, Scene, entity, componentValue, setComponentValue]
)

const handleSkyboxTimeChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target as HTMLInputElement
const newValue = {
...componentValue,
skyboxConfig: {
...componentValue.skyboxConfig,
fixedTime: parseInt(value)
}
}

setComponentValue(newValue)
},
[sdk, Scene, entity, componentValue, setComponentValue]
)

const [spawnPoints, addSpawnPoint, modifySpawnPoint, removeSpawnPoint] = useArrayState<SceneSpawnPoint>(
componentValue === null ? [] : componentValue.spawnPoints
)
Expand Down Expand Up @@ -424,6 +467,26 @@ export default withSdk<Props>(({ sdk, entity }) => {
<Block label="Spawn Settings" className="underlined"></Block>
{spawnPoints.map((spawnPoint, index) => renderSpawnPoint(spawnPoint, index))}
<AddButton onClick={handleAddSpawnPoint}>Add Spawn Point</AddButton>
<Block label="Skybox" className="underlined"></Block>
<CheckboxField
label="Auto (decentraland time)"
checked={componentValue.skyboxConfig?.fixedTime === undefined}
onChange={handleSkyboxAutoChange}
/>
<RangeHourField
value={componentValue.skyboxConfig?.fixedTime ?? MIDDAY_SECONDS}
onChange={handleSkyboxTimeChange}
disabled={componentValue.skyboxConfig?.fixedTime === undefined}
/>
<Dropdown
label="Transition Mode"
options={[
{ label: 'Forward', value: TransitionMode.TM_FORWARD },
{ label: 'Backward', value: TransitionMode.TM_BACKWARD }
]}
{...transitionModeProps}
disabled={componentValue.skyboxConfig?.fixedTime === undefined}
/>
</>
) : null}
</Container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export type SpawnPointInput = {
export type SceneInput = {
name: string
description: string
skyboxConfig: {
fixedTime: string
transitionMode: string
}
thumbnail: string
ageRating: string
categories: string[]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { EditorComponentsTypes, SceneAgeRating, SceneCategory } from '../../../lib/sdk/components'
import { TransitionMode } from '../../../lib/sdk/components/SceneMetadata'
import { Layout } from '../../../lib/utils/layout'
import { SceneInput } from './types'
import { fromScene, isValidInput, parseParcels, toScene } from './utils'

//TODO fix tests
function getInput(base: string, parcels: string): SceneInput {
const input: SceneInput = {
name: 'name',
Expand All @@ -16,6 +18,10 @@ function getInput(base: string, parcels: string): SceneInput {
spawnPoints: [],
author: 'John Doe',
email: 'johndoe@gmail.com',
skyboxConfig: {
fixedTime: '36000',
transitionMode: TransitionMode.TM_FORWARD.toString()
},
layout: {
base,
parcels
Expand All @@ -37,7 +43,11 @@ function getScene(layout: Layout): EditorComponentsTypes['Scene'] {
spawnPoints: [],
author: 'John Doe',
email: 'johndoe@gmail.com',
layout
layout,
skyboxConfig: {
fixedTime: 36000,
transitionMode: TransitionMode.TM_FORWARD
}
}
return scene
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TreeNode } from '../../ProjectAssetExplorer/ProjectView'
import { AssetNodeItem } from '../../ProjectAssetExplorer/types'
import { isAssetNode } from '../../ProjectAssetExplorer/utils'
import { ACCEPTED_FILE_TYPES } from '../../ui/FileUploadField/types'
import { TransitionMode } from '../../../lib/sdk/components/SceneMetadata'

function getValue(coord: SceneSpawnPointCoord) {
return coord.$case === 'range' ? (coord.value[0] + coord.value[1]) / 2 : coord.value
Expand Down Expand Up @@ -71,6 +72,10 @@ export function fromScene(value: EditorComponentsTypes['Scene']): SceneInput {
tags: value.tags ? value.tags.join(', ') : '',
author: value.author || '',
email: value.email || '',
skyboxConfig: {
fixedTime: String(value.skyboxConfig?.fixedTime ?? MIDDAY_SECONDS),
transitionMode: String(value.skyboxConfig?.transitionMode ?? TransitionMode.TM_FORWARD)
},
silenceVoiceChat: typeof value.silenceVoiceChat === 'boolean' ? value.silenceVoiceChat : false,
disablePortableExperiences:
typeof value.disablePortableExperiences === 'boolean' ? value.disablePortableExperiences : false,
Expand All @@ -94,6 +99,10 @@ export function toScene(inputs: SceneInput): EditorComponentsTypes['Scene'] {
tags: inputs.tags.split(',').map((tag) => tag.trim()),
author: inputs.author,
email: inputs.email,
skyboxConfig: {
fixedTime: Number(inputs.skyboxConfig.fixedTime ?? MIDDAY_SECONDS),
transitionMode: Number(inputs.skyboxConfig.transitionMode) as TransitionMode
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above for all the previous ones

silenceVoiceChat: inputs.silenceVoiceChat,
disablePortableExperiences: inputs.disablePortableExperiences,
spawnPoints: inputs.spawnPoints.map((spawnPoint, index) =>
Expand Down Expand Up @@ -131,3 +140,6 @@ export const isImageFile = (value: string): boolean =>
ACCEPTED_FILE_TYPES['image'].some((extension) => value.endsWith(extension))

export const isImage = (node: TreeNode): node is AssetNodeItem => isAssetNode(node) && isImageFile(node.name)

export const MIDDAY_SECONDS = 43200
export const MIDNIGHT_SECONDS = 86400
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@import '../RangeField/RangeField.css';

.RangeHour.Range.Field .RangeContainer input[type='time'] {
width: 85px;
padding: 4px 8px;
background-color: var(--background-gray);
border-radius: 4px;
border: 1px solid var(--background-gray);
color: var(--text);
font-size: 14px;
text-align: center;
}

.RangeHour.Range.Field .RangeContainer input:hover:not(:disabled) {
border-color: var(--base-09);
}

.RangeHour.Range.Field .RangeContainer input:focus {
outline: none;
border-color: var(--base-01);
}

.RangeHour.Range.Field .RangeContainer input:disabled {
background-color: var(--base-12);
border-color: var(--base-12);
color: var(--base-09);
cursor: not-allowed;
}

.RangeHour.Range.Field .RangeContainer input[type='time']::-webkit-calendar-picker-indicator {
display: none;
}

.RangeHour.Range.Field .RangeContainer .RangeInput {
background: linear-gradient(
90deg,
#361c75 0%,
#534787 15%,
#bf8dac 30%,
#46cee9 50%,
#af80a9 70%,
#714171 85%,
#40247b 100%
) !important;
height: 8px !important;
border-radius: 4px !important;
}

.RangeHour.Range.Field .RangeContainer .RangeInput::before,
.RangeHour.Range.Field .RangeContainer .RangeInput::after {
background: #361c75 !important;
}

.RangeHour.Range.Field .RangeContainer .RangeInput::-webkit-slider-thumb {
background: white !important;
}

.RangeHour.Range.Field .RangeContainer .RangeInput::-moz-range-thumb {
background: white !important;
}

.RangeHour.Range.Field .RangeContainer .RangeInput::-ms-thumb {
background: white !important;
}

.RangeHour.Range.Field .RangeContainer .RangeInput::-webkit-slider-runnable-track {
background: none !important;
}

.RangeHour.Range.Field .RangeContainer .RangeInput::-moz-range-track {
background: none !important;
}

.RangeHour.Range.Field .RangeContainer .RangeInput::-ms-track {
background: none !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import cx from 'classnames'
import { MIDNIGHT_SECONDS } from '../../../components/EntityInspector/SceneInspector/utils'
import { Props } from './types'

import './RangeHourField.css'

const MIN_SECONDS = 0
const STEP_SECONDS = 60
// 23:59 in seconds
const MAX_SECONDS = 86340

function formatHour(value: number): string {
const hours = Math.floor(value / 3600)
const minutes = Math.floor((value % 3600) / 60)
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
}

const RangeHourField = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
const { disabled, value = 0, onChange, ...rest } = props

const [time, setTime] = useState({
timeInSeconds: Number(value),
timeInHHMM: formatHour(Number(value))
})

useEffect(() => {
const numValue = Number(value)
if (numValue !== time.timeInSeconds) {
setTime({
timeInSeconds: numValue,
timeInHHMM: formatHour(numValue)
})
}
}, [value])

const completionPercentage = useMemo(() => {
const normalizedValue = Math.min(Math.max(time.timeInSeconds, MIN_SECONDS), MAX_SECONDS)
return ((normalizedValue - MIN_SECONDS) / (MAX_SECONDS - MIN_SECONDS)) * 100 || 0
}, [time.timeInSeconds])

const trackStyle = {
'--completionPercentage': `${completionPercentage}%`
} as any

const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const numValue = parseFloat(e.target.value)
const valueToSend = numValue === MIN_SECONDS ? MIDNIGHT_SECONDS : numValue
setTime({
timeInSeconds: numValue,
timeInHHMM: formatHour(numValue)
})
onChange &&
onChange({
...e,
target: { ...e.target, value: valueToSend.toString() }
} as React.ChangeEvent<HTMLInputElement>)
},
[onChange]
)

const parseTimeInput = useCallback((timeStr: string): number => {
const [hours = '0', minutes = '0'] = timeStr.split(':')
const totalSeconds = parseInt(hours) * 3600 + parseInt(minutes) * 60
// When input is 00:00, send MIDNIGHT_SECONDS (86400)
return totalSeconds === 0 ? MIDNIGHT_SECONDS : totalSeconds
}, [])

const handleChangeTextField = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value
const seconds = parseTimeInput(inputValue)
// When input is 00:00, send MIDNIGHT_SECONDS (86400)
const valueToSend = seconds === MIN_SECONDS ? MIDNIGHT_SECONDS : seconds
setTime({
timeInSeconds: seconds,
timeInHHMM: inputValue
})
onChange &&
onChange({
...e,
target: { ...e.target, value: valueToSend.toString() }
} as React.ChangeEvent<HTMLInputElement>)
},
[onChange, parseTimeInput]
)

return (
<div className="Range Field RangeHour">
<div className={cx('RangeContainer', { disabled })}>
<div className="InputContainer">
<input
ref={ref}
type="range"
className="RangeInput"
min={MIN_SECONDS}
max={MAX_SECONDS}
step={STEP_SECONDS}
value={time.timeInSeconds}
style={trackStyle}
disabled={disabled}
onChange={handleChange}
{...rest}
/>
</div>
<input
className="RangeTextInput"
type="time"
value={time.timeInHHMM}
disabled={disabled}
onChange={handleChangeTextField}
onBlur={handleChangeTextField}
/>
</div>
</div>
)
})

export default React.memo(RangeHourField)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { InputHTMLAttributes } from 'react'

export interface Props extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
value?: number
disabled?: boolean
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
}
Loading
Loading