import { FLY_AREA_CONFIG } from "./env"; /** * Axis-aligned bounding box in mm, world (three.js) XZ coordinates. * `topY` is optional and doubles this same shape as "floor height" data: * undefined means flies never stand on it (e.g. a wall), a number means a * walkable/landable surface at that height (e.g. a tabletop). */ export interface AabbMm { minX: number; maxX: number; minZ: number; maxZ: number; topY?: number; } export interface FurniturePlacement { id: string; /** path under public/, served by Vite at the same path. */ modelUrl: string; position: [number, number, number]; rotationY: number; /** uniform scale converting the source glTF's meter units to this app's mm world. */ scale: number; /** collision footprint(s) this placement contributes -- empty for purely decorative props. */ colliders: AabbMm[]; } // Room interior derives from FLY_AREA_CONFIG (not its own copy) so the two // can never drift apart -- FLY_AREA_CONFIG already drives camera/grid // framing math elsewhere, this just reuses it for wall placement. // wallHeightMm/wallThicknessMm/TILE_MM match the authored size of // public/models/room/wall.glb and floor.glb (Kenney "Furniture Kit", CC0): // each is a 1m x ~1.29m x 0.05m segment meant to be tiled edge-to-edge. export const ROOM_CONFIG = { widthMm: FLY_AREA_CONFIG.widthMm, depthMm: FLY_AREA_CONFIG.depthMm, wallHeightMm: 1289.5, wallThicknessMm: 50, }; export const TILE_MM = 1000; // CC0 tileable textures (Poly Haven), applied as diffuse maps onto the // floor/wall meshes in RoomScene.tsx -- see CREDITS.md. export const FLOOR_TEXTURE_URL = "/textures/floor_diff.jpg"; export const WALL_TEXTURE_URL = "/textures/wall_diff.jpg"; const HALF_WIDTH = ROOM_CONFIG.widthMm / 2; const HALF_DEPTH = ROOM_CONFIG.depthMm / 2; // Four thin boxes ringing the interior, computed from ROOM_CONFIG rather // than hand-authored, since wall placement is mechanical. export const WALL_COLLIDERS: AabbMm[] = [ { minX: -HALF_WIDTH, maxX: HALF_WIDTH, minZ: -HALF_DEPTH, maxZ: -HALF_DEPTH + ROOM_CONFIG.wallThicknessMm }, // south { minX: -HALF_WIDTH, maxX: HALF_WIDTH, minZ: HALF_DEPTH - ROOM_CONFIG.wallThicknessMm, maxZ: HALF_DEPTH }, // north { minX: -HALF_WIDTH, maxX: -HALF_WIDTH + ROOM_CONFIG.wallThicknessMm, minZ: -HALF_DEPTH, maxZ: HALF_DEPTH }, // west { minX: HALF_WIDTH - ROOM_CONFIG.wallThicknessMm, maxX: HALF_WIDTH, minZ: -HALF_DEPTH, maxZ: HALF_DEPTH }, // east ]; // Furniture layout -- positions/colliders measured directly from each // downloaded glTF's accessor bounding box (see tools/inspect done during // execution), not guessed: every piece's local origin sits at a known // corner or center, and `position` is that corner/center's world-space // placement minus the piece's own local min/center, so these numbers // already account for each model's authored origin offset. // // Sources (all CC0, see CREDITS.md): // - table/chair/sofa/plant/rug/floor/wall: Kenney "Furniture Kit" // - bowl/apple/lemon: Poly Haven "Food & Kitchen" collection export const FURNITURE: FurniturePlacement[] = [ // tableCross.glb: local bbox X[0,852.1] Z[-447.4,0] Y[0,347.4] (mm) -- // verified with `gltf-transform inspect` (scene-graph-aware; an earlier // raw-accessor read of this specific file ignored a node-level scale and // was ~2.4x too large, which is why the fruit/chairs looked stranded off // the table before). Placed against the north wall. { id: "table", modelUrl: "/models/room/table.glb", position: [-426.06, 0, 1400], rotationY: 0, scale: 1000, colliders: [{ minX: -426.06, maxX: 426.06, minZ: 952.63, maxZ: 1400, topY: 347.41 }], }, // chair.glb: local bbox X[0,200] Z[-200,0] Y[0,470] (mm), pulled up snug // against the table's near edge (952.63mm, ~22.6mm clearance). topY makes // the seat/back landable, same as the table (see resolvePosition's // altitude-aware obstacle check in collision.ts). { id: "chair-1", modelUrl: "/models/room/chair.glb", position: [-350, 0, 930], rotationY: 0, scale: 1000, colliders: [{ minX: -350, maxX: -150, minZ: 730, maxZ: 930, topY: 470 }], }, { id: "chair-2", modelUrl: "/models/room/chair.glb", position: [150, 0, 930], rotationY: 0, scale: 1000, colliders: [{ minX: 150, maxX: 350, minZ: 730, maxZ: 930, topY: 470 }], }, // loungeSofa.glb: local bbox X[0,980] Z[-410,0] Y[0,460] (mm), placed // along the west wall. Landable (topY), same reasoning as the chairs. { id: "sofa", modelUrl: "/models/room/sofa.glb", position: [-1900, 0, -190], rotationY: 0, scale: 1000, colliders: [{ minX: -1900, maxX: -920, minZ: -600, maxZ: -190, topY: 460 }], }, // pottedPlant.glb: local bbox X[-106.03,106.03] Z[-120.73,120.73] // Y[0,654.02] (mm), origin centered in X/Z (verified with `gltf-transform // inspect`) -- tucked in the corner opposite the sofa. Deliberately no // topY: a potted plant's foliage isn't a landable surface, so it stays a // full-height obstacle at every altitude, unlike the table/chairs/sofa/shelf. { id: "plant", modelUrl: "/models/room/plant.glb", position: [1800, 0, -1300], rotationY: 0, scale: 1000, colliders: [{ minX: 1693.97, maxX: 1906.03, minZ: -1420.73, maxZ: -1179.27 }], }, // rugRectangle.glb: purely decorative (no collider) -- laid under the // free-floor area so the flies have somewhere visually distinct to walk. { id: "rug", modelUrl: "/models/room/rug.glb", position: [-160, 0.5, -265], rotationY: 0, scale: 1000, colliders: [], }, // Fruit bowl + fruit, sitting on the table's topY (347.41mm, centered // over its Z[952.63,1400] footprint) -- colliders stay [] (a grounded // fly's XZ collision already keeps it out of the table's whole footprint // via ALL_OBSTACLES, so nothing here needs to independently block // horizontal movement), but each still gets its own small landable // footprint in FRUIT_FLOOR_SURFACES below so a fly can land specifically // on top of a piece of fruit, not just anywhere on the table. bowl/apple // have bottom-centered local origins (position.y == topY places them // flush); lemon's origin sits mid-body, so its position.y adds back its // local min-Y offset (52.29mm) to rest flush. { id: "bowl", modelUrl: "/models/room/bowl.glb", position: [0, 347.41, 1176.3], rotationY: 0, scale: 1000, colliders: [], }, { id: "apple", modelUrl: "/models/room/apple.glb", position: [-120, 347.41, 1176.3], rotationY: 0, scale: 1000, colliders: [], }, { id: "lemon", modelUrl: "/models/room/lemon.glb", position: [100, 399.7, 1186.3], rotationY: 0, scale: 1000, colliders: [], }, // bookcaseOpen.glb: local bbox X[0,400] Z[-250,0] Y[0,880] (mm), rotated // -90deg so its depth (250mm) faces the east wall and stands flush // against it, in the room's north half (clear of the plant/sofa corners). // topY makes its top landable (reachable around altitude level 8+). { id: "shelf", modelUrl: "/models/room/shelf.glb", position: [1700, 0, 700], rotationY: -Math.PI / 2, scale: 1000, colliders: [{ minX: 1700, maxX: 1950, minZ: 700, maxZ: 1100, topY: 880 }], }, ]; /** Everything collision.ts checks fly positions against (horizontal blocking). */ export const ALL_OBSTACLES: AabbMm[] = [...WALL_COLLIDERS, ...FURNITURE.flatMap((f) => f.colliders)]; /** * Raised walkable surfaces for floorHeightAt's height lookup only -- NOT * horizontal obstacles (a fly can freely walk onto these footprints, it * just stands slightly higher while over them). Includes every * topY-bearing FURNITURE collider (e.g. the table) plus the rug, which * isn't a FURNITURE collider at all (flies must be able to walk across it, * unlike the table) but still has real thickness -- rugRectangle.glb: local * bbox X[0,1570] Z[-920,0] Y[0,10] (mm), at its placed position or a * grounded fly would render with its body clipped partway into the rug * mesh instead of standing on top of it. */ // Each fruit prop's own small landable surface, ON TOP of the table // (topY = table's 347.41 + this prop's own authored height) -- kept // separate from FURNITURE's colliders (which stay [] so these never // independently block horizontal movement, same reasoning as the rug // below). bowl: 130.7x73.1x124.5mm @ (0,347.41,1176.3), bottom-centered. // apple: 97.6x85.4x95.9mm @ (-120,347.41,1176.3), bottom-centered. lemon: // 68.7x95.4x68.3mm @ (100,399.7,1186.3) -- its topY uses its full authored // height (95.4mm) added to the table's topY, not position.y (399.7mm), // since position.y already has its own local-origin offset baked in just // to sit the mesh flush on the table. export const FRUIT_FLOOR_SURFACES: AabbMm[] = [ { minX: -65.35, maxX: 65.35, minZ: 1114.05, maxZ: 1238.55, topY: 420.51 }, // bowl { minX: -168.8, maxX: -71.2, minZ: 1128.35, maxZ: 1224.25, topY: 432.81 }, // apple { minX: 65.65, maxX: 134.35, minZ: 1152.15, maxZ: 1220.45, topY: 442.81 }, // lemon ]; export const FLOOR_SURFACES: AabbMm[] = [ ...ALL_OBSTACLES.filter((box) => box.topY !== undefined), { minX: -160, maxX: 1410, minZ: -1185, maxZ: -265, topY: 10.5 }, // rug ...FRUIT_FLOOR_SURFACES, ]; /** * Free-floor spawn rectangle(s) -- hand-picked to clear every obstacle * above (with margin), not derived by automatic rectangle subtraction * (deliberately out of scope: a human picking 1-2 clear rectangles once is * simpler and just as correct here). */ export const FREE_FLOOR_RECTS: AabbMm[] = [{ minX: -350, maxX: 1600, minZ: -1450, maxZ: 0 }];