Input
{
"patchText": "*** Begin Patch\n*** Add File: /tmp/opencode/n5ocamp_scene/build_n5ocamp_scene.py\n+#!/usr/bin/env blender --background --python\n+\"\"\"Build a fresh five-tent N5OCAMP 3D scene.\n+\n+This is intentionally a clean rewrite. The visible logo coordinates are\n+back-projected through a perspective camera, while every tent gets actual depth\n+so the exported GLB remains inspectable from all sides.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+from pathlib import Path\n+import math\n+\n+import bpy\n+from mathutils import Vector\n+\n+\n+ROOT = Path(__file__).resolve().parent\n+WIDTH = 820\n+HEIGHT = 180\n+ASPECT = WIDTH / HEIGHT\n+FOV_Y = math.radians(13.5)\n+\n+RENDER_OUT = ROOT / \"n5ocamp_render.png\"\n+GLB_OUT = ROOT / \"n5ocamp_scene.glb\"\n+BLEND_OUT = ROOT / \"n5ocamp_scene.blend\"\n+OBJ_OUT = ROOT / \"n5ocamp_scene.obj\"\n+\n+FRONT_DEPTH = 8.0\n+REAR_DEPTH = 9.25\n+DECAL_DEPTH = 7.92\n+TEXT_DEPTH = 7.88\n+\n+WHITE_RGBA = (0.975, 0.975, 0.975, 1.0)\n+BLACK_RGBA = (0.0, 0.0, 0.0, 1.0)\n+\n+Point = tuple[float, float]\n+\n+\n+def reset_scene() -> None:\n+ bpy.ops.object.select_all(action=\"SELECT\")\n+ bpy.ops.object.delete()\n+ for block in (bpy.data.meshes, bpy.data.materials, bpy.data.curves, bpy.data.cameras):\n+ for item in list(block):\n+ block.remove(item)\n+\n+\n+def make_unlit_material(name: str, color: tuple[float, float, float, float]) -> bpy.types.Material:\n+ mat = bpy.data.materials.new(name)\n+ mat.diffuse_color = color\n+ mat.use_nodes = True\n+ mat.use_backface_culling = False\n+ mat.node_tree.nodes.clear()\n+ out = mat.node_tree.nodes.new(\"ShaderNodeOutputMaterial\")\n+ emit = mat.node_tree.nodes.new(\"ShaderNodeEmission\")\n+ emit.inputs[\"Color\"].default_value = color\n+ emit.inputs[\"Strength\"].default_value = 1.0\n+ mat.node_tree.links.new(emit.outputs[\"Emission\"], out.inputs[\"Surface\"])\n+ return mat\n+\n+\n+def screen_to_world(x: float, y: float, depth: float) -> Vector:\n+ half_h = math.tan(FOV_Y / 2.0) * depth\n+ half_w = half_h * ASPECT\n+ nx = x / WIDTH * 2.0 - 1.0\n+ ny = 1.0 - y / HEIGHT * 2.0\n+ return Vector((nx * half_w, ny * half_h, -depth))\n+\n+\n+def make_mesh(name: str, verts: list[Vector], faces: list[tuple[int, ...]], mat: bpy.types.Material) -> bpy.types.Object:\n+ mesh = bpy.data.meshes.new(name)\n+ mesh.from_pydata([tuple(v) for v in verts], [], faces)\n+ mesh.update()\n+ obj = bpy.data.objects.new(name, mesh)\n+ obj.data.materials.append(mat)\n+ bpy.context.collection.objects.link(obj)\n+ return obj\n+\n+\n+def strip_points(a: Point, b: Point, width: float) -> list[Point]:\n+ ax, ay = a\n+ bx, by = b\n+ dx = bx - ax\n+ dy = by - ay\n+ length = math.hypot(dx, dy)\n+ nx = -dy / length * width / 2.0\n+ ny = dx / length * width / 2.0\n+ return [(ax + nx, ay + ny), (bx + nx, by + ny), (bx - nx, by - ny), (ax - nx, ay - ny)]\n+\n+\n+def screen_slab(name: str, points: list[Point], depth: float, thickness: float, mat: bpy.types.Material) -> bpy.types.Object:\n+ front = [screen_to_world(x, y, depth) for x, y in points]\n+ back = [screen_to_world(x, y, depth + thickness) for x, y in points]\n+ n = len(points)\n+ faces: list[tuple[int, ...]] = [tuple(range(n)), tuple(reversed(range(n, 2 * n)))]\n+ for i in range(n):\n+ j = (i + 1) % n\n+ faces.append((i, j, j + n, i + n))\n+ return make_mesh(name, front + back, faces, mat)\n+\n+\n+def add_strip(name: str, a: Point, b: Point, width: float, mat: bpy.types.Material, depth: float = DECAL_DEPTH, thickness: float = 0.035) -> bpy.types.Object:\n+ return screen_slab(name, strip_points(a, b, width), depth, thickness, mat)\n+\n+\n+def add_aframe_tent(\n+ prefix: str,\n+ front_left: Point,\n+ apex: Point,\n+ front_right: Point,\n+ rear_left: Point,\n+ rear_apex: Point,\n+ rear_right: Point,\n+ white: bpy.types.Material,\n+ black: bpy.types.Material,\n+) -> None:\n+ fl = screen_to_world(*front_left, FRONT_DEPTH)\n+ fa = screen_to_world(*apex, FRONT_DEPTH)\n+ fr = screen_to_world(*front_right, FRONT_DEPTH)\n+ rl = screen_to_world(*rear_left, REAR_DEPTH)\n+ ra = screen_to_world(*rear_apex, REAR_DEPTH)\n+ rr = screen_to_world(*rear_right, REAR_DEPTH)\n+\n+ make_mesh(\n+ f\"{prefix}_solid_aframe_body\",\n+ [fl, fa, fr, rl, ra, rr],\n+ [\n+ (0, 1, 2),\n+ (3, 5, 4),\n+ (0, 3, 4, 1),\n+ (1, 4, 5, 2),\n+ (0, 2, 5, 3),\n+ ],\n+ white,\n+ )\n+\n+ # Front logo cut-lines are real black geometry, placed slightly closer than\n+ # the fabric. This preserves the white-on-black mark without requiring a\n+ # transparent material in web viewers.\n+ ax, ay = apex\n+ lx, ly = front_left\n+ rx, ry = front_right\n+ add_strip(f\"{prefix}_left_inner_black_seam\", (ax, ay + 6), (lx + 14, ly - 3), 2.15, black)\n+ add_strip(f\"{prefix}_right_inner_black_seam\", (ax, ay + 6), (rx - 11, ry - 2), 2.15, black)\n+ add_strip(f\"{prefix}_low_black_ground_shadow\", (lx + 7, ly - 1), (rx - 7, ry - 1), 1.2, black)\n+\n+\n+def add_crossed_tent(white: bpy.types.Material, black: bpy.types.Material) -> None:\n+ # Third tent in the source mark: a crossed-pole/collapsed tent form. It is\n+ # separate 3D geometry, not a flat icon, and has front/back thickness.\n+ add_strip(\"tent_03_crossed_left_pole\", (355, 101), (429, 25), 7.6, white, FRONT_DEPTH, 0.22)\n+ add_strip(\"tent_03_crossed_right_pole\", (479, 101), (400, 25), 7.6, white, FRONT_DEPTH, 0.22)\n+ add_strip(\"tent_03_crossed_left_pole_groove\", (362, 99), (430, 29), 1.8, black, DECAL_DEPTH, 0.03)\n+ add_strip(\"tent_03_crossed_right_pole_groove\", (472, 99), (402, 29), 1.8, black, DECAL_DEPTH, 0.03)\n+ add_strip(\"tent_03_top_left_cap\", (398, 25), (411, 38), 6.2, white, FRONT_DEPTH - 0.02, 0.10)\n+ add_strip(\"tent_03_top_right_cap\", (432, 25), (420, 38), 6.2, white, FRONT_DEPTH - 0.02, 0.10)\n+ add_strip(\"tent_03_top_left_cap_groove\", (401, 27), (409, 35), 1.55, black, DECAL_DEPTH, 0.03)\n+ add_strip(\"tent_03_top_right_cap_groove\", (430, 27), (422, 35), 1.55, black, DECAL_DEPTH, 0.03)\n+\n+ # A narrow hidden spine gives the third tent a real footprint from side/top\n+ # views while staying almost invisible in the logo front view.\n+ center_front = screen_to_world(416, 88, FRONT_DEPTH + 0.22)\n+ center_back = screen_to_world(416, 88, REAR_DEPTH + 0.22)\n+ low_front = screen_to_world(416, 101, FRONT_DEPTH + 0.22)\n+ low_back = screen_to_world(416, 101, REAR_DEPTH + 0.22)\n+ make_mesh(\n+ \"tent_03_crossed_internal_spine\",\n+ [center_front, low_front, center_back, low_back],\n+ [(0, 1, 3, 2)],\n+ white,\n+ )\n+\n+\n+def add_label(white: bpy.types.Material) -> None:\n+ font_path = Path(\"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf\")\n+ font = bpy.data.fonts.load(str(font_path)) if font_path.exists() else None\n+ for char, x in zip(\"N5OCAMP\", [325, 355, 385, 415, 445, 475, 505], strict=True):\n+ curve = bpy.data.curves.new(f\"label_curve_{char}\", \"FONT\")\n+ curve.body = char\n+ curve.align_x = \"CENTER\"\n+ curve.align_y = \"CENTER\"\n+ curve.size = 0.205\n+ curve.extrude = 0.005\n+ if font is not None:\n+ curve.font = font\n+ obj = bpy.data.objects.new(f\"label_{char}\", curve)\n+ obj.location = screen_to_world(x, 134.5, TEXT_DEPTH)\n+ obj.data.materials.append(white)\n+ bpy.context.collection.objects.link(obj)\n+\n+\n+def setup_camera() -> None:\n+ cam_data = bpy.data.cameras.new(\"n5ocamp_perspective_camera\")\n+ cam_data.type = \"PERSP\"\n+ cam_data.sensor_fit = \"VERTICAL\"\n+ cam_data.angle_y = FOV_Y\n+ cam_data.clip_start = 0.02\n+ cam_data.clip_end = 80\n+ cam = bpy.data.objects.new(\"n5ocamp_perspective_camera\", cam_data)\n+ cam.location = (0, 0, 0)\n+ cam.rotation_euler = (0, 0, 0)\n+ bpy.context.collection.objects.link(cam)\n+ bpy.context.scene.camera = cam\n+\n+\n+def setup_render() -> None:\n+ scene = bpy.context.scene\n+ scene.render.resolution_x = WIDTH\n+ scene.render.resolution_y = HEIGHT\n+ scene.render.image_settings.file_format = \"PNG\"\n+ scene.render.image_settings.color_mode = \"RGBA\"\n+ scene.render.film_transparent = False\n+ scene.render.engine = \"CYCLES\"\n+ scene.cycles.device = \"CPU\"\n+ scene.cycles.samples = 96\n+ scene.cycles.use_denoising = False\n+ scene.view_settings.view_transform = \"Standard\"\n+ scene.view_settings.look = \"None\"\n+ scene.view_settings.exposure = 0\n+ scene.view_settings.gamma = 1\n+ scene.world = bpy.data.worlds.new(\"black_world\") if scene.world is None else scene.world\n+ scene.world.color = (0, 0, 0)\n+ scene.world.use_nodes = True\n+ bg = scene.world.node_tree.nodes.get(\"Background\")\n+ if bg:\n+ bg.inputs[\"Color\"].default_value = (0, 0, 0, 1)\n+ bg.inputs[\"Strength\"].default_value = 0\n+\n+\n+def convert_text_to_mesh() -> None:\n+ for obj in list(bpy.context.scene.objects):\n+ if obj.type != \"FONT\":\n+ continue\n+ bpy.ops.object.select_all(action=\"DESELECT\")\n+ obj.select_set(True)\n+ bpy.context.view_layer.objects.active = obj\n+ bpy.ops.object.convert(target=\"MESH\")\n+\n+\n+def export_scene() -> None:\n+ bpy.context.scene.render.filepath = str(RENDER_OUT)\n+ bpy.ops.render.render(write_still=True)\n+ convert_text_to_mesh()\n+ bpy.ops.wm.save_as_mainfile(filepath=str(BLEND_OUT))\n+ bpy.ops.export_scene.gltf(filepath=str(GLB_OUT), export_format=\"GLB\", export_cameras=True, export_lights=False, export_apply=True)\n+ if hasattr(bpy.ops.wm, \"obj_export\"):\n+ bpy.ops.wm.obj_export(filepath=str(OBJ_OUT), export_materials=True)\n+ else:\n+ bpy.ops.export_scene.obj(filepath=str(OBJ_OUT), use_materials=True)\n+\n+\n+def build() -> None:\n+ reset_scene()\n+ white = make_unlit_material(\"flat_warm_white\", WHITE_RGBA)\n+ black = make_unlit_material(\"matte_black_cutout\", BLACK_RGBA)\n+ setup_camera()\n+\n+ add_aframe_tent(\"tent_01\", (28, 103), (90, 42), (152, 101), (75, 101), (137, 46), (190, 98), white, black)\n+ add_aframe_tent(\"tent_02\", (190, 103), (254, 42), (318, 101), (235, 101), (277, 46), (330, 99), white, black)\n+ add_crossed_tent(white, black)\n+ add_aframe_tent(\"tent_04\", (516, 101), (579, 42), (642, 101), (503, 99), (555, 46), (595, 99), white, black)\n+ add_aframe_tent(\"tent_05\", (679, 101), (742, 42), (806, 101), (648, 99), (696, 46), (759, 99), white, black)\n+ add_label(white)\n+ setup_render()\n+ export_scene()\n+\n+ print(RENDER_OUT)\n+ print(GLB_OUT)\n+ print(BLEND_OUT)\n+ print(OBJ_OUT)\n+\n+\n+if __name__ == \"__main__\":\n+ build()\n*** Add File: /tmp/opencode/n5ocamp_scene/validate_scene.py\n+#!/usr/bin/env blender --background --python\n+\"\"\"Validate the exported GLB from multiple sides.\"\"\"\n+\n+from __future__ import annotations\n+\n+from pathlib import Path\n+import math\n+\n+import bpy\n+from mathutils import Vector\n+\n+\n+ROOT = Path(__file__).resolve().parent\n+GLB = ROOT / \"n5ocamp_scene.glb\"\n+OUT = ROOT / \"validation\"\n+\n+\n+def reset() -> None:\n+ bpy.ops.object.select_all(action=\"SELECT\")\n+ bpy.ops.object.delete()\n+\n+\n+def setup_render() -> None:\n+ scene = bpy.context.scene\n+ scene.render.resolution_x = 900\n+ scene.render.resolution_y = 620\n+ scene.render.image_settings.file_format = \"PNG\"\n+ scene.render.image_settings.color_mode = \"RGBA\"\n+ scene.render.engine = \"CYCLES\"\n+ scene.cycles.device = \"CPU\"\n+ scene.cycles.samples = 64\n+ scene.cycles.use_denoising = False\n+ scene.view_settings.view_transform = \"Standard\"\n+ scene.view_settings.look = \"None\"\n+ scene.world = bpy.data.worlds.new(\"validation_black\") if scene.world is None else scene.world\n+ scene.world.color = (0, 0, 0)\n+ scene.world.use_nodes = True\n+ bg = scene.world.node_tree.nodes.get(\"Background\")\n+ if bg:\n+ bg.inputs[\"Color\"].default_value = (0, 0, 0, 1)\n+ bg.inputs[\"Strength\"].default_value = 0\n+\n+\n+def bounds() -> tuple[Vector, float]:\n+ pts: list[Vector] = []\n+ for obj in bpy.context.scene.objects:\n+ if obj.type == \"MESH\":\n+ pts.extend(obj.matrix_world @ Vector(corner) for corner in obj.bound_box)\n+ if not pts:\n+ raise RuntimeError(\"no mesh geometry imported\")\n+ lo = Vector((min(p.x for p in pts), min(p.y for p in pts), min(p.z for p in pts)))\n+ hi = Vector((max(p.x for p in pts), max(p.y for p in pts), max(p.z for p in pts)))\n+ center = (lo + hi) * 0.5\n+ radius = max((p - center).length for p in pts)\n+ return center, radius\n+\n+\n+def look_at(obj: bpy.types.Object, target: Vector, up: str) -> None:\n+ direction = target - obj.location\n+ obj.rotation_euler = direction.to_track_quat(\"-Z\", up).to_euler()\n+\n+\n+def render_view(name: str, direction: tuple[float, float, float], center: Vector, radius: float, up: str = \"Y\") -> None:\n+ for cam in [obj for obj in bpy.context.scene.objects if obj.type == \"CAMERA\"]:\n+ bpy.data.objects.remove(cam, do_unlink=True)\n+ cam_data = bpy.data.cameras.new(f\"camera_{name}\")\n+ cam_data.type = \"PERSP\"\n+ cam_data.angle = math.radians(34)\n+ cam_data.clip_start = 0.01\n+ cam_data.clip_end = 200\n+ cam = bpy.data.objects.new(f\"camera_{name}\", cam_data)\n+ bpy.context.collection.objects.link(cam)\n+ cam.location = center + Vector(direction).normalized() * (radius / math.tan(cam_data.angle / 2) * 1.18)\n+ look_at(cam, center, up)\n+ bpy.context.scene.camera = cam\n+ bpy.context.scene.render.filepath = str(OUT / f\"view_{name}.png\")\n+ bpy.ops.render.render(write_still=True)\n+\n+\n+def main() -> None:\n+ OUT.mkdir(parents=True, exist_ok=True)\n+ reset()\n+ bpy.ops.import_scene.gltf(filepath=str(GLB))\n+ setup_render()\n+ center, radius = bounds()\n+ for name, direction, up in [\n+ (\"front\", (0, 0, 1), \"Y\"),\n+ (\"back\", (0, 0, -1), \"Y\"),\n+ (\"left\", (-1, 0, 0), \"Y\"),\n+ (\"right\", (1, 0, 0), \"Y\"),\n+ (\"top\", (0, 1, 0.001), \"Z\"),\n+ (\"isometric\", (1, 0.55, 1), \"Y\"),\n+ ]:\n+ render_view(name, direction, center, radius, up)\n+ print(OUT)\n+\n+\n+if __name__ == \"__main__\":\n+ main()\n*** Add File: /tmp/opencode/n5ocamp_scene/make_contact_sheet.py\n+#!/usr/bin/env python3\n+from pathlib import Path\n+\n+import numpy as np\n+from PIL import Image, ImageDraw, ImageFont\n+\n+\n+ROOT = Path(__file__).resolve().parent\n+VALIDATION = ROOT / \"validation\"\n+OUT = VALIDATION / \"n5ocamp_all_sides.png\"\n+VIEWS = [\"front\", \"back\", \"left\", \"right\", \"top\", \"isometric\"]\n+\n+\n+def main() -> None:\n+ font_path = Path(\"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf\")\n+ font = ImageFont.truetype(str(font_path), 22) if font_path.exists() else ImageFont.load_default()\n+ panels = []\n+ for view in VIEWS:\n+ im = Image.open(VALIDATION / f\"view_{view}.png\").convert(\"RGBA\")\n+ a = np.array(im)\n+ lit = np.max(a[:, :, :3], axis=2) > 20\n+ if lit.any():\n+ ys, xs = np.where(lit)\n+ bbox = (int(xs.min()), int(ys.min()), int(xs.max() + 1), int(ys.max() + 1))\n+ else:\n+ bbox = None\n+ print(view, im.size, bbox, int(lit.sum()))\n+\n+ panel = Image.new(\"RGBA\", (450, 340), (0, 0, 0, 255))\n+ im.thumbnail((430, 285), Image.Resampling.LANCZOS)\n+ panel.alpha_composite(im, ((450 - im.width) // 2, 44 + (285 - im.height) // 2))\n+ draw = ImageDraw.Draw(panel)\n+ draw.text((16, 10), view.upper(), fill=(255, 255, 255, 255), font=font)\n+ panels.append(panel)\n+\n+ sheet = Image.new(\"RGBA\", (900, 1020), (0, 0, 0, 255))\n+ for i, panel in enumerate(panels):\n+ sheet.alpha_composite(panel, ((i % 2) * 450, (i // 2) * 340))\n+ sheet.save(OUT)\n+ print(OUT)\n+\n+\n+if __name__ == \"__main__\":\n+ main()\n*** Add File: /tmp/opencode/n5ocamp_scene/index.html\n+<!doctype html>\n+<html lang=\"de\">\n+ <head>\n+ <meta charset=\"utf-8\" />\n+ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n+ <title>N5OCAMP Five Tent 3D Scene</title>\n+ <script type=\"module\" src=\"https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js\"></script>\n+ <style>\n+ :root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, sans-serif; background: #000; color: #fff; }\n+ * { box-sizing: border-box; }\n+ body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 50% 0, #151515, #000 48rem); }\n+ main { width: min(1180px, calc(100vw - 32px)); margin: 0 auto; padding: 30px 0 44px; }\n+ header { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 20px; }\n+ h1 { margin: 0; font-size: clamp(26px, 4vw, 48px); letter-spacing: .14em; }\n+ p { color: #bbb; line-height: 1.5; }\n+ nav { display: flex; flex-wrap: wrap; gap: 10px; }\n+ a { color: #fff; }\n+ .button { border: 1px solid #ffffff45; border-radius: 999px; padding: 10px 14px; text-decoration: none; background: #ffffff12; }\n+ .stage, .panel { border: 1px solid #ffffff26; border-radius: 26px; background: #000; overflow: hidden; }\n+ model-viewer { display: block; width: 100%; height: min(72vh, 680px); min-height: 420px; background: #000; }\n+ .panel { margin-top: 18px; padding: 18px; }\n+ .panel img { max-width: 100%; height: auto; display: block; border-radius: 14px; background: #000; }\n+ .render img { max-width: 820px; }\n+ @media (max-width: 720px) { header { display: block; } nav { margin-top: 16px; } model-viewer { min-height: 360px; } }\n+ </style>\n+ </head>\n+ <body>\n+ <main>\n+ <header>\n+ <div>\n+ <h1>N5OCAMP</h1>\n+ <p>Clean rebuilt scene: five separate 3D tent forms on a black background.</p>\n+ </div>\n+ <nav aria-label=\"Downloads\">\n+ <a class=\"button\" href=\"n5ocamp_scene.glb\" download>GLB</a>\n+ <a class=\"button\" href=\"n5ocamp_scene.blend\" download>Blend</a>\n+ <a class=\"button\" href=\"n5ocamp_scene.obj\" download>OBJ</a>\n+ <a class=\"button\" href=\"n5ocamp_render.png\" download>Render</a>\n+ </nav>\n+ </header>\n+ <section class=\"stage\">\n+ <model-viewer src=\"n5ocamp_scene.glb\" alt=\"N5OCAMP five tent 3D model\" camera-controls auto-rotate auto-rotate-delay=\"1000\" rotation-per-second=\"18deg\" shadow-intensity=\"0\" exposure=\"1\" environment-image=\"neutral\"></model-viewer>\n+ </section>\n+ <section class=\"panel render\">\n+ <strong>Front render</strong>\n+ <img src=\"n5ocamp_render.png\" alt=\"N5OCAMP front render\" />\n+ </section>\n+ <section class=\"panel\">\n+ <strong>All-side GLB validation</strong>\n+ <p>The exported GLB was re-imported and rendered from front, back, left, right, top and isometric views.</p>\n+ <img src=\"validation/n5ocamp_all_sides.png\" alt=\"All-side validation renders\" />\n+ </section>\n+ </main>\n+ </body>\n+</html>\n*** Add File: /tmp/opencode/n5ocamp_scene/README.md\n+# N5OCAMP Five-Tent Scene\n+\n+Clean rebuild from scratch.\n+\n+Render/export:\n+\n+```bash\n+blender --background --python /tmp/opencode/n5ocamp_scene/build_n5ocamp_scene.py\n+```\n+\n+Validate exported GLB from all sides:\n+\n+```bash\n+blender --background --python /tmp/opencode/n5ocamp_scene/validate_scene.py\n+python3 /tmp/opencode/n5ocamp_scene/make_contact_sheet.py\n+```\n+\n+Outputs:\n+\n+- `n5ocamp_render.png`\n+- `n5ocamp_scene.glb`\n+- `n5ocamp_scene.blend`\n+- `n5ocamp_scene.obj`\n+- `validation/n5ocamp_all_sides.png`\n*** End Patch"
}