Finished up most documentation

This commit is contained in:
2026-09-10 21:11:07 -07:00
parent 2cfa0ca690
commit 8355743ada
14 changed files with 1871 additions and 868 deletions

View File

@@ -0,0 +1,127 @@
import * as THREE from 'three'
import { PackOpening, packContents } from '../src/packOpening.ts'
import { createStudioCubeTexture } from '../src/cardMaterial.ts'
export async function verifyPackPreparation() {
const startedAt = performance.now()
const loader = new THREE.TextureLoader()
const paths = ['david-front', 'david-finish-mask', 'timothy-front', 'timothy-finish-mask', 'card-back', 'linen-normal']
const textures = await Promise.all(paths.map((path) => loader.loadAsync(`/reference/${path}.png`)))
for (const index of [0, 2, 4]) textures[index].colorSpace = THREE.SRGBColorSpace
textures[5].wrapS = textures[5].wrapT = THREE.RepeatWrapping
const environment = createStudioCubeTexture()
const results = []
try {
for (const lightType of ['Point', 'Directional', 'Spot']) {
// A fresh context prevents the preceding pixel test from accidentally warming pack shaders.
const renderer = new THREE.WebGLRenderer({ antialias: false })
renderer.setSize(192, 264)
renderer.outputColorSpace = THREE.SRGBColorSpace
renderer.toneMapping = THREE.ACESFilmicToneMapping
const shaderErrors = []
renderer.debug.onShaderError = (gl, program, vertex, fragment) => {
shaderErrors.push([gl.getProgramInfoLog(program), gl.getShaderInfoLog(vertex),
gl.getShaderInfoLog(fragment)].join('\n'))
}
const scene = new THREE.Scene()
scene.background = new THREE.Color('#0b0e14')
scene.environment = environment
const lightPosition = new THREE.Vector3(2.2, 2.5, 4.4)
const light = lightType === 'Point' ? new THREE.PointLight('#fff0d0', 34, 20, 1.7)
: lightType === 'Directional' ? new THREE.DirectionalLight('#fff0d0', 5.4)
: new THREE.SpotLight('#fff0d0', 54, 20, THREE.MathUtils.degToRad(28), 0.55, 1.7)
light.position.copy(lightPosition)
scene.add(light)
if (light.target) scene.add(light.target)
const backMaterial = new THREE.MeshPhysicalMaterial({
map: textures[4], roughness: 0.42, metalness: 0, clearcoat: 0.2,
clearcoatRoughness: 0.24, normalMap: null, side: THREE.DoubleSide,
})
const pack = new PackOpening({
canvas: renderer.domElement,
textures: {
David: { artwork: textures[0], mask: textures[1] },
Timothy: { artwork: textures[2], mask: textures[3] },
},
backMaterial, normalMap: textures[5], environment, lightPosition, onChange() {},
})
pack.resize(192, 264)
const revealed = []
renderer.domElement.addEventListener('packreveal', (event) => revealed.push(event.detail))
const originalNow = performance.now
try {
const programsBeforePreparation = renderer.info.programs.length
await pack.prepareGPU(renderer, scene)
if (pack.root.visible || pack.state !== 'sealed' || revealed.length) {
throw new Error('Preparation changed visibility or reveal state')
}
const preparedPrograms = new Set(renderer.info.programs.map((program) => program.id))
if (!preparedPrograms.size) throw new Error('Preparation compiled no programs')
const texturesAfterPreparation = renderer.info.memory.textures
const unexpectedPrograms = new Map()
const visitedStates = new Set()
let renderedFrames = 0
let maximumTextureCount = texturesAfterPreparation
const textureGrowth = []
let now = originalNow.call(performance)
performance.now = () => now
function frame() {
pack.tick(now)
renderer.render(scene, pack.camera)
renderedFrames++
visitedStates.add(pack.state)
if (renderer.info.memory.textures > maximumTextureCount) {
textureGrowth.push({ state: pack.state, count: renderer.info.memory.textures })
}
maximumTextureCount = Math.max(maximumTextureCount, renderer.info.memory.textures)
for (const program of renderer.info.programs) {
if (!preparedPrograms.has(program.id) && !unexpectedPrograms.has(program.id)) {
unexpectedPrograms.set(program.id, { id: program.id, name: program.name, firstState: pack.state })
}
}
const gl = renderer.getContext()
if (shaderErrors.length || gl.isContextLost() || gl.getError() !== gl.NO_ERROR) {
throw new Error(`Pack WebGL rendering failed: ${shaderErrors.join('\n')}`)
}
}
scene.add(pack.root)
pack.setActive(true)
frame()
const durations = { opening: 3200, lifting: 620, revealing: 820, advancing: 650 }
for (let action = 0; action < 10 && pack.state !== 'complete'; action++) {
pack.primary()
const duration = durations[pack.state]
if (!duration) throw new Error(`Unexpected animated state: ${pack.state}`)
const started = now
for (const progress of [0, 0.1, 0.25, 0.4, 0.5, 0.65, 0.8, 0.9, 1.001]) {
now = started + duration * progress
frame()
}
}
if (pack.state !== 'complete' || JSON.stringify(revealed.map(({ fixture, finish, substrate }) =>
({ fixture, finish, substrate }))) !== JSON.stringify(packContents)) {
throw new Error('First reveal cycle did not complete in authored order')
}
pack.restart()
frame()
results.push({
lightType, passed: unexpectedPrograms.size === 0, programsBeforePreparation,
programsAfterPreparation: preparedPrograms.size,
programsAfterRevealAndRestart: renderer.info.programs.length,
unexpectedPrograms: [...unexpectedPrograms.values()], texturesAfterPreparation,
maximumTextureCount, textureGrowth, renderedFrames, visitedStates: [...visitedStates], revealed,
})
} finally {
performance.now = originalNow
pack.dispose()
backMaterial.dispose()
renderer.dispose()
renderer.forceContextLoss()
}
}
return { passed: results.every((result) => result.passed),
durationMs: Math.round(performance.now() - startedAt), results }
} finally {
for (const texture of [...textures, environment]) texture.dispose()
}
}