flyyt / src /roomLayout.ts
gpue's picture
Deploy 771c7d3cfb0b08d632f7fe3d02e59b7bcdff47c4
5a3b695 verified
Raw History Blame Contribute Delete
9.7 kB
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 }];