Download src/shared/room-spatial.mjs from xlt99/EmboDi: direct link, hf CLI and curl.
- Browser
- Download file 5.13 kB
-
https://huggingface.co/spaces/xlt99/EmboDi/resolve/main/src/shared/room-spatial.mjs
- Command line
-
hf download hf://spaces/xlt99/EmboDi/src/shared/room-spatial.mjs
-
curl -L -o room-spatial.mjs https://huggingface.co/spaces/xlt99/EmboDi/resolve/main/src/shared/room-spatial.mjs
5.13 kB
| import {languagePattern} from "./language.mjs"; | |
| const dot = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0); | |
| const sub = (a, b) => a.map((v, i) => v - b[i]); | |
| const length = (a) => Math.hypot(...a); | |
| const point = (value) => Array.isArray(value) && value.length === 3 && value.every(Number.isFinite); | |
| function validateRoomMetrics(value) { | |
| if (!value || typeof value !== "object") throw new Error("Room dimensions are missing"); | |
| for (const key of ["width", "depth"]) if (!Number.isFinite(value[key]) || value[key] < 1 || value[key] > 40) throw new Error("Room width and depth must be between 1 and 40 meters"); | |
| if (!Number.isFinite(value.height) || value.height < 1.8 || value.height > 8) throw new Error("Room height must be between 1.8 and 8 meters"); | |
| if (!["estimated", "manual", "controller", "quest-planes"].includes(value.source)) throw new Error("Invalid room dimension source"); | |
| return { width: value.width, depth: value.depth, height: value.height, source: value.source }; | |
| } | |
| function calibrationFromCorners(a, b, c, height) { | |
| if (![a, b, c].every(point)) throw new Error("Invalid corner coordinates"); | |
| if (Math.max(a[1], b[1], c[1]) - Math.min(a[1], b[1], c[1]) > 0.15) throw new Error("All three corners must be on the same floor"); | |
| const ab = [b[0] - a[0], 0, b[2] - a[2]], width = length(ab), x = ab.map((v) => v / width), z = [-x[2], 0, x[0]], bc = sub(c, b), signedDepth = dot(bc, z), depth = Math.abs(signedDepth); | |
| if (!Number.isFinite(width) || width < 1) throw new Error("A and B are too close. Select two floor corners on the same wall."); | |
| if (Math.abs(dot(bc, x)) > Math.max(0.35, depth * 0.15)) throw new Error("C should be the next corner along the wall adjacent to B"); | |
| const metrics = validateRoomMetrics({ width, depth, height, source: "controller" }); | |
| return { metrics, origin: [a[0] + x[0] * width / 2 + z[0] * signedDepth / 2, a[1], a[2] + x[2] * width / 2 + z[2] * signedDepth / 2], yaw: Math.atan2(-x[2], x[0]) }; | |
| } | |
| function floorIntersection(origin, direction, floorY = 0) { | |
| if (!point(origin) || !point(direction) || direction[1] >= -0.015) return null; | |
| const t = (floorY - origin[1]) / direction[1]; | |
| if (t <= 0 || t > 20) return null; | |
| return origin.map((v, i) => v + t * direction[i]); | |
| } | |
| function ceilingHeightAtWall(origin, direction, a, b) { | |
| const ab = sub(b, a), normal = [-ab[2], 0, ab[0]], denom = dot(direction, normal); | |
| if (Math.abs(denom) < 0.02) throw new Error("From inside the room, aim at the ceiling edge above wall A–B"); | |
| const t = dot(sub(a, origin), normal) / denom; | |
| if (t <= 0 || t > 25) throw new Error("Aim at wall A–B"); | |
| const hit = origin.map((v, i) => v + t * direction[i]), u = dot(sub(hit, a), ab) / dot(ab, ab); | |
| if (u < -0.1 || u > 1.1) throw new Error("Aim at the upper edge of the wall between A and B"); | |
| const height = hit[1] - a[1]; | |
| if (height < 1.8 || height > 8) throw new Error("Measured height is outside the valid range. Aim at the wall–ceiling edge."); | |
| return height; | |
| } | |
| function roomFromPlanes(planes, preferredAspect = 1) { | |
| const horizontal = planes.filter((p) => p.orientation === "horizontal" && p.points?.length >= 3); | |
| const area = (p) => Math.abs(p.points.reduce((a, v, i) => { | |
| const q = p.points[(i + 1) % p.points.length]; | |
| return a + v[0] * q[2] - q[0] * v[2]; | |
| }, 0) / 2); | |
| const floor = horizontal.filter((p) => languagePattern("scene.floor").test(p.label || "")).sort((a, b) => area(b) - area(a))[0]; | |
| const ceiling = horizontal.filter((p) => languagePattern("scene.ceiling").test(p.label || "")).sort((a, b) => area(b) - area(a))[0]; | |
| if (!floor || !ceiling || area(floor) < 1) return null; | |
| let edge = [1, 0, 0], longest = 0; | |
| for (let i = 0; i < floor.points.length; i++) { | |
| const d = sub(floor.points[(i + 1) % floor.points.length], floor.points[i]); | |
| d[1] = 0; | |
| if (length(d) > longest) { | |
| longest = length(d); | |
| edge = d; | |
| } | |
| } | |
| if (longest < 1) return null; | |
| let x = edge.map((v) => v / longest), z = [-x[2], 0, x[0]]; | |
| const bounds = (axis) => { | |
| const values = floor.points.map((p) => dot(p, axis)); | |
| return [Math.min(...values), Math.max(...values)]; | |
| }; | |
| let bx = bounds(x), bz = bounds(z), width = bx[1] - bx[0], depth = bz[1] - bz[0]; | |
| if (Math.abs(Math.log(depth / width / preferredAspect)) < Math.abs(Math.log(width / depth / preferredAspect))) { | |
| const old = x; | |
| x = z; | |
| z = old.map((v) => -v); | |
| bx = bounds(x); | |
| bz = bounds(z); | |
| width = bx[1] - bx[0]; | |
| depth = bz[1] - bz[0]; | |
| } | |
| const floorY = floor.points.reduce((v, p) => v + p[1], 0) / floor.points.length, ceilingY = ceiling.points.reduce((v, p) => v + p[1], 0) / ceiling.points.length; | |
| try { | |
| const metrics = validateRoomMetrics({ width, depth, height: ceilingY - floorY, source: "quest-planes" }); | |
| return { metrics, origin: [x[0] * (bx[0] + bx[1]) / 2 + z[0] * (bz[0] + bz[1]) / 2, floorY, x[2] * (bx[0] + bx[1]) / 2 + z[2] * (bz[0] + bz[1]) / 2], yaw: Math.atan2(-x[2], x[0]) }; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| export { | |
| calibrationFromCorners, | |
| ceilingHeightAtWall, | |
| floorIntersection, | |
| roomFromPlanes, | |
| validateRoomMetrics | |
| }; | |