Skip to content

Commit 2e39266

Browse files
authored
feat: add offset & tiling to Texture component (#1147)
1 parent 20813e7 commit 2e39266

7 files changed

Lines changed: 215 additions & 30 deletions

File tree

packages/@dcl/inspector/src/components/EntityInspector/MaterialInspector/Texture/Texture.tsx

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import { useCallback } from 'react'
2+
23
import { removeBasePath } from '../../../../lib/logic/remove-base-path'
34
import { Block } from '../../../Block'
45
import { Container } from '../../../Container'
5-
import { Dropdown, FileUploadField } from '../../../ui'
6+
import { Dropdown, FileUploadField, TextField } from '../../../ui'
67
import { ACCEPTED_FILE_TYPES } from '../../../ui/FileUploadField/types'
78
import { isModel, isValidTexture } from './utils'
89

910
import { Props, Texture, TEXTURE_TYPES, WRAP_MODES, FILTER_MODES } from './types'
1011

1112
function TextureInspector({ label, texture, files, getInputProps }: Props) {
13+
const getTextureProps = useCallback(
14+
(key: string) => {
15+
return getInputProps(`${texture}.${key}`)
16+
},
17+
[getInputProps, texture]
18+
)
19+
1220
const handleDrop = useCallback(
1321
(src: string) => {
14-
const srcInput = getInputProps(`${texture}.src`)
22+
const srcInput = getTextureProps('src')
1523
// The src comes with the basePath, so we need to remove it before setting the value because the utils fromTexture is adding it again
1624
// TODO: Refactor EntityInspector/MaterialInspector/Texture/utils.ts::fromTexture util to not remove the basePath
1725
const value = removeBasePath(files?.basePath ?? '', src)
@@ -23,34 +31,64 @@ function TextureInspector({ label, texture, files, getInputProps }: Props) {
2331
[files, texture, getInputProps]
2432
)
2533

26-
const type = getInputProps(`${texture}.type`)
27-
const src = getInputProps(`${texture}.src`)
28-
const isValid = isValidTexture(src.value, files)
34+
const isValid = useCallback(
35+
(value: string | number | readonly string[]) => {
36+
return isValidTexture(value, files)
37+
},
38+
[files]
39+
)
40+
41+
const type = getTextureProps('type')
2942

3043
return (
3144
<Container label={label} className={label} initialOpen={false} border>
3245
<Block>
3346
<Dropdown label="Type" options={TEXTURE_TYPES} {...type} />
3447
</Block>
48+
{type.value === Texture.TT_TEXTURE && (
49+
<NormalTexture getTextureProps={getTextureProps} handleDrop={handleDrop} isValid={isValid} />
50+
)}
3551
<Block>
36-
{type.value === Texture.TT_TEXTURE && (
37-
<FileUploadField
38-
{...src}
39-
label="Path"
40-
accept={ACCEPTED_FILE_TYPES['image']}
41-
onDrop={handleDrop}
42-
error={!!src.value && !isValid}
43-
isValidFile={isModel}
44-
acceptURLs
45-
/>
46-
)}
47-
{/* {type.value === Texture.TT_AVATAR_TEXTURE && <TextField label="User ID" {...getInputProps(`${texture}.userId`)} />}*/}
52+
<Dropdown label="Wrap mode" options={WRAP_MODES} {...getTextureProps('wrapMode')} />
53+
<Dropdown label="Filter node" options={FILTER_MODES} {...getTextureProps('filterMode')} />
4854
</Block>
55+
</Container>
56+
)
57+
}
58+
59+
function NormalTexture({
60+
getTextureProps,
61+
handleDrop,
62+
isValid
63+
}: {
64+
getTextureProps: (key: string) => ReturnType<Props['getInputProps']>
65+
handleDrop: (src: string) => void
66+
isValid: (value: string | number | readonly string[]) => boolean
67+
}) {
68+
const src = getTextureProps('src')
69+
70+
return (
71+
<>
4972
<Block>
50-
<Dropdown label="Wrap mode" options={WRAP_MODES} {...getInputProps(`${texture}.wrapMode`)} />
51-
<Dropdown label="Filter node" options={FILTER_MODES} {...getInputProps(`${texture}.filterMode`)} />
73+
<FileUploadField
74+
{...src}
75+
label="Path"
76+
accept={ACCEPTED_FILE_TYPES['image']}
77+
onDrop={handleDrop}
78+
error={!!src.value && !isValid(src.value)}
79+
isValidFile={isModel}
80+
acceptURLs
81+
/>
5282
</Block>
53-
</Container>
83+
<Block label="Offset">
84+
<TextField leftLabel="X" type="number" {...getTextureProps('offset.x')} autoSelect />
85+
<TextField leftLabel="Y" type="number" {...getTextureProps('offset.y')} autoSelect />
86+
</Block>
87+
<Block label="Tiling">
88+
<TextField leftLabel="X" type="number" {...getTextureProps('tiling.x')} autoSelect />
89+
<TextField leftLabel="Y" type="number" {...getTextureProps('tiling.y')} autoSelect />
90+
</Block>
91+
</>
5492
)
5593
}
5694

packages/@dcl/inspector/src/components/EntityInspector/MaterialInspector/Texture/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ export type TextureInput = {
2525
videoPlayerEntity?: string
2626
wrapMode: string
2727
filterMode: string
28+
offset?: {
29+
x: string
30+
y: string
31+
}
32+
tiling?: {
33+
x: string
34+
y: string
35+
}
2836
}
2937

3038
export const WRAP_MODES = [

packages/@dcl/inspector/src/components/EntityInspector/MaterialInspector/Texture/utils.spec.ts

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ describe('fromTexture', () => {
6060
texture: {
6161
src: 'image.png',
6262
wrapMode: TextureWrapMode.TWM_REPEAT,
63-
filterMode: TextureFilterMode.TFM_TRILINEAR
63+
filterMode: TextureFilterMode.TFM_TRILINEAR,
64+
offset: { x: 0.5, y: 0.25 },
65+
tiling: { x: 2.0, y: 1.5 }
6466
}
6567
}
6668
}
@@ -71,10 +73,37 @@ describe('fromTexture', () => {
7173
expect(result.src).toBe('image.png')
7274
expect(result.wrapMode).toBe(String(TextureWrapMode.TWM_REPEAT))
7375
expect(result.filterMode).toBe(String(TextureFilterMode.TFM_TRILINEAR))
76+
expect(result.offset).toEqual({ x: '0.50', y: '0.25' })
77+
expect(result.tiling).toEqual({ x: '2.00', y: '1.50' })
7478
})
7579

7680
it('should convert from texture without a base path', () => {
7781
const base = ''
82+
const value: TextureUnion = {
83+
tex: {
84+
$case: 'texture',
85+
texture: {
86+
src: 'image.png',
87+
wrapMode: TextureWrapMode.TWM_REPEAT,
88+
filterMode: TextureFilterMode.TFM_TRILINEAR,
89+
offset: { x: 0, y: 0 },
90+
tiling: { x: 1, y: 1 }
91+
}
92+
}
93+
}
94+
95+
const result = fromTexture(base, value)
96+
97+
expect(result.type).toBe(Texture.TT_TEXTURE)
98+
expect(result.src).toBe('image.png')
99+
expect(result.wrapMode).toBe(String(TextureWrapMode.TWM_REPEAT))
100+
expect(result.filterMode).toBe(String(TextureFilterMode.TFM_TRILINEAR))
101+
expect(result.offset).toEqual({ x: '0.00', y: '0.00' })
102+
expect(result.tiling).toEqual({ x: '1.00', y: '1.00' })
103+
})
104+
105+
it('should convert from texture with default offset and tiling when not provided', () => {
106+
const base = 'base-path'
78107
const value: TextureUnion = {
79108
tex: {
80109
$case: 'texture',
@@ -92,6 +121,33 @@ describe('fromTexture', () => {
92121
expect(result.src).toBe('image.png')
93122
expect(result.wrapMode).toBe(String(TextureWrapMode.TWM_REPEAT))
94123
expect(result.filterMode).toBe(String(TextureFilterMode.TFM_TRILINEAR))
124+
expect(result.offset).toEqual({ x: '0', y: '0' })
125+
expect(result.tiling).toEqual({ x: '1', y: '1' })
126+
})
127+
128+
it('should convert from texture with different offset and tiling values', () => {
129+
const base = 'base-path'
130+
const value: TextureUnion = {
131+
tex: {
132+
$case: 'texture',
133+
texture: {
134+
src: 'image.png',
135+
wrapMode: TextureWrapMode.TWM_REPEAT,
136+
filterMode: TextureFilterMode.TFM_TRILINEAR,
137+
offset: { x: 0.1, y: 0.3 },
138+
tiling: { x: 2.5, y: 3.0 }
139+
}
140+
}
141+
}
142+
143+
const result = fromTexture(base, value)
144+
145+
expect(result.type).toBe(Texture.TT_TEXTURE)
146+
expect(result.src).toBe('image.png')
147+
expect(result.wrapMode).toBe(String(TextureWrapMode.TWM_REPEAT))
148+
expect(result.filterMode).toBe(String(TextureFilterMode.TFM_TRILINEAR))
149+
expect(result.offset).toEqual({ x: '0.10', y: '0.30' })
150+
expect(result.tiling).toEqual({ x: '2.50', y: '3.00' })
95151
})
96152
})
97153

@@ -130,7 +186,28 @@ describe('toTexture', () => {
130186
expect(result.tex.videoTexture.filterMode).toBe(TextureFilterMode.TFM_POINT)
131187
})
132188

133-
it('should convert to texture', () => {
189+
it('should convert to texture with offset and tiling', () => {
190+
const base = 'base-path'
191+
const value: TextureInput = {
192+
type: Texture.TT_TEXTURE,
193+
src: 'image.png',
194+
wrapMode: String(TextureWrapMode.TWM_REPEAT),
195+
filterMode: String(TextureFilterMode.TFM_POINT),
196+
offset: { x: '0.5', y: '0.25' },
197+
tiling: { x: '2.0', y: '1.5' }
198+
}
199+
200+
const result = toTexture(base, value) as { tex: { $case: 'texture'; texture: EcsTexture } }
201+
202+
expect(result.tex.$case).toBe('texture')
203+
expect(result.tex.texture.src).toBe('base-path/image.png')
204+
expect(result.tex.texture.wrapMode).toBe(TextureWrapMode.TWM_REPEAT)
205+
expect(result.tex.texture.filterMode).toBe(TextureFilterMode.TFM_POINT)
206+
expect(result.tex.texture.offset).toEqual({ x: 0.5, y: 0.25 })
207+
expect(result.tex.texture.tiling).toEqual({ x: 2.0, y: 1.5 })
208+
})
209+
210+
it('should convert to texture with default offset and tiling when not provided', () => {
134211
const base = 'base-path'
135212
const value: TextureInput = {
136213
type: Texture.TT_TEXTURE,
@@ -145,6 +222,29 @@ describe('toTexture', () => {
145222
expect(result.tex.texture.src).toBe('base-path/image.png')
146223
expect(result.tex.texture.wrapMode).toBe(TextureWrapMode.TWM_REPEAT)
147224
expect(result.tex.texture.filterMode).toBe(TextureFilterMode.TFM_POINT)
225+
expect(result.tex.texture.offset).toEqual({ x: 0, y: 0 })
226+
expect(result.tex.texture.tiling).toEqual({ x: 1, y: 1 })
227+
})
228+
229+
it('should convert to texture with different offset and tiling values', () => {
230+
const base = 'base-path'
231+
const value: TextureInput = {
232+
type: Texture.TT_TEXTURE,
233+
src: 'image.png',
234+
wrapMode: String(TextureWrapMode.TWM_REPEAT),
235+
filterMode: String(TextureFilterMode.TFM_POINT),
236+
offset: { x: '0.1', y: '0.3' },
237+
tiling: { x: '2.5', y: '3.0' }
238+
}
239+
240+
const result = toTexture(base, value) as { tex: { $case: 'texture'; texture: EcsTexture } }
241+
242+
expect(result.tex.$case).toBe('texture')
243+
expect(result.tex.texture.src).toBe('base-path/image.png')
244+
expect(result.tex.texture.wrapMode).toBe(TextureWrapMode.TWM_REPEAT)
245+
expect(result.tex.texture.filterMode).toBe(TextureFilterMode.TFM_POINT)
246+
expect(result.tex.texture.offset).toEqual({ x: 0.1, y: 0.3 })
247+
expect(result.tex.texture.tiling).toEqual({ x: 2.5, y: 3.0 })
148248
})
149249
})
150250

packages/@dcl/inspector/src/components/EntityInspector/MaterialInspector/Texture/utils.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,15 @@ export const fromTexture = (base: string, value: TextureUnion): TextureInput =>
3333
type: Texture.TT_TEXTURE,
3434
src: isValidHttpsUrl(src) ? src : removeBasePath(base, src),
3535
wrapMode: toString(value?.tex?.texture.wrapMode),
36-
filterMode: toString(value?.tex?.texture.filterMode)
36+
filterMode: toString(value?.tex?.texture.filterMode),
37+
offset: {
38+
x: value?.tex?.texture.offset?.x?.toFixed(2) ?? '0',
39+
y: value?.tex?.texture.offset?.y?.toFixed(2) ?? '0'
40+
},
41+
tiling: {
42+
x: value?.tex?.texture.tiling?.x?.toFixed(2) ?? '1',
43+
y: value?.tex?.texture.tiling?.y?.toFixed(2) ?? '1'
44+
}
3745
}
3846
}
3947
}
@@ -70,7 +78,15 @@ export const toTexture = (base: string, value?: TextureInput): TextureUnion => {
7078
texture: {
7179
src: isValidHttpsUrl(src) ? src : (src && base ? base + '/' : '') + src,
7280
wrapMode: toNumber(value?.wrapMode ?? '0', TextureWrapMode.TWM_REPEAT),
73-
filterMode: toNumber(value?.filterMode ?? '0', TextureFilterMode.TFM_POINT)
81+
filterMode: toNumber(value?.filterMode ?? '0', TextureFilterMode.TFM_POINT),
82+
offset: {
83+
x: toNumber(value?.offset?.x ?? '0'),
84+
y: toNumber(value?.offset?.y ?? '0')
85+
},
86+
tiling: {
87+
x: toNumber(value?.tiling?.x ?? '1'),
88+
y: toNumber(value?.tiling?.y ?? '1')
89+
}
7490
}
7591
}
7692
}

packages/@dcl/inspector/src/components/EntityInspector/MaterialInspector/utils.spec.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ describe('fromMaterial', () => {
3232
expect(result.alphaTest).toBe('0.75')
3333
expect(result.castShadows).toBe(true)
3434
expect(result.diffuseColor).toEqual('#FF0000')
35-
expect(result.texture).toEqual({ type: 'texture', src: 'some-src', wrapMode: '0', filterMode: '0' })
35+
expect(result.texture).toEqual({
36+
type: 'texture',
37+
src: 'some-src',
38+
wrapMode: '0',
39+
filterMode: '0',
40+
offset: { x: '0', y: '0' },
41+
tiling: { x: '1', y: '1' }
42+
})
3643
})
3744

3845
it('should convert from pbr material', () => {
@@ -71,7 +78,14 @@ describe('fromMaterial', () => {
7178
expect(result.type).toBe(MaterialType.MT_PBR)
7279
expect(result.alphaTest).toBe('0.6')
7380
expect(result.castShadows).toBe(false)
74-
expect(result.texture).toEqual({ type: 'texture', src: 'some-src', wrapMode: '0', filterMode: '0' })
81+
expect(result.texture).toEqual({
82+
type: 'texture',
83+
src: 'some-src',
84+
wrapMode: '0',
85+
filterMode: '0',
86+
offset: { x: '0', y: '0' },
87+
tiling: { x: '1', y: '1' }
88+
})
7589
expect(result.bumpTexture).toEqual({ type: 'avatarTexture', userId: 'some-id', wrapMode: '2', filterMode: '2' })
7690
expect(result.metallic).toBe('0.5')
7791
expect(result.specularIntensity).toBe('1')
@@ -108,7 +122,9 @@ describe('toMaterial', () => {
108122
texture: {
109123
src: 'base-path/some-src',
110124
wrapMode: 1,
111-
filterMode: 1
125+
filterMode: 1,
126+
offset: { x: 0, y: 0 },
127+
tiling: { x: 1, y: 1 }
112128
}
113129
}
114130
})
@@ -147,7 +163,9 @@ describe('toMaterial', () => {
147163
texture: {
148164
src: 'base-path/some-src',
149165
wrapMode: 1,
150-
filterMode: 1
166+
filterMode: 1,
167+
offset: { x: 0, y: 0 },
168+
tiling: { x: 1, y: 1 }
151169
}
152170
}
153171
})

packages/@dcl/inspector/src/lib/babylon/decentraland/editorComponents/selection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const toggleSelection = (entity: EcsEntity, value: boolean) => {
4343
toggleMeshSelection(entity.meshRenderer, value)
4444
}
4545

46-
entity.onAssetLoaded().then(() => {
46+
void entity.onAssetLoaded().then(() => {
4747
if (entity.gltfContainer) {
4848
for (const mesh of entity.gltfContainer.getChildMeshes()) {
4949
if (mesh.name.includes('collider')) continue

packages/@dcl/inspector/src/lib/babylon/decentraland/sdkComponents/material.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,12 @@ async function loadTexture(entity: EcsEntity, tx: TextureUnion['tex']): Promise<
117117
if (!content) return null
118118
const textureBlob = new Blob([content])
119119
const textureUrl = URL.createObjectURL(textureBlob)
120-
return new Texture(textureUrl, entity.getScene(), true, true)
120+
const texture = new Texture(textureUrl, entity.getScene(), true, true)
121+
texture.uOffset = tx.texture.offset?.x ?? 0
122+
texture.vOffset = tx.texture.offset?.y ?? 0
123+
texture.uScale = tx.texture.tiling?.x ?? 1
124+
texture.vScale = tx.texture.tiling?.y ?? 1
125+
return texture
121126
})
122127
}
123128

0 commit comments

Comments
 (0)