Download src/shared/interaction-regions.mjs from xlt99/EmboDi: direct link, hf CLI and curl.
- Browser
- Download file 9.03 kB
-
https://huggingface.co/spaces/xlt99/EmboDi/resolve/main/src/shared/interaction-regions.mjs
- Command line
-
hf download hf://spaces/xlt99/EmboDi/src/shared/interaction-regions.mjs
-
curl -L -o interaction-regions.mjs https://huggingface.co/spaces/xlt99/EmboDi/resolve/main/src/shared/interaction-regions.mjs
9.03 kB
| import { isDoor } from "./doors.mjs"; | |
| import { smoothCurve } from "./curve-smoothing.mjs"; | |
| import { isRegionObject, validateSurfaceFrame, objectRegionFrame, surfaceLocalPoint } from "./region-surfaces.mjs"; | |
| import { isRegionObject as isRegionObject2 } from "./region-surfaces.mjs"; | |
| const regionObjectId = (r) => r?.objectId || r?.doorId; | |
| const isSourceRegion = (r) => r?.surface === "object-surface" || r?.surface === "door-frame"; | |
| const REGION_LIMITS = Object.freeze({ points: 128, regions: 32, perDoor: 8, spacing: 0.018, maxGap: 0.6, perimeter: 30 }); | |
| const finite = (n) => Number.isFinite(n) && Math.abs(n) <= 100; | |
| const distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]); | |
| const cross = (a, b, c) => (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); | |
| const polygonArea = (points) => Math.abs(points.reduce((s, p, i) => { | |
| const q = points[(i + 1) % points.length]; | |
| return s + p[0] * q[1] - q[0] * p[1]; | |
| }, 0)) / 2; | |
| function edgeDistance(p, a, b) { | |
| const dx = b[0] - a[0], dy = b[1] - a[1], t = Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / (dx * dx + dy * dy || 1))); | |
| return distance(p, [a[0] + dx * t, a[1] + dy * t]); | |
| } | |
| function polygonContains(points, p, margin = 0) { | |
| let inside = false, nearest = Infinity; | |
| for (let i = 0, j = points.length - 1; i < points.length; j = i++) { | |
| const a = points[i], b = points[j]; | |
| nearest = Math.min(nearest, edgeDistance(p, a, b)); | |
| if (a[1] > p[1] !== b[1] > p[1] && p[0] < (b[0] - a[0]) * (p[1] - a[1]) / (b[1] - a[1]) + a[0]) inside = !inside; | |
| } | |
| return inside || nearest <= margin + 1e-8; | |
| } | |
| function intersects(a, b, c, d) { | |
| const x = cross(a, b, c), y = cross(a, b, d), z = cross(c, d, a), w = cross(c, d, b); | |
| return x * y < 0 && z * w < 0 || Math.abs(x) < 1e-9 && edgeDistance(c, a, b) < 1e-8 || Math.abs(y) < 1e-9 && edgeDistance(d, a, b) < 1e-8 || Math.abs(z) < 1e-9 && edgeDistance(a, c, d) < 1e-8 || Math.abs(w) < 1e-9 && edgeDistance(b, c, d) < 1e-8; | |
| } | |
| function validatePolygon(points, surface, maxPoints = REGION_LIMITS.points) { | |
| if (!Array.isArray(points) || points.length < 3 || points.length > maxPoints || points.some((p) => !Array.isArray(p) || p.length !== 2 || !p.every(finite))) throw Error("Invalid or excessive region points"); | |
| const area = polygonArea(points); | |
| if (area < (surface === "floor" ? 0.04 : 2e-3) || area > 30) throw Error("Region is too small or too large. Draw it again."); | |
| let length = 0; | |
| for (let i = 0; i < points.length; i++) { | |
| const a = points[i], b = points[(i + 1) % points.length], step = distance(a, b); | |
| if (step < 1e-5) throw Error("Region has duplicate vertices"); | |
| length += step; | |
| for (let j = i + 2; j < points.length; j++) { | |
| if (i === 0 && j === points.length - 1) continue; | |
| if (intersects(a, b, points[j], points[(j + 1) % points.length])) throw Error("Region boundaries cannot cross. Draw the loop again."); | |
| } | |
| } | |
| if (length > REGION_LIMITS.perimeter) throw Error("Region boundary is too long"); | |
| return points; | |
| } | |
| function closeRegionStroke(raw, surface) { | |
| if (raw.length < 4) throw Error("Draw a closed region"); | |
| if (distance(raw[0], raw.at(-1)) > 0.18) throw Error("Return near the starting point to close the region"); | |
| const points = raw.filter((p, i) => i === 0 || distance(p, raw[i - 1]) > 1e-5).map((p) => [...p]); | |
| if (distance(points[0], points.at(-1)) < 0.025) points.pop(); | |
| validatePolygon(points, surface, 1024); | |
| const closed = [...points, points[0]].map((p) => [p[0], 0, p[1]]), smooth = smoothCurve(closed, "standard", { maxPoints: REGION_LIMITS.points + 1 }).slice(0, -1).map((p) => [p[0], p[2]]); | |
| const bounded = points.length <= 128 ? points : points.filter((_, i) => Math.floor(i * 128 / points.length) !== Math.floor((i - 1) * 128 / points.length)); | |
| try { | |
| validatePolygon(smooth, surface); | |
| return smooth; | |
| } catch { | |
| validatePolygon(bounded, surface); | |
| return bounded; | |
| } | |
| } | |
| function regionFrame(scene, region) { | |
| if (region.surface === "floor") return { origin: [0, region.floorY, 0], u: [1, 0, 0], v: [0, 0, 1], normal: [0, 1, 0] }; | |
| if (region.surface === "object-surface") { | |
| const object = scene.objects.find((o) => o.id === regionObjectId(region)); | |
| if (!object) throw Error("The region object no longer exists."); | |
| return objectRegionFrame({ ...object, ...scene.regionPoses?.get(object.id) }, region.frame); | |
| } | |
| const door = scene.objects.find((o) => o.id === region.doorId); | |
| if (!door) throw Error("The region's door no longer exists"); | |
| const c = Math.cos(door.rotation), s = Math.sin(door.rotation), side = region.side; | |
| return { origin: [door.position[0] + s * side * Math.max(door.size[2] / 2 + 0.025, 0.12), door.position[1], door.position[2] + c * side * Math.max(door.size[2] / 2 + 0.025, 0.12)], u: [c * side, 0, -s * side], v: [0, 1, 0], normal: [s * side, 0, c * side] }; | |
| } | |
| const framePoint = (frame, p) => frame.project ? frame.project(p) : frame.origin.map((n, i) => n + frame.u[i] * p[0] + frame.v[i] * p[1]); | |
| const frameUV = (frame, p) => [frame.u, frame.v].map((axis) => axis.reduce((sum, n, i) => sum + n * (p[i] - frame.origin[i]), 0)); | |
| function validateRegions(regions, scene) { | |
| if (!Array.isArray(regions) || regions.length > REGION_LIMITS.regions) throw Error("Up to 32 interaction regions"); | |
| const ids = /* @__PURE__ */ new Set(), counts = /* @__PURE__ */ new Map(); | |
| for (const r of regions) { | |
| const owner = scene.objects.find((o) => o.id === regionObjectId(r)); | |
| if (!r || r.schema !== "vrbuild-region/1" || typeof r.id !== "string" || !/^[\w-]{1,80}$/.test(r.id) || ids.has(r.id) || !(isRegionObject(owner) || isDoor(owner)) || r.objectId && r.doorId && r.objectId !== r.doorId || !["door-frame", "object-surface", "floor"].includes(r.surface) || typeof r.name !== "string" || r.name.length > 80) throw Error("Invalid region ID or object target."); | |
| if (r.surface === "door-frame" && !isDoor(owner)) throw Error("Legacy door-frame regions require their original door."); | |
| validatePolygon(r.points, r.surface); | |
| if (r.surface === "object-surface") { | |
| validateSurfaceFrame(r.frame); | |
| if (!Array.isArray(r.points) || r.points.some((p) => !Array.isArray(p) || p.length !== 2 || !p.every(finite))) throw Error("Invalid surface region points."); | |
| for (let i = 0; i < r.points.length; i++) { | |
| const a = r.points[i], b = r.points[(i + 1) % r.points.length]; | |
| for (let j = 0; j <= 4; j++) if (!surfaceLocalPoint(owner, r.frame, a.map((n, k) => n + (b[k] - n) * j / 4))) throw Error("Region leaves the object surface. Redraw a smaller closed region."); | |
| } | |
| } | |
| if (r.surface === "door-frame" && ![-1, 1].includes(r.side) || r.surface === "floor" && !finite(r.floorY)) throw Error("Invalid region surface"); | |
| ids.add(r.id); | |
| counts.set(regionObjectId(r), (counts.get(regionObjectId(r)) || 0) + 1); | |
| if (counts.get(regionObjectId(r)) > REGION_LIMITS.perDoor) throw Error("Each object supports up to 8 regions."); | |
| const frame = regionFrame(scene, r); | |
| if (r.points.some((p) => framePoint(frame, p).some((n) => !finite(n)))) throw Error("Region is outside the scene"); | |
| if (r.surface === "door-frame") { | |
| const door = scene.objects.find((o) => o.id === r.doorId); | |
| if (r.points.some((p) => Math.abs(p[0]) > door.size[0] / 2 + 0.6 || p[1] < -door.size[1] / 2 - 0.1 || p[1] > door.size[1] / 2 + 0.3)) throw Error("Source region is outside the door frame. Draw it again."); | |
| } | |
| } | |
| return regions; | |
| } | |
| function replaceObjectRegions(scene, objectId, regions) { | |
| const owner = scene.objects.find((o) => o.id === objectId); | |
| if (!(isRegionObject(owner) || isDoor(owner)) || !Array.isArray(regions) || regions.some((r) => regionObjectId(r) !== objectId)) throw Error("The draft must belong to the selected object."); | |
| const next = structuredClone(scene), previous = (next.regions || []).filter((r) => regionObjectId(r) === objectId); | |
| next.regions = [...(next.regions || []).filter((r) => regionObjectId(r) !== objectId), ...structuredClone(regions)]; | |
| validateRegions(next.regions, next); | |
| if (JSON.stringify(previous) !== JSON.stringify(regions) && next.floods) next.floods = next.floods.filter((f) => regionObjectId(f) !== objectId); | |
| return next; | |
| } | |
| const replaceDoorRegions = replaceObjectRegions; | |
| function regionDescription(scene, r) { | |
| const frame = regionFrame(scene, r), worldPoints = r.points.map((p) => framePoint(frame, p)); | |
| return { id: r.id, name: r.name, surface: r.surface, objectId: regionObjectId(r), ...r.doorId ? { doorId: r.doorId } : {}, area: polygonArea(r.points), center: worldPoints.reduce((a, p) => a.map((v, i) => v + p[i] / worldPoints.length), [0, 0, 0]), normal: frame.normal, outline: worldPoints }; | |
| } | |
| export { | |
| REGION_LIMITS, | |
| closeRegionStroke, | |
| edgeDistance, | |
| framePoint, | |
| frameUV, | |
| isRegionObject2 as isRegionObject, | |
| isSourceRegion, | |
| polygonArea, | |
| polygonContains, | |
| regionDescription, | |
| regionFrame, | |
| regionObjectId, | |
| replaceDoorRegions, | |
| replaceObjectRegions, | |
| validatePolygon, | |
| validateRegions | |
| }; | |