"""Mini HTML returned next to every picture. HTML is the final destination: a chat model reads this page, edits it and builds its own on it. The page is self-contained Three.js. Geometry is inlined at low poly (a rung of the simplification ladder), so it outlives Gradio's temporary file URLs. Even low poly costs thousands of tokens, so the tools return the page as a URL unless the caller asks for its text. """ import json import numpy as np from .scene import GLOW, _sky from .simplify import base_color, decimate THREE = "https://unpkg.com/three@0.160.0/" SCENE_FACE_BUDGET = 400 # per object in a scene page LADDER_MAX_FACES = 1000 # rungs above this are left to the GLB files def _hex(rgb): return "#%02x%02x%02x" % tuple(int(c) for c in rgb[:3]) def _sky_hex(sun): sky = _sky(sun) return sky if isinstance(sky, str) else _hex([c * 255 for c in sky]) def _geometry(mesh, budget): """Inline mesh; "floor" marks a mesh that wouldn't decimate down to the budget.""" m = decimate(mesh, budget) if len(mesh.faces) > budget else mesh return {"faces": int(len(m.faces)), "budget": budget, "floor": bool(len(m.faces) > budget), "p": np.round(m.vertices, 2).ravel().tolist(), # cm: plenty at low poly, fewer tokens "i": m.faces.ravel().astype(int).tolist()} def _object(oid, word, mesh, position, yaw_deg, cand=None, glow=False, budget=SCENE_FACE_BUDGET, label=None): o = {"id": oid, "word": word, "label": label, "position": [round(float(c), 3) for c in position], "yaw_deg": round(float(yaw_deg), 1), "color": _hex(GLOW if glow else base_color(mesh)), "emits_light": glow} if cand and cand.get("source") == "primitive": o["primitive"] = {"shape": cand["shape"], "size": [round(float(e), 3) for e in mesh.extents]} else: o["mesh"] = _geometry(mesh, budget) return o def scene_page(ledger, meshes, config): sun = config["sun"] objects = [] for it in ledger["items"]: p = config["placements"].get(it["id"]) if it["kind"] != "object" or p is None: continue dec = it["decision"] objects.append(_object(it["id"], it["word"], meshes[it["id"]], p["position"], p["yaw_deg"], cand=dec["chosen"], glow=bool(p.get("light")), label=dec.get("label"))) ground = None if any(i["kind"] == "environment" and i["role"] == "plane" for i in ledger["items"]): reach = max([abs(c) for o in objects if not o["emits_light"] for c in (o["position"][0], o["position"][2])] + [0.3]) ground = {"radius": round(1.6 * reach + 0.5, 3)} data = { "title": ledger["sentence"], "reading": [{"word": t["text"], "kind": t["kind"], "commits": t["commits"]} for t in ledger["tokens"]], "relations": [{"subject": i["subject"], "relation": i["relation"], "object": i["object"]} for i in ledger["items"] if i["kind"] == "relation"], "unresolved": [{"question": q.get("question"), "default": q.get("default_chosen")} for q in ledger["unresolved"]], "sky": _sky_hex(sun), "sun": sun, "ground": ground, "objects": objects, } return page(data) def ladder_page(item, rungs): """Rungs left to right, from the most detailed embeddable one down to the box.""" objects, x = [], 0.0 for label, rung in rungs: if len(rung.faces) > LADDER_MAX_FACES: continue hw = 0.5 * max(rung.extents[0], rung.extents[2]) objects.append(_object(f"rung{len(objects)}", item["word"], rung, [x + hw, 0, 0], 0, budget=LADDER_MAX_FACES, label=label)) x += 2 * hw + 0.3 * max(rung.extents[1], 0.1) for o in objects: o["position"][0] = round(o["position"][0] - x / 2, 3) data = {"title": f"{item['word']}: simplification ladder", "caption": "left → right: " + " · ".join(o["label"] for o in objects), "sky": "#2a3346", "sun": None, "ground": None, "objects": objects} return page(data) def page(data): payload = json.dumps(data, separators=(",", ":")).replace("", ">") TEMPLATE = """ __TITLE__
"""