import assert from 'node:assert/strict'; import test from 'node:test'; import { BoxGeometry, Group, Mesh, MeshStandardMaterial, Vector3 } from 'three'; import { applyCharacterAppearance, captureAppearanceEditor, getAppearanceRegion, getAppearanceStats, paintAppearanceRegion, prepareAppearanceEditor, restoreAppearanceEditor, } from '../src/utils/characterAppearance.js'; function fixture(material = new MeshStandardMaterial()) { const model = new Group(); const mesh = new Mesh(new BoxGeometry(), material); model.add(mesh); model.updateMatrixWorld(true); return { model, mesh }; } test('painting separates material regions while preserving the original mesh and UVs', () => { const { model, mesh } = fixture(); const original = mesh.geometry; const originalUV = original.getAttribute('uv').array.slice(); prepareAppearanceEditor(model); assert.notEqual(mesh.geometry, original); assert.deepEqual(getAppearanceStats(model), { body: 12, hair: 0, iris: 0 }); const snapshot = captureAppearanceEditor(model); const indexBuffer = mesh.geometry.index; assert.equal(paintAppearanceRegion({ object: mesh, faceIndex: 0, point: new Vector3(.5, 0, 0) }, 0, 'hair', new Vector3(-1, 0, 0)), true); assert.deepEqual(getAppearanceStats(model), { body: 11, hair: 1, iris: 0 }); assert.deepEqual(mesh.geometry.getAttribute('uv').array, originalUV); assert.equal(mesh.geometry.index, indexBuffer); assert.equal(original.groups.length, 6); restoreAppearanceEditor(snapshot); assert.deepEqual(getAppearanceStats(model), { body: 12, hair: 0, iris: 0 }); assert.equal(paintAppearanceRegion({ object: mesh, faceIndex: 0, point: new Vector3() }, 1, 'unknown', new Vector3()), false); }); test('hair and iris colors remain independent of other instances and can return to the original', () => { const originalMaterial = new MeshStandardMaterial(); const first = fixture(originalMaterial); const second = fixture(originalMaterial); prepareAppearanceEditor(first.model); prepareAppearanceEditor(second.model); applyCharacterAppearance(first.model, { hairColor: '#ff0000', eyeColor: '#0000ff' }); applyCharacterAppearance(second.model, { hairColor: '#00ff00' }); const shader = () => ({ uniforms: {}, fragmentShader: '#include \n#include ' }); const hairShader = shader(), irisShader = shader(), otherShader = shader(); first.mesh.material[1].onBeforeCompile(hairShader); first.mesh.material[2].onBeforeCompile(irisShader); second.mesh.material[1].onBeforeCompile(otherShader); assert.equal(hairShader.uniforms.falukantTargetColor.value.getHexString(), 'ff0000'); assert.equal(irisShader.uniforms.falukantTargetColor.value.getHexString(), '0000ff'); assert.equal(otherShader.uniforms.falukantTargetColor.value.getHexString(), '00ff00'); applyCharacterAppearance(first.model, { hairColor: '#cccccc', eyeColor: null }); assert.equal(hairShader.uniforms.falukantTargetColor.value.getHexString(), 'cccccc'); assert.equal(irisShader.uniforms.falukantColorEnabled.value, 0); assert.equal(otherShader.uniforms.falukantTargetColor.value.getHexString(), '00ff00'); applyCharacterAppearance(first.model, { hairColor: 'invalid' }); assert.equal(hairShader.uniforms.falukantColorEnabled.value, 0); }); test('saved glTF material metadata can be prepared again without multiplying materials', () => { const first = fixture(); prepareAppearanceEditor(first.model); const model = new Group(); const mesh = new Mesh(first.mesh.geometry.clone(), first.mesh.material.map(material => { const copy = material.clone(); copy.userData = JSON.parse(JSON.stringify(material.userData)); return copy; })); model.add(mesh); prepareAppearanceEditor(model); assert.equal(mesh.material.length, 3); assert.equal(getAppearanceRegion(mesh.material[1]), 'hair'); assert.equal(getAppearanceRegion(mesh.material[2]), 'iris'); prepareAppearanceEditor(model); assert.equal(mesh.material.length, 3); });