From ac681861258e8d31b1dd710e97f7b5da2a785854 Mon Sep 17 00:00:00 2001 From: openclaw-agent Date: Fri, 17 Jul 2026 04:05:49 +0000 Subject: [PATCH 1/2] fix(viewer): guard texture.repeat against non-array values (Sentry MONOREPO-EDITOR-EK) --- packages/viewer/src/lib/materials.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/viewer/src/lib/materials.ts b/packages/viewer/src/lib/materials.ts index c150139d3..41c663bd6 100644 --- a/packages/viewer/src/lib/materials.ts +++ b/packages/viewer/src/lib/materials.ts @@ -183,7 +183,9 @@ function getCacheKey(props: MaterialProperties, shading: RenderShading): string function getTextureKey(material?: MaterialSchema): string { const texture = material?.texture if (!texture) return 'none' - const repeat = texture.repeat?.join('x') ?? 'default' + const repeat = Array.isArray(texture.repeat) + ? texture.repeat.join('x') + : (texture.repeat ?? 'default') const scale = texture.scale ?? 'default' return `${texture.url}-${repeat}-${scale}` } From c40517d6b2527a201e9025044c60c66d183ee96f Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Sun, 19 Jul 2026 17:37:41 +0200 Subject: [PATCH 2/2] fix(viewer): normalize legacy texture repeats --- packages/viewer/src/lib/materials.test.ts | 32 +++++++++++++++++++++++ packages/viewer/src/lib/materials.ts | 29 ++++++++++++++------ 2 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 packages/viewer/src/lib/materials.test.ts diff --git a/packages/viewer/src/lib/materials.test.ts b/packages/viewer/src/lib/materials.test.ts new file mode 100644 index 000000000..57d170ebf --- /dev/null +++ b/packages/viewer/src/lib/materials.test.ts @@ -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 })), + ) + }) +}) diff --git a/packages/viewer/src/lib/materials.ts b/packages/viewer/src/lib/materials.ts index 41c663bd6..504e30344 100644 --- a/packages/viewer/src/lib/materials.ts +++ b/packages/viewer/src/lib/materials.ts @@ -180,14 +180,28 @@ function getCacheKey(props: MaterialProperties, shading: RenderShading): string 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 if (!texture) return 'none' - const repeat = Array.isArray(texture.repeat) - ? texture.repeat.join('x') - : (texture.repeat ?? 'default') - const scale = texture.scale ?? 'default' - return `${texture.url}-${repeat}-${scale}` + const [repeatX, repeatY] = resolveTextureRepeat(texture.repeat, texture.scale) + return `${texture.url}-${repeatX}x${repeatY}` } function getTexture(material?: MaterialSchema): THREE.Texture | undefined { @@ -202,8 +216,7 @@ function getTexture(material?: MaterialSchema): THREE.Texture | undefined { texture.wrapS = THREE.RepeatWrapping texture.wrapT = THREE.RepeatWrapping - const repeatX = textureConfig.repeat?.[0] ?? textureConfig.scale ?? 1 - const repeatY = textureConfig.repeat?.[1] ?? textureConfig.scale ?? 1 + const [repeatX, repeatY] = resolveTextureRepeat(textureConfig.repeat, textureConfig.scale) texture.repeat.set(repeatX, repeatY) texture.updateMatrix() texture.colorSpace = THREE.SRGBColorSpace