fix(viewer): guard texture.repeat against non-array values (Sentry MONOREPO-EDITOR-EK) (#504)

* fix(viewer): guard texture.repeat against non-array values (Sentry MONOREPO-EDITOR-EK)

* fix(viewer): normalize legacy texture repeats

---------

Co-authored-by: openclaw-agent <agent@pascal.app>
Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com>
This commit is contained in:
Anton
2026-07-19 17:40:18 +02:00
committed by GitHub
co-authored by openclaw-agent Aymeric Rabot
parent d0ac9fb739
commit 16ea186f77
2 changed files with 53 additions and 6 deletions
+32
View File
@@ -0,0 +1,32 @@
// @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
// depend on @types/bun so the import type is unresolved at compile time.
import { describe, expect, test } from 'bun:test'
import type { MaterialSchema } from '@pascal-app/core'
import { getTextureKey, resolveTextureRepeat } from './materials'
function materialWithRepeat(repeat: unknown): MaterialSchema {
return {
texture: {
url: 'https://example.com/texture.png',
repeat,
},
} as unknown as MaterialSchema
}
describe('legacy texture repeat values', () => {
test('normalizes tuple, scalar, and Vector2-shaped repeats', () => {
expect(resolveTextureRepeat([2, 3], undefined)).toEqual([2, 3])
expect(resolveTextureRepeat(2, undefined)).toEqual([2, 2])
expect(resolveTextureRepeat({ x: 2, y: 3 }, undefined)).toEqual([2, 3])
})
test('falls back to scale for malformed repeats', () => {
expect(resolveTextureRepeat({ width: 2 }, 4)).toEqual([4, 4])
})
test('keeps distinct Vector2-shaped repeats in distinct cache entries', () => {
expect(getTextureKey(materialWithRepeat({ x: 2, y: 3 }))).not.toBe(
getTextureKey(materialWithRepeat({ x: 4, y: 5 })),
)
})
})
+21 -6
View File
@@ -180,12 +180,28 @@ function getCacheKey(props: MaterialProperties, shading: RenderShading): string
return `${shading}-${props.color}-${props.roughness}-${props.metalness}-${props.opacity}-${props.transparent}-${props.side}` return `${shading}-${props.color}-${props.roughness}-${props.metalness}-${props.opacity}-${props.transparent}-${props.side}`
} }
function getTextureKey(material?: MaterialSchema): string { function isFiniteNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value)
}
export function resolveTextureRepeat(repeat: unknown, scale: unknown): [number, number] {
const fallback = isFiniteNumber(scale) ? scale : 1
if (Array.isArray(repeat) && isFiniteNumber(repeat[0]) && isFiniteNumber(repeat[1])) {
return [repeat[0], repeat[1]]
}
if (isFiniteNumber(repeat)) return [repeat, repeat]
if (repeat && typeof repeat === 'object' && 'x' in repeat && 'y' in repeat) {
const { x, y } = repeat
if (isFiniteNumber(x) && isFiniteNumber(y)) return [x, y]
}
return [fallback, fallback]
}
export function getTextureKey(material?: MaterialSchema): string {
const texture = material?.texture const texture = material?.texture
if (!texture) return 'none' if (!texture) return 'none'
const repeat = texture.repeat?.join('x') ?? 'default' const [repeatX, repeatY] = resolveTextureRepeat(texture.repeat, texture.scale)
const scale = texture.scale ?? 'default' return `${texture.url}-${repeatX}x${repeatY}`
return `${texture.url}-${repeat}-${scale}`
} }
function getTexture(material?: MaterialSchema): THREE.Texture | undefined { function getTexture(material?: MaterialSchema): THREE.Texture | undefined {
@@ -200,8 +216,7 @@ function getTexture(material?: MaterialSchema): THREE.Texture | undefined {
texture.wrapS = THREE.RepeatWrapping texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping texture.wrapT = THREE.RepeatWrapping
const repeatX = textureConfig.repeat?.[0] ?? textureConfig.scale ?? 1 const [repeatX, repeatY] = resolveTextureRepeat(textureConfig.repeat, textureConfig.scale)
const repeatY = textureConfig.repeat?.[1] ?? textureConfig.scale ?? 1
texture.repeat.set(repeatX, repeatY) texture.repeat.set(repeatX, repeatY)
texture.updateMatrix() texture.updateMatrix()
texture.colorSpace = THREE.SRGBColorSpace texture.colorSpace = THREE.SRGBColorSpace