Ergänze 3D-Vorschau zur individuellen Haar- und Augenfarbe
All checks were successful
Deploy to production / deploy (push) Successful in 2m36s

This commit is contained in:
Torsten Schulz (local)
2026-11-04 15:45:02 +01:00
parent 6e31a114cf
commit b805632826
9 changed files with 695 additions and 42 deletions

View File

@@ -0,0 +1,76 @@
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 <map_fragment>\n#include <emissivemap_fragment>' });
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);
});