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
Download code/fastfill/interface.py from XXXpilar/Fastfill-test: direct link, hf CLI and curl.
- Browser
- Download file 48.2 kB
-
https://huggingface.co/XXXpilar/Fastfill-test/resolve/main/code/fastfill/interface.py
- Command line
-
hf download hf://XXXpilar/Fastfill-test/code/fastfill/interface.py
-
curl -L -o interface.py https://huggingface.co/XXXpilar/Fastfill-test/resolve/main/code/fastfill/interface.py
48.2 kB
| """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])) | |