| const modelResourcesURL = new URL("../../model-element/resources/", document.currentScript.src); | |
| const createPortal = (test, { width, height, portalTransform, portalAction } = {}) => { | |
| const portal = document.createElement("div"); | |
| portal.className = "portal"; | |
| if (width) | |
| portal.style.width = width; | |
| if (height) | |
| portal.style.height = height; | |
| if (portalTransform) | |
| portal.style.portalTransform = portalTransform; | |
| if (portalAction) | |
| portal.style.portalAction = portalAction; | |
| document.body.appendChild(portal); | |
| test.add_cleanup(() => portal.remove()); | |
| return portal; | |
| }; | |
| const appendModel = (portal, asset, stageMode) => { | |
| const model = document.createElement("model"); | |
| if (stageMode) | |
| model.stageMode = stageMode; | |
| portal.appendChild(model); | |
| const source = document.createElement("source"); | |
| source.src = new URL(asset, modelResourcesURL).href; | |
| model.appendChild(source); | |
| return model; | |
| }; | |
| // Wraps the internals values back into a DOMMatrix. | |
| const resolvedPortalTransform = portal => { | |
| const values = internals.spatialPortalResolvedTransform(portal); | |
| return values ? new DOMMatrixReadOnly(values) : null; | |
| }; | |
| const portalTransformsApproxEqual = (a, b) => { | |
| if (!a || !b) | |
| return false; | |
| const aValues = a.toFloat64Array(); | |
| const bValues = b.toFloat64Array(); | |
| return aValues.every((value, index) => Math.abs(value - bValues[index]) <= epsilon); | |
| }; | |
| const portalTransformScaleIsUnit = transform => { | |
| return !!transform | |
| && Math.abs(transform.m11 - 1) < epsilon | |
| && Math.abs(transform.m22 - 1) < epsilon | |
| && Math.abs(transform.m33 - 1) < epsilon; | |
| }; | |
| const portalTransformIsResolved = transform => !!transform; | |
| const portalTransformIsCleared = transform => transform === null; | |
| const resolvedTransformForPortal = async (test, asset, portalTransform, portalOptions = { }) => { | |
| const portal = createPortal(test, { ...portalOptions, portalTransform }); | |
| const model = appendModel(portal, asset); | |
| await model.ready; | |
| return await waitForPortalTransform(portal, portalTransformIsResolved, `portal-transform: ${portalTransform ?? "auto"}`); | |
| }; | |
| async function waitForPortalTransform(portal, predicate, description, timeout = 5000) { | |
| const startTime = Date.now(); | |
| while (true) { | |
| const transform = resolvedPortalTransform(portal); | |
| if (predicate(transform)) | |
| return transform; | |
| if (Date.now() - startTime > timeout) | |
| throw new Error(`Timeout waiting for the portal's resolved transform: ${description}`); | |
| await sleepForSeconds(0.1); | |
| } | |
| } | |
| async function waitForEntityTransform(model, predicate, description, timeout = 5000) { | |
| const startTime = Date.now(); | |
| while (true) { | |
| const transform = model.entityTransform; | |
| if (predicate(transform)) | |
| return transform; | |
| if (Date.now() - startTime > timeout) | |
| throw new Error(`Timeout waiting for the child's entity transform: ${description}`); | |
| await sleepForSeconds(0.1); | |
| } | |
| } | |
| async function snapshotOnceModelsRender(afterReady) | |
| { | |
| window.testRunner?.waitUntilDone(); | |
| window.internals?.disableModelLoadDelaysForTesting(); | |
| await Promise.all([...document.querySelectorAll("model")].map(model => model.ready)); | |
| await UIHelper.ensureStablePresentationUpdate(); | |
| if (afterReady) { | |
| afterReady(); | |
| await UIHelper.ensureStablePresentationUpdate(); | |
| } | |
| await UIHelper.renderingUpdate(); | |
| window.testRunner?.notifyDone(); | |
| } | |
| // The 3D matrix assertions reject a 2D argument outright, and DOMMatrix stays 2D until an operation touches z. | |
| const as3d = matrix => new DOMMatrixReadOnly(matrix.toFloat64Array()); | |
| const effectiveEnvironmentMap = element => internals.effectiveEnvironmentMap(element); | |
| const declareEnvironmentMap = (test, { name, format = "equirectangular", src } = {}) => { | |
| const descriptors = []; | |
| if (name !== undefined) | |
| descriptors.push(`name: "${name}"`); | |
| if (format !== undefined) | |
| descriptors.push(`format: ${format}`); | |
| if (src !== undefined) | |
| descriptors.push(`src: url("${src}")`); | |
| const text = `@environment-map { ${descriptors.join("; ")} }`; | |
| const style = document.createElement("style"); | |
| style.textContent = text; | |
| document.head.appendChild(style); | |
| test.add_cleanup(() => style.remove()); | |
| return style; | |
| }; | |
| async function waitForEnvironmentMap(element, predicate, description, timeout = 5000) { | |
| const startTime = Date.now(); | |
| while (true) { | |
| const value = effectiveEnvironmentMap(element); | |
| if (predicate(value)) | |
| return value; | |
| if (Date.now() - startTime > timeout) | |
| throw new Error(`Timeout waiting for the effective environment map: ${description}`); | |
| await sleepForSeconds(0.1); | |
| } | |
| } | |
| const parseSceneGraph = text => { | |
| const entities = []; | |
| const openEntities = []; | |
| for (const line of text.split("\n")) { | |
| const depth = line.search(/\S/); | |
| const entityMatch = /^\s*\(entity(?: (.+))?$/.exec(line); | |
| if (entityMatch) { | |
| while (openEntities.length && openEntities.at(-1).depth >= depth) | |
| openEntities.pop(); | |
| const entity = { name: entityMatch[1] ?? "", element: null, rotation: [0, 0, 0, 1], parent: openEntities.at(-1) ?? null, depth }; | |
| entities.push(entity); | |
| openEntities.push(entity); | |
| continue; | |
| } | |
| const elementMatch = /^\s*\(element (.+)\)$/.exec(line); | |
| if (elementMatch && openEntities.length) | |
| openEntities.at(-1).element = elementMatch[1]; | |
| const rotationMatch = /^\s*\(rotation (.+)\)$/.exec(line); | |
| if (rotationMatch && openEntities.length) | |
| openEntities.at(-1).rotation = rotationMatch[1].split(" ").map(Number); | |
| } | |
| return entities; | |
| }; | |
| const parentOfElement = (entities, label) => entities.find(entity => entity.element === label)?.parent; | |
| const modelSceneGraph = async element => parseSceneGraph(await internals.modelSceneGraphAsText(element)); |