Instructions to use XXXpilar/Fastfill-test with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use XXXpilar/Fastfill-test with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 48,155 Bytes
e6f80f3 | 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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 | """WorldEdge request (scope doc, Appendix B) <-> FastFill model text.
room, ctx = request_to_room(request)
prompt = prompt_text(tok, messages(room, room["constraints"], with_target=False))
... one generation call ...
result = placements_from_text(text, room, ctx)
Model side (scene.py): room frame with the boundary AABB min corner at (0, 0), object local +X = semantic front,
integer-degree yaw inside the text. Interface side: the request's own frame (boundary_xy, floor_z), position_m of
the box bottom center, yaw_rad about +Z of the object's own local frame, support_parent = floor_id or the id of the
object it stands on, support_surface = "top". Objects anchored to walls or ceilings ("anchor": "wall" | "ceiling")
and fronts that are not along a horizontal local axis are returned as unsupported instead of being sent to the model,
and so is any constraint that names one of them.
Floor/object anchors are enforced after generation and reported in validation.constraints as ["anchor", id, kind],
with hard=true, trained=false, model_conditioned=false: the existing model protocol cannot express them.
room.fixed_geometry (optional): boxes already in the room that nothing may be placed on or through (doors, windows,
columns, stairs, furniture that stays), each {"id", "category"?, "size_xyz_m", "position_m" (bottom center),
"yaw_rad"?, "fixed_kind"?}; shown to the model as `fixed`, never placed, and overlaps with them are reported in validation
(fixed_collisions), and any such overlap with a non-window box fails the layout (fixed_blocking, serve.py 422). Normalized as build.prep shaped training: |z| <= FLOOR_Z is snapped to
0; a box starting at or above FIXED_MAX_Z, covering half the room or more, or standing clear of the walls is not shown
and is reported in unsupported as "fixed_ignored". The wall band is 0.30 m for fixed_kind="structure", otherwise
0.10 m; if fixed_kind is omitted, anchors.is_structure(category) selects the band. An explicit kind takes precedence.
A constraint may not name a fixed box (v1 relates objects to place).
layout_constraints (Appendix B names). Relational constraints are shown to the model as must-hold; after repair a failed
"hard" one (the default; "hard": false marks a soft one, which is only reported) fails the layout:
{"type": "faces", "subject", "target"} {"type": "between", "subject", "anchors": [a, b]}
{"type": "supported_by", "subject", "parent", "surface": "top"} {"type": "against_wall", "subject"}
{"type": "near", "subject", "target", "max_gap_m"} footprint gap, NOT centre distance (trained on 0.1-0.5)
{"type": "keepout", "polygon_xy": [[x, y], ...]} hard: cut out of the floor, padded by KEEPOUT_PAD
Soft keepouts leave the floor unchanged. They are reported as ["keepout", polygon_xy] in request coordinates,
with hard=false, trained=false, model_conditioned=false; the existing model protocol has no advisory region field.
A soft keepout holds when no placed footprint overlaps it by more than 1e-9 square meters (touching is allowed).
against_wall is accepted against the real walls, not keepout cuts, although the model sees a cut as wall.
A padded keepout that stops less than WALL_SNAP short of a wall (or of another keepout) is snapped to it: the strip
left between them is dropped from the floor the model sees (cm rounding folded one under 5 mm into a self-intersection,
a bad_boundary 422, for 35 of 20,000 EmbodiedGen-shaped requests); floor cut off behind such a gap is a pocket, and a
gap that alone would cut off POCKET_M2 or more is left open. A cut that still folds under cm rounding (a gap under 5 mm
to a large corner, a keepout crossing a wall by under 5 mm) is keepout_not_representable, not bad_boundary.
Forms training never contains (build.extract_constraints) are still shown and checked as asked, but reported with
"trained": false in validation: against_wall in a hull room, faces whose subject's category head is not in
build.FRONT, against_wall / near / faces / between naming an object that stands on another, between with anchors of
two categories, an argument (of supported_by: the subject) that is a FLAT covering or under 5 cm tall.
Rejected with ValueError (HTTP 422 in serve.py): malformed or non-finite numbers, a coordinate_system other than
m / right / Z / bbox_bottom_center / rad, duplicate ids, unknown anchors, non-positive sizes or height, constraints
naming unknown objects, fixed boxes or themselves, 'on' cycles or two parents, a keepout that splits the room or leaves an
island (enclosed pockets under POCKET_M2 are treated as unusable floor).
"""
import copy
import math
from shapely.affinity import translate
from shapely.errors import ShapelyError
from shapely.geometry import Point, Polygon
from shapely.ops import nearest_points, unary_union
from fastfill.anchors import FIXED_MAX_Z, FLAT, FLOOR_Z, is_structure
from fastfill.build import FRONT, THICK_WALL_M, WALL_BAND_M
FLAT_Z = 0.15 # = build.FLAT_Z: a sunk fixed box whose top stays under this is not shown (checked in __main__)
from fastfill.scene import apply, canonical, clean_boundary, footprint, head, norm_cat, num, parse
from fastfill.validate import check, holds, support_contains
HULL_SOURCES = {"convex_hull", "hull", "scan_hull", "floor_hull"}
KEEPOUT_PAD = 0.08 # < validate.check's oob tolerance (0.10) minus cm rounding: repair onto the keepout edge stays valid
POCKET_M2 = 0.5 # free floor enclosed by keepouts and walls smaller than this is dropped, not a reason to refuse
WALL_SNAP = 0.01 # a strip of floor narrower than this left by a padded keepout (at a wall or keepout) is dropped
ANCHORS = {None, "floor", "object", "wall", "ceiling"}
COORDS = {"units": "m", "handedness": "right", "up_axis": "Z", "position_reference": "bbox_bottom_center",
"yaw_units": "rad"}
SNAP_TOL = 0.10 # training layouts cross walls by up to this much (40% of test rooms by > 1 mm); EmbodiedGen allows 1 mm
SNAP_GRID = sorted(((dx / 100, dy / 100) for dx in range(-10, 11) for dy in range(-10, 11) if dx * dx + dy * dy <= 100),
key=lambda d: d[0] ** 2 + d[1] ** 2)
def request_to_room(req):
"""-> (canonical model room with constraints, ctx for mapping the answer back). Raises ValueError on bad input."""
try:
return _request_to_room(req)
except (KeyError, TypeError, IndexError, AttributeError, OverflowError, ShapelyError) as e:
raise ValueError(f"bad_request: {e!r}") from e
def _finite(v, lo=None, above=None):
if not (isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v) and abs(v) < 1e4
and (lo is None or v >= lo) and (above is None or v > above)):
raise ValueError(f"bad_number: {v!r}")
return float(v)
def _text(v, optional=True):
if not (isinstance(v, str) or (optional and v is None)):
raise ValueError(f"bad_text: {v!r}")
return v
def _request_to_room(req):
rm = req["room"]
for k, v in (req.get("coordinate_system") or {}).items():
if k in COORDS and v != COORDS[k]:
raise ValueError(f"unsupported coordinate_system {k}={v!r} (FastFill speaks {COORDS})")
cons_in = req.get("layout_constraints") or []
pts = rm["boundary_xy"]
keepout_constraints = [c for c in cons_in if c["type"] == "keepout"]
for c in keepout_constraints:
if not isinstance(c.get("hard", True), bool):
raise ValueError(f"malformed constraint: {c}")
for p in [q for poly in [pts, *(c["polygon_xy"] for c in keepout_constraints)] for q in poly]:
_finite(p[0]), _finite(p[1])
for c in keepout_constraints:
poly = Polygon(c["polygon_xy"])
if not poly.is_valid or poly.is_empty or poly.area <= 0:
raise ValueError("bad_keepout")
keepouts = [c["polygon_xy"] for c in keepout_constraints if c.get("hard", True)]
if rm.get("height_m") is not None:
_finite(rm["height_m"], above=0)
_text(rm.get("room_type"))
walls = Polygon(pts) # against_wall is checked against these, not against keepout cuts
floor = walls # what answers are snapped into: the walls minus the keepouts themselves
if keepouts: # cut out, padded, from what the model sees: it treats a keepout as wall
if not floor.is_valid:
raise ValueError("bad_boundary")
cut = unary_union([Polygon(k) for k in keepouts])
seen = floor.difference(cut.buffer(KEEPOUT_PAD, join_style="mitre"))
# opening by WALL_SNAP / 2: strips under WALL_SNAP wide vanish, everything wider comes back edge for edge; the
# intersection clips the mitre corners that re-grow past a slanted wall where a strip vanished. A gap whose
# closing alone would cut off POCKET_M2 or more stays open (9 of 20,000 EmbodiedGen-shaped requests: two
# padded keepouts 5-10 mm apart in front of a corner): the corner behind it stays floor, as without the snap
snap = seen.intersection(seen.buffer(-WALL_SNAP / 2, join_style="mitre").buffer(WALL_SNAP / 2, join_style="mitre"))
big = lambda g: sum(q.area >= POCKET_M2 for q in getattr(g, "geoms", [g]))
seen = seen if big(snap) > 1 and big(seen) == 1 else snap
parts = sorted(getattr(seen, "geoms", [seen]), key=lambda g: -g.area)
pockets = [g for g in parts[1:] if g.area < POCKET_M2]
if seen.is_empty or len(parts) - len(pockets) != 1 or parts[0].interiors:
raise ValueError("keepout_not_representable") # fills or splits the room, or leaves an island
seen = parts[0]
floor = floor.difference(unary_union([cut, *pockets]))
pts = seen.exterior.coords[:-1]
x0, y0 = (min(p[i] for p in pts) for i in (0, 1))
boundary = clean_boundary([[x - x0, y - y0] for x, y in pts])
if boundary is None: # walls that round cleanly: the cut folds under cm rounding (a sub-5 mm gap or wall mouth)
wx, wy = walls.bounds[:2]
ok = keepouts and clean_boundary([[x - wx, y - wy] for x, y in walls.exterior.coords[:-1]])
raise ValueError("keepout_not_representable" if ok else "bad_boundary")
ctx = {"x0": x0, "y0": y0, "floor_z": _finite(rm.get("floor_z", 0.0)),
"floor_id": _text(rm.get("floor_id", "floor"), optional=False),
"floor": translate(floor, -x0, -y0), "walls": [[x - x0, y - y0] for x, y in walls.exterior.coords[:-1]],
"soft_keepouts": [(copy.deepcopy(c["polygon_xy"]), translate(Polygon(c["polygon_xy"]), -x0, -y0))
for c in keepout_constraints if not c.get("hard", True)],
"front_k": {}, "order": [], "unsupported": []}
fixed, fixed_ids = [], set()
near, wall = walls.buffer(WALL_BAND_M), walls.buffer(THICK_WALL_M)
for f in rm.get("fixed_geometry") or []:
_text(f["id"], optional=False), _text(f.get("category"))
kind = _text(f.get("fixed_kind"))
structural = kind == "structure" if kind is not None else is_structure(f.get("category"))
sx, sy, sz = (_finite(v, above=0) for v in f["size_xyz_m"])
x, y, z = (_finite(v) for v in f["position_m"])
yaw = _finite(f.get("yaw_rad", 0.0)) % (2 * math.pi)
fixed_ids.add(f["id"])
z -= ctx["floor_z"]
fp = Polygon(footprint({"pos": [x, y], "size": [sx, sy], "yaw": yaw}))
# as build.prep / anchors.fixed_geometry shaped training: nothing starting at FIXED_MAX_Z or above, no slab
# covering half the room, nothing clear of the walls; a floor-level bottom is z=0
if z + sz < FLAT_Z: # a covering or trim (or wholly under the floor): not shown, as in build._keep_fixed
ctx["unsupported"].append({"id": f["id"], "reason": "fixed_ignored"})
continue
if z < -FLOOR_Z: # sunk below the floor: cut at the floor
sz, z = z + sz, 0.0
if z >= FIXED_MAX_Z or fp.area >= 0.5 * walls.area or not fp.intersects(wall if structural else near):
ctx["unsupported"].append({"id": f["id"], "reason": "fixed_ignored"})
continue
fixed.append({"id": f["id"], "ext_id": f["id"], "category": f.get("category") or "structure",
"size": [sx, sy, sz], "pos": [x - x0, y - y0, 0.0 if abs(z) <= FLOOR_Z else z], "yaw": yaw})
ids = [o["id"] for o in req["objects_to_place"]] + [f["id"] for f in rm.get("fixed_geometry") or []]
if len(set(ids)) < len(ids) or ctx["floor_id"] in ids:
raise ValueError("duplicate_object_id")
objs = []
for o in req["objects_to_place"]:
_text(o["id"], optional=False), _text(o["category"], optional=False), _text(o.get("desc"))
sx, sy, sz = (_finite(v, above=0) for v in o["size_xyz_m"])
if o.get("anchor") not in ANCHORS:
raise ValueError(f"unknown anchor: {o.get('anchor')!r}")
fx, fy, fz = (_finite(v) for v in (o.get("semantic_front_local") or [1, 0, 0]))
phi, h = math.atan2(fy, fx), math.hypot(fx, fy)
k = round(phi / (math.pi / 2)) % 4
if o.get("anchor") in ("wall", "ceiling"):
ctx["unsupported"].append({"id": o["id"], "reason": "unsupported_anchor"})
continue
if h < 1e-9 or abs(fz) > 1e-3 * h or abs(math.remainder(phi - k * math.pi / 2, 2 * math.pi)) > 1e-3:
ctx["unsupported"].append({"id": o["id"], "reason": "front_not_axis_aligned"})
continue
if k % 2: # front along local Y: the model's local X is that axis
sx, sy = sy, sx
ctx["front_k"][o["id"]] = k
ctx["order"].append(o["id"])
objs.append({"id": o["id"], "ext_id": o["id"], "category": o["category"], "size": [sx, sy, sz],
"desc": o.get("desc"), "anchor": o.get("anchor")})
room = canonical({"room_type": rm.get("room_type"), "boundary": boundary, "height": rm.get("height_m"),
"boundary_type": "hull" if str(rm.get("boundary_source")).lower() in HULL_SOURCES else "polygon",
"fixed": fixed, "objects": objs})
m = {o["ext_id"]: o["id"] for o in room["objects"]}
skipped = {u["id"] for u in ctx["unsupported"]}
cons, ctx["checks"] = [], [] # what the model is shown / what the answer is accepted against
for c in cons_in:
t = c["type"]
if t == "keepout":
continue
s = c["subject"]
anchors = c.get("anchors", [])
if not isinstance(anchors, list) or not isinstance(c.get("hard", True), bool):
raise ValueError(f"malformed constraint: {c}")
refs = [s] + [c[k] for k in ("target", "parent") if k in c] + anchors
if any(r in fixed_ids for r in refs):
raise ValueError(f"constraint names a fixed box (v1 constraints only relate objects to place): {c}")
if any(r not in m and r not in skipped for r in refs):
raise ValueError(f"constraint on an unknown object: {c}")
if len(set(refs)) < len(refs):
raise ValueError(f"constraint names the same object twice: {c}")
if any(r in skipped for r in refs): # the rest of the room is still placed
ctx["unsupported"].append({"constraint": c, "reason": "references_unsupported_object"})
continue
if t == "faces":
k = ["faces", m[s], m[c["target"]]]
elif t == "between" and len(c["anchors"]) == 2:
k = ["between", m[s]] + [m[a] for a in c["anchors"]]
elif t == "supported_by" and c.get("surface", "top") == "top":
k = ["on", m[s], m[c["parent"]]]
elif t == "near":
k = ["near", m[s], m[c["target"]], _finite(c["max_gap_m"], lo=0)]
elif t == "against_wall":
k = ["against_wall", m[s]]
else:
raise ValueError(f"unsupported constraint: {c}")
# the model sees cm (near rounded DOWN, so meeting what it sees meets the request); acceptance uses the
# request's own number; hard=false constraints are reported but do not fail the layout
cons.append(k[:3] + [num(math.floor(k[3] * 100 + 1e-9) / 100)] if t == "near" else k)
ctx["checks"].append((k, c.get("hard", True)))
parent = {}
for k in cons:
if k[0] == "on":
if parent.setdefault(k[1], k[2]) != k[2]:
raise ValueError(f"{k[1]} is asked to stand on two objects")
for x in parent: # an 'on' cycle can never hold
seen, cur = {x}, parent[x]
while cur in parent:
if cur in seen:
raise ValueError(f"'on' cycle through {cur}")
seen.add(cur)
cur = parent[cur]
room["constraints"] = cons
return room, ctx
def snap_inside(pl, room, floor, cons=(), walls=None):
"""Move each object that crosses `floor` (model frame) by at most SNAP_TOL back inside, with what stands on it.
Lower objects first, so a child is judged after its parent moved; a child is only moved as far as its centre
stays over its parent (validate.check's support rule), else it is left for validation to reject. A move is never
taken if it would deepen the overlap of the object OR of anything standing on it (the whole support subtree
moves with it) with a fixed box of the room (room["fixed"], at that object's height).
The hard constraints `cons` (validate.holds, against_wall against `walls`) that held before the repair hold after
it: if the plain pass breaks one, the repair is redone refusing every move that breaks one of them. The plain pass
goes first because a constraint may fail only between two moves (three chairs pushed off one wall in turn: the
between through the middle one fails until it moves too). Without this, pushing chairs off walls broke 7 GT-true
constraints in 8,254 test_constrained rooms (faces 4, against_wall 2, near 1)."""
size = {o["id"]: o["size"] for o in room["objects"]}
lay = lambda q: {j: {"pos": p["pos"], "yaw": p["yaw"], "size": size[j], "parent": p["on"]} for j, p in q.items()}
held = [c for c in cons if holds(c, None, walls, lay(pl))]
raw = copy.deepcopy(pl)
_push(pl, room, floor, size, [], walls)
if not all(holds(c, None, walls, lay(pl)) for c in held):
pl.update(raw)
_push(pl, room, floor, size, held, walls)
def _push(pl, room, floor, size, held, walls):
inside = floor.buffer(1e-3) # validation()'s tolerance
fixed = room.get("fixed") or []
kids = {}
for i, p in pl.items():
if p["on"]:
kids.setdefault(p["on"], []).append(i)
for i in sorted(pl, key=lambda i: pl[i]["pos"][2]):
p = pl[i]
box = lambda dx, dy: Polygon(footprint({"pos": [p["pos"][0] + dx, p["pos"][1] + dy], "size": size[i], "yaw": p["yaw"]}))
if inside.covers(box(0, 0)):
continue
q = pl.get(p["on"])
seat = {**q, "size": size[p["on"]]} if q else None
push = [0.0, 0.0] # per axis, the largest move any outside corner needs to reach the boundary
for c in box(0, 0).exterior.coords[:-1]:
if not floor.covers(Point(c)):
q = nearest_points(floor.boundary, Point(c))[0]
for k, d in enumerate((q.x - c[0], q.y - c[1])):
if abs(d) > abs(push[k]):
push[k] = d
# the object and everything standing on it move together (parse() rejects support cycles); fixed boxes at
# each one's height (validate.check's rule): no move may deepen any of their overlaps
tree, todo = [], [i]
while todo:
j = todo.pop()
tree.append(j)
todo += kids.get(j, [])
at = lambda j, dx, dy: Polygon(footprint({"pos": [pl[j]["pos"][0] + dx, pl[j]["pos"][1] + dy],
"size": size[j], "yaw": pl[j]["yaw"]}))
blocks = {j: unary_union([Polygon(footprint(f)) for f in fixed if min(
pl[j]["pos"][2] + size[j][2], f["pos"][2] + f["size"][2]) - max(pl[j]["pos"][2], f["pos"][2]) > 0.05])
for j in tree}
worst = {j: at(j, 0, 0).intersection(blocks[j]).area + 1e-6 for j in tree}
mine = [c for c in held if set(tree) & set(c[1:])]
def keeps(dx, dy): # every held constraint on the subtree still holds after the move
ix = {j: {"pos": [q["pos"][0] + dx, q["pos"][1] + dy, q["pos"][2]] if j in tree else q["pos"], "yaw": q["yaw"],
"size": size[j], "parent": q["on"]} for j, q in pl.items()} if mine else {}
return all(holds(c, None, walls, ix) for c in mine)
# the nearest-boundary push, its axis parts, then any move within SNAP_TOL (1 cm grid, shortest first)
# (a nearest-boundary push over SNAP_TOL is dropped, the grid still tries every move within SNAP_TOL: a box
# stuck at a re-entrant corner may need a long diagonal push but a short move along one axis)
for dx, dy in [c for c in (tuple(push), (push[0], 0), (0, push[1])) if math.hypot(*c) <= SNAP_TOL] + SNAP_GRID:
if inside.covers(box(dx, dy)) and all(at(j, dx, dy).intersection(blocks[j]).area <= worst[j] for j in tree) \
and (seat is None or support_contains(seat, [p["pos"][0] + dx, p["pos"][1] + dy])) and keeps(dx, dy):
for j in tree:
pl[j]["pos"] = [pl[j]["pos"][0] + dx, pl[j]["pos"][1] + dy, pl[j]["pos"][2]]
break
def placements_from_text(text, room, ctx):
"""Model answer -> {"placements": after repair, "raw_placements": as the model proposed, "validation":
{"raw": ..., "repaired": ...}, "error": code | None, "unsupported": [...]}, placements in request order.
Repair = floor objects crossing a wall or a keepout by at most SNAP_TOL moved back inside (snap_inside); it is
deterministic post-processing, so the raw proposal and its validation are always returned next to it."""
ids = [o["id"] for o in room["objects"]]
pl, err = parse(text, ids)
if err is not None:
return {"placements": [], "raw_placements": [], "validation": None, "error": err,
"unsupported": ctx["unsupported"]}
raw = copy.deepcopy(pl)
snap_inside(pl, room, ctx["floor"], [k for k, hard in ctx["checks"] if hard], ctx["walls"])
return {"placements": _to_request(pl, room, ctx), "raw_placements": _to_request(raw, room, ctx),
"validation": {"raw": validation(raw, room, ctx), "repaired": validation(pl, room, ctx)},
"error": None, "unsupported": ctx["unsupported"]}
def _to_request(pl, room, ctx):
model2ext = {o["id"]: o["ext_id"] for o in room["objects"]}
by_ext = {}
for mid, p in pl.items():
ext = model2ext[mid]
yaw = (p["yaw"] - ctx["front_k"][ext] * math.pi / 2) % (2 * math.pi) # model +X (front) -> asset frame
item = {"id": ext, "position_m": [p["pos"][0] + ctx["x0"], p["pos"][1] + ctx["y0"], p["pos"][2] + ctx["floor_z"]],
"yaw_rad": yaw, "support_parent": model2ext[p["on"]] if p["on"] else ctx["floor_id"]}
if p["on"]:
item["support_surface"] = "top"
by_ext[ext] = item
return [by_ext[i] for i in ctx["order"]]
def trained(k, lay):
"""False for a constraint form build.extract_constraints never makes, so the model never saw it (v2.1 train: 0
against_wall in 5,117 constrained hull rooms; no faces with a non-FRONT subject; no against_wall / near / faces /
between with an argument on another object; no between with anchors of two categories; no argument (of 'on': the
subject) that is FLAT or under 5 cm tall). Reported, not refused: the caller decides. Thresholds (near 0.1-0.5) and
whether the relation held on a reference layout are not forms and are not checked here."""
o = {x["id"]: x for x in lay["objects"]}
args = [o[i] for i in k[1:2 if k[0] == "on" else None] if i in o]
return not ((k[0] == "against_wall" and lay["boundary_type"] != "polygon")
or (k[0] == "faces" and head(o[k[1]]) not in FRONT)
or (k[0] != "on" and any(x.get("parent") for x in args))
or any(head(x) in FLAT or x["size"][2] < 0.05 for x in args)
or (k[0] == "between" and norm_cat(o[k[2]]["category"]) != norm_cat(o[k[3]]["category"])))
def validation(pl, room, ctx):
"""The canonical checks (validate.check / validate.holds) on a placement, with request ids. ok = valid (inside
boundary + 0.1, supported, under the ceiling), every object (on others too) within 1 mm of the walls minus the
keepouts, and every hard constraint holding with the request's own numbers against the real walls.
An overlap with a fixed box other than a window (validate.check fixed_blocking: a column, a door, a stair) also
fails ok: those are physical obstacles; furniture in front of a window is reported in fixed_collisions only.
Collisions between placed objects are reported, not gated (a chair tucked under a table is a
box overlap; the reference layouts themselves have ~10% rooms with a severe one)."""
ext = {o["id"]: o["ext_id"] for o in room["objects"] + (room.get("fixed") or [])}
lay = apply(room, pl)
c = check(lay)
grown = ctx["floor"].buffer(1e-3)
outside = [ext[o["id"]] for o in lay["objects"] if not grown.covers(Polygon(footprint(o)))]
cons = [{"constraint": [k[0]] + [ext.get(v, v) for v in k[1:]], "hard": hard,
"holds": holds(k, lay["objects"], ctx["walls"]), "trained": trained(k, lay)} for k, hard in ctx["checks"]]
cons += [{"constraint": ["anchor", ext[o["id"]], o["anchor"]], "hard": True,
"holds": bool(o.get("parent")) == (o["anchor"] == "object"),
"trained": False, "model_conditioned": False}
for o in lay["objects"] if o.get("anchor") in ("floor", "object")]
cons += [{"constraint": ["keepout", pts], "hard": False,
"holds": all(Polygon(footprint(o)).intersection(zone).area <= 1e-9 for o in lay["objects"]),
"trained": False, "model_conditioned": False} for pts, zone in ctx["soft_keepouts"]]
return {"ok": c["valid"] and not outside and not c["fixed_blocking"] and all(x["holds"] for x in cons if x["hard"]),
"oob": [ext[i] for i in c["oob"]],
"support_fail": [ext[i] for i in c["support_fail"]], "ceiling": [ext[i] for i in c["ceiling"]],
"outside_1mm": outside, "collisions": {k: [[ext[a], ext[b]] for a, b in v] for k, v in c["collisions"].items()},
"fixed_collisions": [[ext[a], ext[b]] for a, b in c["fixed_collisions"]],
"fixed_blocking": [[ext[a], ext[b]] for a, b in c["fixed_blocking"]], "constraints": cons}
if __name__ == "__main__":
import json
from fastfill.scene import messages, target_json
from fastfill.validate import holds
req = { # scope doc Appendix B, room moved to (10, 20) and one chair modelled with its front along local +Y
"room": {"id": "new_room", "room_type": "meeting room", "boundary_xy": [[10, 20], [14, 20], [14, 24], [10, 24]],
"boundary_source": "specified_polygon", "floor_id": "room_floor", "floor_z": 0.0, "height_m": 2.8},
"objects_to_place": [
{"id": "table_1", "category": "table", "size_xyz_m": [1.4, 0.8, 0.75]},
{"id": "chair_1", "category": "chair", "size_xyz_m": [0.55, 0.5, 0.9], "semantic_front_local": [1, 0, 0]},
{"id": "chair_2", "category": "chair", "size_xyz_m": [0.5, 0.55, 0.9], "semantic_front_local": [0, 1, 0]},
{"id": "cup_1", "category": "cup", "size_xyz_m": [0.08, 0.08, 0.1]},
{"id": "clock_1", "category": "clock", "size_xyz_m": [0.3, 0.05, 0.3], "anchor": "wall"}],
"layout_constraints": [
{"type": "faces", "subject": "chair_1", "target": "chair_2", "hard": True},
{"type": "faces", "subject": "chair_2", "target": "chair_1", "hard": True},
{"type": "between", "subject": "table_1", "anchors": ["chair_1", "chair_2"], "hard": True},
{"type": "supported_by", "subject": "cup_1", "parent": "table_1", "surface": "top", "hard": True}]}
room, ctx = request_to_room(req)
assert [o["id"] for o in room["objects"]] == ["table_1", "chair_1", "chair_2", "cup_1"], room["objects"]
assert room["objects"][2]["size"] == [0.55, 0.5, 0.9] # chair_2 re-framed: front axis first
assert ctx["unsupported"] == [{"id": "clock_1", "reason": "unsupported_anchor"}]
assert room["constraints"][3] == ["on", "cup_1", "table_1"]
# the model answers in its own frame: chair_1 at x=0.9 facing +X, chair_2 at x=3.1 facing -X, cup on the table
ans = {"table_1": ([2, 2, 0], 0), "chair_1": ([0.9, 2, 0], 0), "chair_2": ([3.1, 2, 0], math.pi),
"cup_1": ([1.7, 2, 0.75], 0)}
placed = {**room, "objects": [{**o, "pos": ans[o["id"]][0], "yaw": ans[o["id"]][1],
"parent": "table_1" if o["id"] == "cup_1" else None} for o in room["objects"]]}
assert all(holds(c, placed["objects"], placed["boundary"]) for c in room["constraints"])
text = target_json(placed)
assert "constraints" in messages(room, room["constraints"], with_target=False)[1]["content"]
out = placements_from_text(text, room, ctx)
got = {p["id"]: p for p in out["placements"]}
assert out["error"] is None and [p["id"] for p in out["placements"]] == ["table_1", "chair_1", "chair_2", "cup_1"]
assert got["chair_1"]["position_m"] == [10.9, 22, 0] and got["chair_1"]["yaw_rad"] == 0
assert abs(got["chair_2"]["yaw_rad"] - math.pi / 2) < 1e-9 # front on local +Y: asset turned by 90 deg
assert got["cup_1"]["support_parent"] == "table_1" and got["cup_1"]["support_surface"] == "top"
assert got["table_1"]["support_parent"] == "room_floor" and "support_surface" not in got["table_1"]
assert placements_from_text("not json", room, ctx)["error"] == "bad_json"
# table 3 cm through the east wall (x up to 4.03): moved back inside, the cup on it moves along; 20 cm is left alone
over = {**placed, "objects": [{**o, "pos": [3.33 if o["id"] == "table_1" else 3.0 if o["id"] == "cup_1" else o["pos"][0],
*o["pos"][1:]]} for o in placed["objects"]]}
got = {p["id"]: p["position_m"] for p in placements_from_text(target_json(over), room, ctx)["placements"]}
assert abs(got["table_1"][0] - 13.3) < 1e-9 and abs(got["cup_1"][0] - 12.97) < 1e-9, got
far = {**over, "objects": [{**o, "pos": [3.5, *o["pos"][1:]]} if o["id"] == "table_1" else o for o in over["objects"]]}
assert placements_from_text(target_json(far), room, ctx)["placements"][0]["position_m"][0] == 13.5
# a doorway keepout on the west wall becomes a notch (padded) in the floor the model sees; the frame is unchanged
door = {"type": "keepout", "polygon_xy": [[10, 21.5], [11, 21.5], [11, 22.5], [10, 22.5]]}
room2, ctx2 = request_to_room({**req, "layout_constraints": req["layout_constraints"] + [door]})
assert (ctx2["x0"], ctx2["y0"]) == (10, 20) and [0, 1.42] in room2["boundary"] and [1.08, 2.58] in room2["boundary"]
assert room2["constraints"] == room["constraints"]
# a constraint naming an unsupported object is skipped and reported; the rest of the room is still placed
hang = {"type": "near", "subject": "clock_1", "target": "table_1", "max_gap_m": 0.5}
room3, ctx3 = request_to_room({**req, "layout_constraints": [hang]})
assert room3["constraints"] == [] and ctx3["unsupported"][1] == {"constraint": hang, "reason": "references_unsupported_object"}
up = {**req, "objects_to_place": [{**req["objects_to_place"][0], "semantic_front_local": [0, 0, 1]}], "layout_constraints": []}
assert request_to_room(up)[1]["unsupported"] == [{"id": "table_1", "reason": "front_not_axis_aligned"}]
bad = [{**req, "objects_to_place": req["objects_to_place"] * 2}, # duplicate ids
{**req, "layout_constraints": [{"type": "near", "id": "cup_1", "target": "table_1", "max_distance_m": 1.5}]},
{**req, "layout_constraints": [{"type": "between", "subject": "table_1", "anchors": ["chair_1"]}]},
{**req, "room": {**req["room"], "floor_z": None}},
{**req, "objects_to_place": [{**req["objects_to_place"][0], "size_xyz_m": [1.4, -0.8, 0.75]}]},
{**req, "objects_to_place": [{**req["objects_to_place"][0], "size_xyz_m": [1.4, float("nan"), 0.75]}]},
{**req, "layout_constraints": [{**door, "polygon_xy": [[9, 19], [15, 19], [15, 25], [9, 25]]}]}] # fills the room
obj0 = req["objects_to_place"][0]
bad += [{**req, "layout_constraints": [{"type": "supported_by", "subject": "cup_1", "parent": "cup_1"}]}, # self
{**req, "layout_constraints": [{"type": "supported_by", "subject": "cup_1", "parent": "table_1"},
{"type": "supported_by", "subject": "table_1", "parent": "cup_1"}]}, # cycle
{**req, "layout_constraints": [{"type": "faces", "subject": "chair_1", "target": "chair_2", "hard": "false"}]},
{**req, "layout_constraints": [{"type": "between", "subject": "table_1", "anchors": "cc"}]},
{**req, "coordinate_system": {"units": "cm"}},
{**req, "objects_to_place": [{**obj0, "anchor": "banana"}]},
{**req, "objects_to_place": req["objects_to_place"] + [{"id": "x", "category": "clock", "size_xyz_m": [-1, 1, 1],
"anchor": "wall"}]},
{**req, "room": {**req["room"], "room_type": 7}},
{**req, "room": {**req["room"], "height_m": 0}},
{**req, "objects_to_place": [{**obj0, "size_xyz_m": [1e200, 1, 1]}]},
{**req, "room": {**req["room"], "floor_z": float("inf")}},
{**req, "room": {**req["room"], "height_m": float("nan")}},
{**req, "layout_constraints": [{"type": "near", "subject": "cup_1", "target": "table_1", "max_gap_m": -0.1}]}]
for b in bad:
try:
request_to_room(b)
raise AssertionError(b)
except ValueError:
pass
# near: the model is shown the threshold rounded down to cm, the answer is accepted against the request's own
# number; a failed hard constraint fails the layout (after repair), a failed soft one is only reported
near = lambda gap, hard=True: {"type": "near", "subject": "chair_1", "target": "chair_2", "max_gap_m": gap, "hard": hard}
r5, c5 = request_to_room({**req, "layout_constraints": [near(0.126)]})
assert r5["constraints"] == [["near", "chair_1", "chair_2", 0.12]] and c5["checks"][0][0][3] == 0.126
apart = target_json(placed) # chairs 1.7 m apart: fails any near <= 0.5
v = placements_from_text(apart, *request_to_room({**req, "layout_constraints": [near(0.1)]}))["validation"]
assert not v["repaired"]["ok"] and v["repaired"]["constraints"][0]["holds"] is False, v
v = placements_from_text(apart, *request_to_room({**req, "layout_constraints": [near(0.1, hard=False)]}))["validation"]
assert v["repaired"]["ok"], v
# a cup hanging 3 cm over the east wall from a table flush with it: the cup is pushed in, staying on the table
edge = {**placed, "objects": [{**o, "pos": [3.3, 2, 0] if o["id"] == "table_1" else [3.99, 2, 0.75] if o["id"] == "cup_1"
else o["pos"]} for o in placed["objects"]]}
out = placements_from_text(target_json(edge), room, ctx)
cup = {p["id"]: p for p in out["placements"]}["cup_1"]["position_m"]
assert out["validation"]["raw"]["outside_1mm"] == ["cup_1"] and out["validation"]["repaired"]["ok"], out["validation"]
assert abs(cup[0] - 13.96) < 1e-9, cup
# fixed geometry: a column at (12, 22) is shown to the model in the room frame, never placed, and a table on it is
# reported (not refused); a fixed id that repeats an object id is a duplicate
col = {"id": "col_1", "category": "column", "size_xyz_m": [0.3, 0.3, 2.8], "position_m": [12, 22, 0]}
room6, ctx6 = request_to_room({**req, "room": {**req["room"], "fixed_geometry": [col]}, "layout_constraints": []})
assert room6["fixed"][0]["id"] == "column_1" and room6["fixed"][0]["pos"] == [2, 2, 0], room6["fixed"]
assert json.loads(messages(room6, [], with_target=False)[1]["content"])["fixed"] == [
{"id": "column_1", "size": [0.3, 0.3, 2.8], "pos": [2, 2, 0], "yaw": 0}]
out6 = placements_from_text(target_json(placed), room6, ctx6) # table_1 centred on the column
assert not out6["validation"]["repaired"]["ok"] and out6["validation"]["repaired"]["fixed_collisions"] == [["table_1", "col_1"]]
# the repair moves a table with the cup on it; a move that would push the CUP into a fixed upper cabinet (above the
# table top, at the cup's height) is not taken, so the table stays 3 cm through the wall and the layout fails
cab = {"id": "cab_1", "category": "cabinet", "size_xyz_m": [0.4, 0.6, 1.0], "position_m": [12.75, 22, 0.78]}
room9, ctx9 = request_to_room({**req, "room": {**req["room"], "fixed_geometry": [cab]}, "layout_constraints": []})
moved = {"table_1": [3.33, 2, 0], "cup_1": [3.0, 2, 0.75], "chair_2": [1.5, 3.5, 0]}
lay9 = {**placed, "objects": [{**o, "pos": moved.get(o["id"], o["pos"])} for o in placed["objects"]]}
out9 = placements_from_text(target_json(lay9), room9, ctx9)
got = {p["id"]: p["position_m"] for p in out9["placements"]}
assert abs(got["table_1"][0] - 13.33) < 1e-9 and abs(got["cup_1"][0] - 13.0) < 1e-9, got
assert not out9["validation"]["repaired"]["fixed_collisions"] and "table_1" in out9["validation"]["repaired"]["outside_1mm"]
for b in ({**col, "id": "table_1"}, {**col, "size_xyz_m": [0, 0.3, 2.8]}): # duplicate id; zero size
try:
request_to_room({**req, "room": {**req["room"], "fixed_geometry": [b]}})
raise AssertionError(b)
except ValueError:
pass
# a constraint may not name a fixed box: refused with its own message
try:
request_to_room({**req, "room": {**req["room"], "fixed_geometry": [col]},
"layout_constraints": [{"type": "near", "subject": "table_1", "target": "col_1", "max_gap_m": 0.5}]})
raise AssertionError("constraint on a fixed box accepted")
except ValueError as e:
assert "fixed box" in str(e), e
# training-side normalization: |z| <= FLOOR_Z snapped to 0; a box starting at FIXED_MAX_Z or above, covering half
# the room, or standing clear of it is not shown and reported as fixed_ignored
fx = lambda **kw: {**col, **kw}
room8, ctx8 = request_to_room({**req, "layout_constraints": [], "room": {**req["room"], "floor_z": 1.0, "fixed_geometry": [
fx(id="door", position_m=[12, 20.5, 1.06]), fx(id="beam", size_xyz_m=[4, 0.3, 0.3], position_m=[12, 22, 2.5]),
fx(id="slab", size_xyz_m=[3, 3, 0.1], position_m=[12, 22, 1]), fx(id="far", position_m=[30, 30, 1])]}})
assert [f["ext_id"] for f in room8["fixed"]] == ["door"] and room8["fixed"][0]["pos"] == [2, 0.5, 0], room8["fixed"]
assert [u["id"] for u in ctx8["unsupported"] if u["reason"] == "fixed_ignored"] == ["beam", "slab", "far"], ctx8["unsupported"]
# repair never deepens the overlap with a fixed box: a table lying along the east wall 3 cm out (yaw 90, x 3.23..4.03)
# with its west edge 8 cm over a column at (3.3, 2) is left where it is (every move within SNAP_TOL pushes it further
# in, so it stays outside_1mm and serve.py refuses the layout); the same table clear of the column is moved back
col2 = {**col, "position_m": [13.3, 22, 0]}
room7, ctx7 = request_to_room({**req, "room": {**req["room"], "fixed_geometry": [col2]}, "layout_constraints": []})
side = lambda y: {**placed, "objects": [{**o, "pos": [3.63, y, o["pos"][2]], "yaw": math.pi / 2 if o["id"] == "table_1" else o["yaw"]}
if o["id"] in ("table_1", "cup_1") else o for o in placed["objects"]]}
out7 = placements_from_text(target_json(side(2.0)), room7, ctx7)
got = {p["id"]: p["position_m"] for p in out7["placements"]}
assert abs(got["table_1"][0] - 13.63) < 1e-9 and out7["validation"]["repaired"]["outside_1mm"] == ["table_1"], out7["validation"]
assert ["table_1", "col_1"] in out7["validation"]["repaired"]["fixed_collisions"]
got = {p["id"]: p["position_m"] for p in placements_from_text(target_json(side(2.9)), room7, ctx7)["placements"]}
assert abs(got["table_1"][0] - 13.6) < 1e-9 and abs(got["cup_1"][0] - 13.6) < 1e-9, got
# a column sunk 0.5 m below the floor is cut at the floor (height 2.3); a box sunk under the floor is not shown
from fastfill.build import FLAT_Z as BUILD_FLAT_Z
assert FLAT_Z == BUILD_FLAT_Z
sunk = [{"id": "c9", "category": "column", "size_xyz_m": [0.3, 0.3, 2.8], "position_m": [12, 22, -0.5]},
{"id": "u9", "category": "unknown", "size_xyz_m": [1, 1, 0.4], "position_m": [12.5, 22.5, -0.6]}]
r7, c7 = request_to_room({**req, "room": {**req["room"], "fixed_geometry": sunk}, "layout_constraints": []})
assert [(f["ext_id"], f["pos"][2], f["size"][2]) for f in r7["fixed"]] == [("c9", 0.0, 2.3)], r7["fixed"]
assert {"id": "u9", "reason": "fixed_ignored"} in c7["unsupported"]
# objects are ordered by cm-rounded sizes clamped at 1 cm, as in training (a 3 mm side sorts as 1 cm)
thin = {**req, "objects_to_place": [{"id": "mat", "category": "mat", "size_xyz_m": [1.0, 0.003, 0.9]},
{"id": "pad", "category": "mat", "size_xyz_m": [0.5, 0.01, 0.9]}],
"layout_constraints": []}
assert [o["ext_id"] for o in request_to_room(thin)[0]["objects"]] == ["mat", "pad"]
# re-entrant corner of an L-shaped room: the nearest-boundary push is diagonal and long, a 6 cm move along -x works
ell = Polygon([(0, 0), (4, 0), (4, 2), (2, 2), (2, 4), (0, 4)])
plc = {"b": {"pos": [1.76, 2.22, 0], "yaw": 0.0, "on": None}}
snap_inside(plc, {"objects": [{"id": "b", "size": [0.6, 0.6, 0.8]}]}, ell)
assert plc["b"]["pos"][:2] == [1.7, 2.22] or abs(plc["b"]["pos"][0] - 1.7) < 1e-9, plc
# the repair never breaks a hard constraint that held: a chair 3 cm through the south wall faces a box 28.5 deg off
# its front; the shortest push (0, +3 cm) turns that to 32 deg (> 30), so (-4, +3) cm is taken instead
sq, two = Polygon([(0, 0), (4, 0), (4, 3), (0, 3)]), {"objects": [{"id": "c", "size": [.5, .5, .9]}, {"id": "b", "size": [.06, .06, .3]}]}
lay = lambda q: [{"id": i, "pos": q[i]["pos"], "yaw": 0.0, "size": s["size"]} for i, s in zip("cb", two["objects"])]
for cons, want in (([], [1, .25]), ([["faces", "c", "b"]], [.96, .25])):
plc = {"c": {"pos": [1, .22, 0], "yaw": 0.0, "on": None}, "b": {"pos": [1.35, .03, 0], "yaw": 0.0, "on": None}}
assert holds(["faces", "c", "b"], lay(plc), sq.exterior.coords[:-1])
snap_inside(plc, two, sq, cons, sq.exterior.coords[:-1])
assert all(abs(u - v) < 1e-9 for u, v in zip(plc["c"]["pos"], want)), plc
assert holds(["faces", "c", "b"], lay(plc), sq.exterior.coords[:-1]) == bool(cons)
# two chairs and a side table between them, all 5 cm through the east wall: the between fails after the second chair
# moves and holds again once the table moves, so the plain pass is kept (a move-by-move guard left chair b outside)
row = {"objects": [{"id": "a", "size": [.7, .7, .9]}, {"id": "b", "size": [.7, .7, .9]}, {"id": "s", "size": [.38, .38, .6]}]}
plc = {i: {"pos": [x, y, 0], "yaw": 0.0, "on": None} for i, x, y in (("a", 3.7, 1), ("b", 3.7, 3), ("s", 3.86, 2))}
sq4 = Polygon([(0, 0), (4, 0), (4, 4), (0, 4)])
snap_inside(plc, row, sq4, [["between", "s", "a", "b"]], sq4.exterior.coords[:-1])
assert all(abs(plc[i]["pos"][0] - x) < 1e-9 for i, x in (("a", 3.65), ("b", 3.65), ("s", 3.81))), plc
# a padded keepout ending 3 mm short of the east wall (the finding's 1.48 m room) snaps to it; 2 cm short stays
ko = lambda x1: {"room": {"boundary_xy": [[0, 0], [1.48, 0], [1.48, 3.48], [0, 3.48]]},
"objects_to_place": [{"id": "a", "category": "chair", "size_xyz_m": [0.5, 0.5, 0.9]}],
"layout_constraints": [{"type": "keepout", "polygon_xy": [[.495, -.2], [x1, -.2], [x1, 1], [.495, 1]]}]}
assert [1.48, 1.08] in request_to_room(ko(1.48 - KEEPOUT_PAD - 0.003))[0]["boundary"]
assert [1.46, 1.08] in request_to_room(ko(1.48 - KEEPOUT_PAD - 0.02))[0]["boundary"]
# two padded keepouts 7.5 mm apart in front of a 1.08 x 0.62 m corner (EmbodiedGen-shaped eg#173): the gap stays open
# and the corner stays floor; 2.9 mm apart the gap folds under cm rounding, as does a keepout crossing a 45-degree
# wall by 3 mm: keepout_not_representable, not bad_boundary (the walls themselves round cleanly)
pair = lambda x1: {"room": {"boundary_xy": [[0, 0], [4.5, 0], [4.5, 3], [0, 3]]}, "objects_to_place": ko(0)["objects_to_place"],
"layout_constraints": [{"type": "keepout", "polygon_xy": [[3.5, 1.1], [4.5, 1.1], [4.5, 2.3], [3.5, 2.3]]},
{"type": "keepout", "polygon_xy": [[2.1, 2.4], [x1, 2.4], [x1, 3], [2.1, 3]]}]}
assert [4.5, 3] in request_to_room(pair(3.3325))[0]["boundary"]
mouth = {"room": {"boundary_xy": [[0, 0], [4, 0], [4, 4], [1, 4], [0, 3]]}, "objects_to_place": ko(0)["objects_to_place"],
"layout_constraints": [{"type": "keepout", "polygon_xy": [[.58, 2.08], [1.42, 2.08], [1.42, 3.423], [.58, 3.423]]}]}
for b in (pair(3.3371), mouth):
try:
request_to_room(b)
raise AssertionError(b)
except ValueError as e:
assert str(e) == "keepout_not_representable", e
# forms training never has are checked as asked but flagged: faces from a round table (no FRONT head), against_wall
# in a hull room, near from a cup that stands on the table; faces / between / on of the Appendix-B request are trained
assert all(c["trained"] for c in placements_from_text(text, room, ctx)["validation"]["repaired"]["constraints"])
hull = {"room": {**req["room"], "boundary_source": "scan_hull"}, "objects_to_place": [
{"id": "rt", "category": "round table", "size_xyz_m": [1, 1, .75]}, {"id": "sh", "category": "shelf", "size_xyz_m": [1, .4, 1.8]},
{"id": "cup", "category": "cup", "size_xyz_m": [.08, .08, .1]}], "layout_constraints": [
{"type": "faces", "subject": "rt", "target": "sh"}, {"type": "against_wall", "subject": "sh"},
{"type": "near", "subject": "cup", "target": "sh", "max_gap_m": 0.5}]}
rh, ch = request_to_room(hull)
ans = '{"placements":[{"id":"round_table_1","pos":[2,2,0],"yaw":270},{"id":"shelf_1","pos":[2,0.2,0],"yaw":0},' \
'{"id":"cup_1","on":"round_table_1","pos":[2,2,0.75],"yaw":0}]}' # the cup 1.56 m from the shelf
v = placements_from_text(ans, rh, ch)["validation"]["repaired"]
assert [(c["trained"], c["holds"]) for c in v["constraints"]] == [(False, True), (False, True), (False, False)], v
assert not v["ok"] and not v["outside_1mm"] and not v["oob"], v
# build never relates a rug, a sub-5 cm box or (except by 'on') an object standing on another
lay = {"boundary_type": "polygon", "objects": [
{"id": "s", "category": "sofa", "size": [2, .9, .8]}, {"id": "r", "category": "rug", "size": [2, 1.5, .01]},
{"id": "l", "category": "lamp", "size": [.3, .3, .5], "parent": "s"},
{"id": "k", "category": "coaster", "size": [.1, .1, .01], "parent": "s"}]}
assert [trained(k, lay) for k in (["near", "s", "r", .5], ["faces", "s", "r"], ["against_wall", "l"], ["on", "k", "s"],
["on", "l", "s"], ["against_wall", "s"])] == [False] * 4 + [True] * 2
print("interface.py self-check ok")
print(json.dumps(out["placements"][2]))
|