File size: 4,419 Bytes
970fc3e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | import {languagePattern} from "./language.mjs";
import { validateScene } from "./scene.mjs";
import { validateRoomMetrics } from "./room-spatial.mjs";
function validateCapturePlan(plan) {
if (!plan || plan.closeUnscanned !== true) throw new Error("Invalid capture bounds");
return { ...plan, metrics: validateRoomMetrics(plan.metrics) };
}
function roomDraft(title, metrics) {
metrics = validateRoomMetrics(metrics);
return validateScene({ title, description: "New scene awaiting photo-based construction. The floor stores scan dimensions only; no photo reconstruction or actors have been added.", room: metrics, actors: [], objects: [{
id: "ground",
name: "Scan boundary placeholder floor",
group: "floor",
category: "structure",
shape: "box",
position: [0, -0.06, 0],
size: [metrics.width, 0.12, metrics.depth],
rotation: 0,
color: "#ccd2dc",
roughness: 0.95,
metalness: 0
}] });
}
function closeCaptureBoundary(scene) {
validateScene(scene);
const floor = scene.objects.find((o) => o.id === "ground"), ceiling = scene.objects.find((o) => o.id === "ceiling");
if (!floor || floor.shape !== "box" || !ceiling) throw new Error("Enclosed capture bounds require a floor and ceiling");
const floorY = floor.position[1] + floor.size[1] / 2, height = ceiling.position[1] - ceiling.size[1] / 2 - floorY;
if (height < 1.8 || height > 8) throw new Error("Invalid enclosure wall height");
const c = Math.cos(floor.rotation), s = Math.sin(floor.rotation), walls = scene.objects.filter((o) => o.shape === "box" && languagePattern("scene.boundaryWall").test(`${o.group} ${o.name}`));
const local = ([x, z]) => {
x -= floor.position[0];
z -= floor.position[2];
return [c * x - s * z, s * x + c * z];
};
const world = ([x, z]) => [floor.position[0] + c * x + s * z, floorY + height / 2, floor.position[2] - s * x + c * z];
const coverage = walls.filter((o) => Math.abs(Math.sin(2 * (o.rotation - floor.rotation))) < 0.02 && o.position[1] - o.size[1] / 2 <= floorY + 0.15 && o.position[1] + o.size[1] / 2 >= floorY + height - 0.15).map((o) => {
const a = Math.cos(o.rotation), b = Math.sin(o.rotation), p = [];
for (const x of [-o.size[0] / 2, o.size[0] / 2]) for (const z of [-o.size[2] / 2, o.size[2] / 2]) p.push(local([o.position[0] + a * x + b * z, o.position[2] - b * x + a * z]));
return { min: [Math.min(...p.map((v) => v[0])), Math.min(...p.map((v) => v[1]))], max: [Math.max(...p.map((v) => v[0])), Math.max(...p.map((v) => v[1]))] };
});
const next = structuredClone(scene), ids = new Set(scene.objects.map((o) => o.id)), thickness = 0.15;
const edges = [{ axis: 0, at: floor.size[2] / 2, sign: 1 }, { axis: 0, at: -floor.size[2] / 2, sign: -1 }, { axis: 1, at: floor.size[0] / 2, sign: 1 }, { axis: 1, at: -floor.size[0] / 2, sign: -1 }];
for (const [edgeIndex, edge] of edges.entries()) {
const axis = edge.axis, normal = 1 - axis, half = floor.size[axis === 0 ? 0 : 2] / 2;
const spans = coverage.filter((p) => p.min[normal] <= edge.at + 0.08 && p.max[normal] >= edge.at - 0.08).map((p) => [Math.max(-half, p.min[axis]), Math.min(half, p.max[axis])]).filter(([a, b]) => b > a).sort((a, b) => a[0] - b[0]);
let cursor = -half;
const gaps = [];
for (const [a, b] of spans) {
if (a - cursor > 0.04) gaps.push([cursor, a]);
cursor = Math.max(cursor, b);
}
if (half - cursor > 0.04) gaps.push([cursor, half]);
for (const [index, [a, b]] of gaps.entries()) {
let id = `capture-cap-${edgeIndex + 1}-${index + 1}`;
while (ids.has(id)) id += "x";
ids.add(id);
const p = [];
p[axis] = (a + b) / 2;
p[normal] = edge.at + edge.sign * thickness / 2;
next.objects.push({ id, name: `Boundary wall for unscanned area · ${edgeIndex + 1}-${index + 1}`, group: "capture-boundary", category: "structure", shape: "box", position: world(p), size: axis === 0 ? [b - a, height, thickness] : [thickness, height, b - a], rotation: floor.rotation, color: "#b3c3d3", roughness: 0.95, metalness: 0 });
}
}
if (next.objects.length > scene.objects.length) next.description = `${scene.description.slice(0, 1750)} Unscanned areas are enclosed by editable virtual walls at the selected floor boundary. These added walls are inferred, not measured.`;
return validateScene(next);
}
export {
closeCaptureBoundary,
roomDraft,
validateCapturePlan
};
|