Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
843e190cd6 | ||
|
|
aa20cbf1e0 | ||
|
|
aad5c040c2 | ||
|
|
fd7931370f | ||
|
|
965f404171 |
No files matched your search
@@ -606,7 +606,7 @@ Toy Master doesn't request a model after that (it uses its own), so the glb load
|
||||
**Vader Immortal (GitHub #49):** stuck on its loading card and leaked GPU mappings (solved below: `gl_error_clear`); diagnostics FrameBridge `layer_debug`, `strip_color_bias`, `frame_balance`, `snapshot=N` (`snapshot_gl.c`: saves the submitted left-eye image as `files/fb_snap_*.ppm`). → docs/DEVICE_NOTES.md#vader-immortal-headless-investigation
|
||||
**SteamVR overlay apps:** host OpenVR overlays composite over Lepton games; detection `analysis/vroverlay.py`, patch `pcvr.vr_overlay`, Linux fields `vr_overlay`/`vr_overlay_autostart`, agent `register_vr_overlay`/`unregister_vr_overlay`/`launch_vr_overlay`; SteamVR on the Frame reads only `binary_path_linux_arm`; FramePort's own autostart (`ensure_overlay_service`, `frameport-vr-overlays.service`). See also docs/FRAME_RUNTIME.md "Overlay apps". → docs/DEVICE_NOTES.md#steamvr-overlay-apps
|
||||
**Vader's leak solved / AC Nexus flips:** stale GL errors broke SteamVR's GL interop → adapter `gl_error_clear` (GLES, catalog for Vader ep1-3); read `xrclient_<pkg>.txt` in Steam's logs for runtime errors; the runtime caps swapchains per session (~15) → flip emulation in place; adapter `flip_dump`. → docs/DEVICE_NOTES.md#vaders-leak-solved--ac-nexus-flips
|
||||
**Mixed reality: scanned rooms (branch mr-scene):** agent `room_capture` records Valve's RoomView 3D depth mesh -> `fp_roomscan` -> `room.txt` + `room-mesh.bin` (docs/mr/ROOM_FORMAT.md; refined by default: the owner's cleanup passes + label rules in `native/roomscan/refine.c`/`rules.c`, unrefined output kept as `room-raw.txt` + `room-mesh-raw.bin`, head path/settings/report/labels/door suggestions in `scan.json`, `--refine-only <dir>` re-runs it, `--refine 0` = old output; docs/mr/scan.md "Refinement") in `~/.local/share/frameport/rooms/<universe>/`, copied next to settings.conf (`_room_sync`); FrameBridge (`scene_room.c/scene_meta.c/scene_anchors.c`, all through xrshim) serves walls, volumes, planes, the room mesh, META discovery/persistence, saved anchors and `XR_META_boundary_visibility`; `scene_emul` + `frame.meta_permissions` for every USE_SCENE/USE_ANCHOR_API game (migrations `scene_emul_all`, `scene_permissions`); per-game `scene_hide_bounds` (Demeter). → docs/DEVICE_NOTES.md#mixed-reality-scanned-rooms
|
||||
**Mixed reality: scanned rooms (branch mr-scene):** agent `room_capture` records Valve's RoomView 3D depth mesh -> `fp_roomscan` -> `room.txt` + `room-mesh.bin` (docs/mr/ROOM_FORMAT.md) in `~/.local/share/frameport/rooms/<universe>/`, copied next to settings.conf (`_room_sync`); FrameBridge (`scene_room.c/scene_meta.c/scene_anchors.c`, all through xrshim) serves walls, volumes, planes, the room mesh, META discovery/persistence, saved anchors and `XR_META_boundary_visibility`; `scene_emul` + `frame.meta_permissions` for every USE_SCENE/USE_ANCHOR_API game (migrations `scene_emul_all`, `scene_permissions`); per-game `scene_hide_bounds` (Demeter). → docs/DEVICE_NOTES.md#mixed-reality-scanned-rooms
|
||||
**Unreal's self-restart:** UE 4.21+ restarts itself after the first shader precompile and Lepton ends the container → launch.sh RESTART_BLOCK starts Lepton once more (`upgrade_launchers`; `FRAMEPORT_NO_RESTART=1` off). → docs/DEVICE_NOTES.md#unreals-self-restart
|
||||
**xrWaitFrame runs ahead:** the runtime sometimes returns a display time <1 refresh after the last submit and rejects the second submission (`VRCompositorError_AlreadySubmitted`) → the VrApi bridge re-waits (`pacing 2s:` counters). → docs/DEVICE_NOTES.md#xrwaitframe-runs-ahead-journey-of-the-gods
|
||||
**Lepton's logcat mirror dies** (EOF, read failure, or silent) → launch.sh `_logcat_keeper` reads `podman exec <container> logcat` itself into launch.log (log `<base>/logcat-keeper.log`). → docs/DEVICE_NOTES.md#leptons-logcat-mirror-dies
|
||||
|
||||
@@ -36,9 +36,6 @@ Frame, patches each game so it runs there, uploads it and adds it to your Steam
|
||||
in FramePort.
|
||||
- **Your Frame from your PC:** Type on Frame (your keyboard on the Frame), Files, Screenshots, Live view (what the
|
||||
headset shows, in your browser) and Monitor (frame rate, temperatures, battery, processes).
|
||||
- **Your room for mixed reality (experimental):** scan the room in the headset, see it in 3D, clean up the scan and
|
||||
fix furniture, screen and door labels, so mixed-reality games get your room. Try it with
|
||||
[Test Quest MR](https://github.com/spoopyghosty0/TestQuestMR).
|
||||
- **Updates itself**, and reports problems without personal data.
|
||||
|
||||
Details for each: [Install and first steps](docs/INSTALL.md).
|
||||
@@ -47,8 +44,6 @@ Details for each: [Install and first steps](docs/INSTALL.md).
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Quick start
|
||||
|
||||
1. [Download](https://github.com/spoopyghosty0/frameport/releases/latest) the build for Windows, macOS (Apple
|
||||
|
||||
@@ -27,7 +27,7 @@ b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING
|
||||
6cda3076a8c09b31bb1f90da614ea89e7ee194589626eeac613e522849604e47 ./licenses/LuaJIT.txt
|
||||
1aa733117cf57ccff7cf23f0425dbfd73b0332a3ef1c905ceeca0ccd0449c4e0 ./linux-arm64/XR_APILAYER_FRAMEPORT_timefix.json
|
||||
28c2430a02bbd8902c5bfb9562c6fd0e318654f9e05c095bfeb94b0450ab1b07 ./linux-arm64/libxr_frameport_timefix.so
|
||||
2cc16b90abcd3d359f1658c581172bd650017bf2725e24268915f50904c000ec ./linux-arm64-bin/fp_roomscan
|
||||
63af0050054ad3705b17a0b00ce0c076ba1af0b73abd673ba6c588f64b4591cd ./linux-arm64-bin/fp_roomscan
|
||||
b10b3a5c10c3339fc63fd9f6cb4d01ea29be11a863d0ebad1389ea69fd471940 ./linux-arm64-bin/fp_venc
|
||||
00c2dead0fd7497929833467f0e55699d57d26be1b85dce35402993852f7b111 ./roomscan/ceiling.png
|
||||
c1962de31ed2810d31c760cf9ee63316ac2b8c9528ccbcd9299ebf5ba39b2995 ./roomscan/done.png
|
||||
|
||||
@@ -202,10 +202,6 @@ Mixed-reality games ask for your room: walls, furniture and its surfaces. **Room
|
||||
the scan again (**Back to the scan's boxes** under Labels drops them).
|
||||
- The free **Test Quest MR** app (Get Test Quest MR on the tab) shows what a game receives from your scan.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Updating
|
||||
|
||||
FramePort checks for a new version at start and every 6 hours. When there is one, the Library shows **Update now**:
|
||||
|
||||
@@ -117,10 +117,6 @@ Hooks (`steps.HOOKS`) cover what a real Frame would do:
|
||||
- `pairing_done`: the Frame ran the setup command, so FramePort connects.
|
||||
- `live_stream`: a test picture through the real relay.
|
||||
- `monitor_details`, `type_tab`, `select_files` and `settings_section`.
|
||||
- `open_room` (the Room tab with the pretend Frame's room drawn) and `room_editing` (its editing cards: `cleanup`,
|
||||
`more`, `box`, `doors`, `suggestion`). The room is a made-up bedroom built in code (`fakes.fake_room`, never a real
|
||||
scan): a bed under a wall-mounted TV, a desk with two monitors under a window, a closet, a door, a lamp and a plant,
|
||||
with a noisy scanned-surface mesh and a clean-up report and labels that match it.
|
||||
|
||||
In a video, clicking Install runs the scripted install, and the game is then installed on the pretend Frame.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 425 KiB After Width: | Height: | Size: 421 KiB |
|
Before Width: | Height: | Size: 506 KiB After Width: | Height: | Size: 505 KiB |
|
Before Width: | Height: | Size: 665 KiB After Width: | Height: | Size: 663 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 599 KiB |
|
Before Width: | Height: | Size: 596 KiB After Width: | Height: | Size: 594 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 89 KiB |
@@ -65,39 +65,8 @@ float32 xyz[vertex_count], u32 indices[index_count] (triangles; counter-clockw
|
||||
```
|
||||
Coordinates in STAGE space. Kept below 200k triangles (decimated).
|
||||
|
||||
## Raw files and scan.json (scanner sidecars)
|
||||
`fp_roomscan` refines its output by default (docs/mr/scan.md "Refinement"). `room.txt` + `room-mesh.bin` are always
|
||||
the refined room (what games and the GUI read; the contract above is unchanged). Next to them:
|
||||
- `room-raw.txt` + `room-mesh-raw.bin`: the scanner's unrefined output in the same formats (its `mesh` record names
|
||||
`room-mesh-raw.bin`). Same UUIDs as the refined room.txt, so anchors stay the same when the room is refined again.
|
||||
The refined room differs in: the ceiling height (re-estimated from the scan, wall tops follow it), the volumes
|
||||
(after the label rules) and the mesh. Records the scanner doesn't model (e.g. `plane`) are copied verbatim.
|
||||
- `scan.json` (UTF-8 JSON, `version` 1):
|
||||
```
|
||||
{"version": 1,
|
||||
"heads": [[x, y, z], ...], # head path of the recording (STAGE), <= 2000 points, decimated evenly
|
||||
"refine": {"refine": 1, "walls": "panels", "band": 0.35, ..., "erase": [[x, y, z, r], ...]},
|
||||
# every refinement setting used (keys = the CLI options, docs/mr/scan.md)
|
||||
"report": {"triangles", "kept", "structural", "near_walls", "outside", "small_pieces", "thin_pieces",
|
||||
"erased", "walked", "tops", "panel_triangles", "truncated", "walked_skipped", "floor_y",
|
||||
"ceiling_y", "boxes", "relabelled", "removed_boxes", "suggestions",
|
||||
"plane_shifts": [{"kind": "wall"|"floor"|"ceiling", "wall": i, "shift": m}, ...]},
|
||||
"labels": [{"from": "COUCH", "to": "TABLE"|null, "reason": "top 0.69 m", "suggest": "door"|"closet" (optional),
|
||||
"id": "<volume uuid>", "box": {"cx", "cz", "top_y", "yaw_deg", "w", "d", "h"}}, ...],
|
||||
# one per scanner volume, in room-raw.txt order; to = null: removed
|
||||
"suggestions": [{"kind": "door"|"closet", "box": {...}, "wall": i, "along": t, "reason": "..."}, ...]}
|
||||
# tall thin slabs; never applied by the scanner. wall = index of the nearest
|
||||
# wall record in room-raw.txt, along = 0..1 position on it (from x0,z0)
|
||||
```
|
||||
`report` counts triangles of the raw mesh per pass (`panel_triangles` are added ones), `wall` in `plane_shifts`
|
||||
indexes the wall records. Readers ignore unknown keys.
|
||||
- `fp_roomscan --refine-only <dir>` re-runs only the refinement from these files (an older folder without them:
|
||||
its room.txt + mesh are copied to the raw names first). Publishing order: [raw mesh, raw text,] `scan.json`,
|
||||
`room-mesh.bin`, `room.txt`.
|
||||
|
||||
## Where the files live
|
||||
- Frame: `~/.local/share/frameport/rooms/<universe>/room.txt` + `room-mesh.bin` (+ `scan.log`, the raw files and
|
||||
`scan.json` above).
|
||||
- Frame: `~/.local/share/frameport/rooms/<universe>/room.txt` + `room-mesh.bin` (+ `scan.log`).
|
||||
`~/.local/share/frameport/rooms/current` = symlink to the newest room's folder.
|
||||
- Per game: launch.sh copies the current universe's room (else `current`) to `<app_dir>/room.txt` (+ mesh) before
|
||||
Lepton starts, next to `settings.conf`; FrameBridge reads `dirname($FRAMEBRIDGE_CONFIG)/room.txt`. No room ->
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
`fp_roomscan` converts recorded FPRS v1 meshes to the [room contract](ROOM_FORMAT.md), including the optional
|
||||
wall label adopted from upstream commit `8cb2783`:
|
||||
`room.txt` and `room-mesh.bin`, refined by default (see "Refinement" below), plus the unrefined `room-raw.txt` +
|
||||
`room-mesh-raw.bin` and `scan.json` (head path, settings, per-pass report, label decisions, door/closet
|
||||
suggestions; ROOM_FORMAT.md "Raw files and scan.json"). It does not capture RoomView queues, contact a headset, install rooms, or modify
|
||||
`room.txt` and `room-mesh.bin`. It does not capture RoomView queues, contact a headset, install rooms, or modify
|
||||
the current-room symlink. Scanner work changes `native/roomscan/`, its build target and artifact/checksum,
|
||||
`scripts/mr/synth_scan.py`, `tests/test_roomscan.py`, and this note. The upstream contract commit also brings its
|
||||
existing prompt assets and renderer unchanged. No runtime, agent, patch, or analysis versions were bumped.
|
||||
@@ -19,7 +17,7 @@ python native/build.py --only roomscan
|
||||
|
||||
The result is `artifacts/linux-arm64-bin/fp_roomscan`, a static aarch64 Linux ELF with its own `_start`, raw Linux
|
||||
syscalls, and no interpreter, dynamic section, libc, libm, UAPI headers, generated tables, or new dependencies.
|
||||
`platform.h` is the reconstruction code's entire I/O/allocation boundary. The same eight C sources compile on the
|
||||
`platform.h` is the reconstruction code's entire I/O/allocation boundary. The same five C sources compile on the
|
||||
host:
|
||||
|
||||
```sh
|
||||
@@ -36,13 +34,8 @@ exactly one JSON summary, whether `--json` is supplied or omitted. Success exits
|
||||
{"ok":true,"frames":30,"skipped_frames":0,"far_vertices":0,"discontinuity_triangles":0,
|
||||
"walls":4,"detected_walls":4,"volumes":2,"floor_y":-0.0023,
|
||||
"ceiling_y":2.5992,"coverage_percent":100.0,"hint_used":false,"triangles":70997,
|
||||
"accepted_triangles":118804,"rejected_triangles":704,"heads":28,
|
||||
"refine":{"triangles":70997,"kept":7159,"structural":46121,"near_walls":148,"outside":0,"small_pieces":38,
|
||||
"thin_pieces":0,"erased":0,"walked":203,"tops":1896,"panel_triangles":12,"truncated":0,"walked_skipped":false,
|
||||
"floor_y":-0.0051,"ceiling_y":2.6028,"boxes":2,"relabelled":1,"removed_boxes":0,"suggestions":0,
|
||||
"plane_shifts":[{"kind":"wall","wall":0,"shift":-0.0004},...,{"kind":"ceiling","shift":-0.0021}],
|
||||
"output_triangles":7171,"output_volumes":2},"warnings":[],
|
||||
"timings_ms":{"read":0,"fusion":0,"model":0,"mesh":0,"refine":0,"total":0}}
|
||||
"accepted_triangles":118804,"rejected_triangles":704,"warnings":[],
|
||||
"timings_ms":{"read":0,"fusion":0,"model":0,"mesh":0,"total":0}}
|
||||
```
|
||||
|
||||
The example above illustrates the schema; actual counts and timings depend on the input. Heights are absolute
|
||||
@@ -50,10 +43,7 @@ STAGE coordinates. `frames` includes skipped frames; `skipped_frames`, `far_vert
|
||||
`discontinuity_triangles` count input exclusions once, not once per pass. `walls` counts output polygon
|
||||
edges including synthesized edges; `detected_walls` counts fitted physical planes. Coverage means the percentage
|
||||
of the output perimeter supported by observed wall planes, **not camera coverage of all room surfaces**. Timings
|
||||
include parsing, fusion, structural/furniture modelling, mesh generation, refinement, and total time including
|
||||
output. The scanner fields describe the unrefined scan (`triangles`, `volumes`, `ceiling_y` are the raw files');
|
||||
`refine` is the refinement report (same keys as scan.json's `report`) plus the refined `output_triangles` and
|
||||
`output_volumes`. `heads` counts the head-path points kept for the walked-volume pass.
|
||||
include parsing, fusion, structural/furniture modelling, mesh generation, and total time including output.
|
||||
|
||||
All frame headers, timestamps/vertices/poses, approximately unit quaternions, counts, indices and EOF boundaries
|
||||
are checked before publication. A non-finite timestamp/pose/vertex or invalid tracking quaternion skips the entire
|
||||
@@ -67,10 +57,9 @@ triangles fails. Empty frames may accompany useful frames. Structural corruption
|
||||
bad counts or indices) remains fatal, preserves any existing output byte for byte and creates no output directory.
|
||||
UUIDs come from `getrandom`, fresh per scan; mesh geometry is repeatable.
|
||||
|
||||
All files are computed first, then each is written to an exclusive, randomly named temporary file and renamed, in
|
||||
the order `room-mesh-raw.bin`, `room-raw.txt`, `scan.json`, `room-mesh.bin`, `room.txt` (meshes before their room
|
||||
text, `room.txt` last). A write failure removes the temporary files. These are atomic **file** replacements, not a
|
||||
multi-file filesystem transaction: the installer should scan into a fresh staging directory
|
||||
Both complete files are written to exclusive, randomly named temporary files, then individually renamed, with
|
||||
the mesh first and `room.txt` last. A write failure removes the temporary files. These are atomic **file**
|
||||
replacements, not a two-file filesystem transaction: the installer should scan into a fresh staging directory
|
||||
and publish that directory/symlink after success. Do not run concurrent writers into the same output directory.
|
||||
There is no fsync-based power-loss durability promise. Neither scan nor build writes application data outside
|
||||
the worktree's `FRAMEPORT_HOME`; fixtures and test output use temporary directories.
|
||||
@@ -122,77 +111,6 @@ the worktree's `FRAMEPORT_HOME`; fixtures and test output use temporary director
|
||||
excess beyond that fails. Vertices are unshared triangle vertices, so at most 600,000 vertices. Structural
|
||||
planes are semantic anchors, not substituted for the observed global mesh.
|
||||
|
||||
## Refinement
|
||||
|
||||
After the mesh, the scanner cleans the room the way the owner tuned it in the room-scan cleanup page (2026-10-18);
|
||||
`native/roomscan/refine.c` and `rules.c` port that page's `refine()` and label rules pass for pass (local parity
|
||||
check on the owner's recording: identical counts and bit-identical refined meshes, see below). `--refine 0`
|
||||
switches it off: room.txt and room-mesh.bin are then byte-for-byte what the scanner wrote before refinement existed
|
||||
(the raw files and scan.json are still written, so refinement can be switched on later without a new scan).
|
||||
|
||||
The input is exactly what the raw files hold: the raw room is written as text and read back (doubles parsed from the
|
||||
4-decimal text, like the page reads it), the head path is rounded the same way. Therefore `--refine-only <dir>`
|
||||
on the raw files reproduces the scan's refined files byte for byte. The mesh is welded at 1 mm (quantized, as the
|
||||
page does) for the passes that need topology; the output is again unshared triangle vertices.
|
||||
|
||||
1. **Label rules** (`rules.c`, `--label-rules 0` off) on the scanner's volumes, using the raw room (walls, outline,
|
||||
floor): a box that holds >= 60% of two boxes whose centres are >= 1 m apart covers several things (removed); a
|
||||
box >= 75% inside a bigger box with a top no higher (+5 cm) is part of it (removed); a centre > 10 cm outside the
|
||||
outline is behind a wall (removed). A tall thin slab from the floor (thinner than 22 cm, >= 1.8 m high, bottom
|
||||
<= 0.3 m, at most 0.5 m behind the wall line) becomes a door (<= 1.1 m wide) or closet **suggestion** in
|
||||
scan.json (box removed, nothing added: the GUI decides). Thin (< 22 cm) upright boxes are SCREEN only when
|
||||
raised >= 0.3 m, top <= 2.2 m, width/height >= 1.2, within 15 degrees of the nearest wall (when closer than
|
||||
0.35 m) and flat: the raw mesh's depth spread inside the box <= 3 cm (with >= 30 triangles; fewer = unknown,
|
||||
accepted); otherwise removed. Then BED (top 0.25-0.65 m, area >= 2 m^2, long side 1.8-3 m), COUCH (top
|
||||
0.35-0.6 m, long side >= 1.4 m), TABLE (top 0.6-1.1 m), STORAGE (>= 1.1 m high, >= 25 cm deep, within 0.35 m of a
|
||||
wall); anything taller than 1.1 m otherwise is removed, the rest is OTHER. Every box gets a reason in scan.json.
|
||||
These thresholds are constants in `rules.c` (the page's `RULES0`).
|
||||
2. **Furniture**: triangles within 10 cm of a ruled box (after the rules) are furniture; the passes below never
|
||||
classify, clear or walk them away, and the tubes pass keeps pieces that are mostly furniture.
|
||||
3. **Planes**: seen walls (`WALL_FACE` records), floor and ceiling. Each plane's offset moves to the median signed
|
||||
distance of the non-furniture triangles facing it (normal within `--facing-deg`) within `--band` of it (walls:
|
||||
within their length + band), when there are more than 50. An unseen ceiling (floor + 2.5 m) moves to the real
|
||||
one; the re-estimated ceiling is written to the refined room.txt (ceiling record and wall tops).
|
||||
4. **Structural triangles**: facing a plane, not furniture, behind it by less than `--band`, in front of it by less
|
||||
than `--band-in` (walls), `--floor-in`, `--ceil-in`; in a wall's top `--top-strip` below the ceiling the full
|
||||
band applies in front too (the depth bends where wall meets ceiling). The nearest qualifying plane owns it.
|
||||
5. **Erase spheres** (`--erase "x,y,z,r;..."`, from the GUI's eraser) drop every triangle whose centroid is inside.
|
||||
6. **Walked volume** (`--walked` = radius R, 0 off): where the user stood, nothing solid can be. Non-furniture
|
||||
triangles from 0.2 m above the floor up to 0.25 m above a head position, within R horizontally of it, go.
|
||||
Needs the head path (scan.json); without it the pass is skipped (`walked_skipped`).
|
||||
7. **Walls** (`--walls`): `panels` (default) removes the structural triangles, then non-furniture triangles closer
|
||||
than `--near` to any plane (within the wall's extent), and adds flat panels: one quad per seen wall (moved by its
|
||||
offset shift) from floor to ceiling, and the outline ear-clipped at the floor and the ceiling heights, all wound
|
||||
counter-clockwise seen from inside. `snap` instead projects every vertex of a structural triangle onto its plane
|
||||
(the last owner wins). `off` keeps them.
|
||||
8. **Furniture tops** (`--tops`, 0 off): upward-facing (normal y > 0.8) triangles within `--tops` of a box top and
|
||||
inside its footprint are flattened onto it.
|
||||
9. **Clip**: centroids further than `--clip` outside the outline (`--clip` + `--clip-open` past an unseen wall), or
|
||||
further than `--clip` above the ceiling / below the floor plane go. `--clip 1 --clip-open 2` switches it off.
|
||||
10. **Islands**: connected pieces smaller than `--island` m^2 go (`--protect 1` keeps furniture triangles).
|
||||
11. **Thin floating pieces** (`--tubes`, 0 off): pieces of >= 10 triangles, < 1 m^2, at most half furniture, whose
|
||||
longest principal axis (vertex PCA, std) is >= `--tubes` times the second and the second <= `--tube-thick`.
|
||||
12. **Smoothing**: `--smooth` Taubin iterations (lambda 0.5, mu -0.53) over the kept triangles.
|
||||
|
||||
Output: the kept scan triangles, then the panels (`refine.panel_triangles`), at most 200,000 triangles (more kept
|
||||
triangles would be cut, `truncated`, with a warning). The refinement adds about 20 ms on the host for the owner's
|
||||
64k-triangle scan; its buffers (welding hash, double positions, union-find, adjacency) stay below ~170 MiB for a
|
||||
200k-triangle mesh and the fusion grid is freed before it runs, so peak memory is still the fusion phase's.
|
||||
|
||||
`--refine-only <room dir> [refinement options] [--json]` runs only steps 1-12 from `room-raw.txt` +
|
||||
`room-mesh-raw.bin` (+ the head path from `scan.json`) and rewrites scan.json, room-mesh.bin and room.txt. A folder
|
||||
from an older scanner without raw files: its room.txt + mesh become the raw files first (the room text is rewritten
|
||||
with the mesh record pointing at `room-mesh-raw.bin`), a warning says so, and there is no head path. Its JSON:
|
||||
`{"ok":true,"refine_only":true,"raw_triangles":..,"volumes":..,"floor_y":..,"ceiling_y":..,"heads":..,
|
||||
"refine":{...},"warnings":[...],"timings_ms":{"refine":..,"total":..}}`.
|
||||
|
||||
Local parity (2026-10-18, private recording `room1.fprs`, not in the repository): the reference page's functions run
|
||||
under node on this scanner's raw output give the same per-pass counts, the same label decisions and reasons, and
|
||||
bit-identical refined meshes for the defaults, snap, off + protect, erase spheres + tubes 2, and a wide-band setting
|
||||
(one triangle differs there: a facing test exactly at the threshold), and the same on an older 128k-triangle room
|
||||
folder. Distance ties at outline corners decide which wall's clip allowance applies, so the corner distance uses
|
||||
V8's Math.hypot algorithm.
|
||||
|
||||
Peak allocation stays below 260 MiB, including maximum frame buffers, the grid, model worklists and mesh buffers
|
||||
(the latter phases do not retain frame buffers). Default synthetic scenes use a much smaller grid. Thresholds
|
||||
are CLI options below or named constants next to their algorithms. No room-specific coordinates or dimensions are
|
||||
@@ -225,31 +143,6 @@ Distances are metres, areas m², volumes m³. `--universe` accepts the full unsi
|
||||
| `--gap` | 0.65 | 0.1–1.5, maximum adjacent-wall endpoint extrapolation |
|
||||
| `--horizontal` | 0.85 | 0.6–0.99, minimum floor/ceiling/top normal cosine |
|
||||
|
||||
Refinement (all distances metres; defaults = the owner's settings):
|
||||
|
||||
| Option | Default | Accepted range / meaning |
|
||||
| --- | ---: | --- |
|
||||
| `--refine` | 1 | 0/1; 0 = the unrefined output in room.txt/room-mesh.bin |
|
||||
| `--walls` | panels | `off`, `snap` (project onto the planes) or `panels` (flat wall/floor/ceiling panels) |
|
||||
| `--band` | 0.35 | 0.02–1, structural band behind a surface; plane re-estimation window |
|
||||
| `--facing-deg` | 60 | 5–89, maximum angle between a triangle's normal and the surface's |
|
||||
| `--band-in` | 0.12 | 0–1, band in front of a wall (room side) |
|
||||
| `--floor-in` | 0.08 | 0–1, band above the floor |
|
||||
| `--ceil-in` | 0.25 | 0–1, band below the ceiling |
|
||||
| `--top-strip` | 0.5 | 0–2, top part of a wall where the full band applies in front too |
|
||||
| `--near` | 0.03 | 0–0.3, panels: also clear non-furniture scan this close to a plane |
|
||||
| `--clip` | 0.05 | 0–1, clip distance outside the outline / beyond floor and ceiling |
|
||||
| `--clip-open` | 0.10 | 0–2, extra allowance past unseen walls (`--clip 1 --clip-open 2` = no clipping) |
|
||||
| `--island` | 0.20 | 0–5 m^2, minimum connected piece area (0 off) |
|
||||
| `--smooth` | 8 | 0–50 integer, Taubin iterations |
|
||||
| `--tops` | 0.10 | 0–0.5, flatten upward scan this close to a box top (0 off) |
|
||||
| `--protect` | 0 | 0/1, islands never remove furniture triangles |
|
||||
| `--tubes` | 3 | 0–20, elongation (PCA axis ratio) of a thin floating piece (0 off) |
|
||||
| `--tube-thick` | 0.15 | 0.01–1, maximum second PCA axis (std) of a thin floating piece |
|
||||
| `--walked` | 0.40 | 0 or 0.05–1.5, walked-volume radius around the head path (0 off) |
|
||||
| `--label-rules` | 1 | 0/1, label rules on the scanner's volumes |
|
||||
| `--erase` | none | `"x,y,z,r;..."`, up to 64 spheres (r 0–5 m) whose scan is removed |
|
||||
|
||||
Additional constants in `model.c` include RANSAC trials, normal agreement, wall span, furniture margins,
|
||||
label height/area thresholds, box trimming and circle recognition. Fusion sampling/weight limits are in
|
||||
`fusion.c`; mesh splat radii/decimation limits in `mesh.c`; hard resource limits in `scan.h`.
|
||||
@@ -281,17 +174,10 @@ Incompatible inset play boundaries are ignored with a warning. Hint-completed ed
|
||||
|
||||
## Offline validation
|
||||
|
||||
`tests/test_roomscan.py` builds with the host compiler (skips only if none exists). Its tests cover the noisy
|
||||
`tests/test_roomscan.py` builds with the host compiler (skips only if none exists). Its 41 tests cover the noisy
|
||||
4 x 3 x 2.6 m room with table/couch, six-wall L, moving and rotated/translated mesh poses, ceiling fallback, a single
|
||||
frame, missing walls, circular/matching hints, correct UUIDs/wire layout, interior mesh winding, triangle budget,
|
||||
repeatable mesh bytes with fresh UUIDs, truncated/corrupt dumps, invalid counts/indices/poses and invalid CLI values.
|
||||
Refinement tests: a floating pole and a column in the walked path (`synth_scan.py` `TUBE`/`GHOST`, `extra=`)
|
||||
are meshed by the scanner and removed by the tubes / walked passes (each kept with its pass off, the table top kept);
|
||||
panels wind into the room on the re-estimated planes; `--refine-only` reproduces a scan's files byte for byte;
|
||||
`--refine 0` equals the raw output (and refining it later equals the default); scan.json settings/report/heads; head
|
||||
path decimation to <= 2000 points; erase spheres; the label rules on a hand-written older room folder (merged,
|
||||
nested, behind a wall, SCREEN from a flat panel, a door suggestion with wall and position, unknown records kept,
|
||||
idempotent, `--label-rules 0`); invalid refinement options.
|
||||
The test module takes about fourteen seconds including synthetic rendering and compilation (under the 60 s budget).
|
||||
New cases cover non-finite frames before/after usable frames, per-vertex far rejection, depth jumps below the edge
|
||||
limit, weak ceiling evidence, wall labels and a complete camera-fan scan. The deterministic stdlib renderer now
|
||||
@@ -307,7 +193,7 @@ UV_OFFLINE=1 UV_CACHE_DIR=/tmp/frameport-uv-cache uv run ruff check scripts/mr/s
|
||||
```
|
||||
|
||||
The synthetic camera fan passes AddressSanitizer + UndefinedBehaviorSanitizer. Two consecutive final native builds
|
||||
produce identical SHA-256 `2cc16b90abcd3d359f1658c581172bd650017bf2725e24268915f50904c000ec` (97,808 bytes, with refinement),
|
||||
produce identical SHA-256 `0d88e5adfa1a21da7e0cfec2698a835d7767961e15c0b41c84235fa24ae1e751` (54,248 bytes),
|
||||
matching `artifacts/SHA256SUMS`. ELF inspection confirms aarch64, little-endian, no PT_INTERP / PT_DYNAMIC, and no
|
||||
embedded local build paths.
|
||||
|
||||
|
||||
@@ -108,21 +108,3 @@ shots:
|
||||
steps:
|
||||
- go: settings
|
||||
- wait_for: "Portal (OLED)"
|
||||
|
||||
- name: room # experimental: the pretend Frame's made-up bedroom scan (fakes.fake_room), no real scan
|
||||
docs: [README.md, docs/INSTALL.md]
|
||||
viewport: [1280, 1690]
|
||||
steps:
|
||||
- hook: open_room
|
||||
- wait_for: "Furniture & screens"
|
||||
- park: true
|
||||
|
||||
- name: room-editing # a box picked in the list (its type, place and size) next to the clean-up's labels
|
||||
docs: [docs/INSTALL.md]
|
||||
viewport: [1280, 2000] # (tall enough that nothing scrolls)
|
||||
steps:
|
||||
- hook: open_room
|
||||
- wait_for: "Furniture & screens"
|
||||
- hook: {name: room_editing, args: [box]}
|
||||
- park: true
|
||||
crop: {anchor: "Furniture & screens", left: -26, top: -28, width: 670, height: 1262}
|
||||
@@ -209,8 +209,8 @@ def build_roomscan(tc: Path):
|
||||
run([tc / "bin/clang", "--target=aarch64-linux-gnu", "-ffreestanding", "-nostdlib", "-nostdlibinc", "-static",
|
||||
"-fno-stack-protector", "-fno-builtin", "-ffp-contract=off", "-fno-math-errno", "-O2", "-Wall", "-Wextra", "-Werror",
|
||||
"-fuse-ld=lld", "-Wl,--build-id=none", "-Wl,-z,max-page-size=65536", "-e", "_start",
|
||||
f"-ffile-prefix-map={HERE}=native", "main.c", "fusion.c", "model.c", "mesh.c", "refine.c", "rules.c",
|
||||
"roomio.c", "platform.c", "-o", out / "fp_roomscan"], cwd=src)
|
||||
f"-ffile-prefix-map={HERE}=native", "main.c", "fusion.c", "model.c", "mesh.c", "platform.c",
|
||||
"-o", out / "fp_roomscan"], cwd=src)
|
||||
|
||||
|
||||
OCULUSHMD_IMPORTS = ("CreateEventW", "CreateJobObjectW", "AssignProcessToJobObject", "QueryInformationJobObject",
|
||||
|
||||
@@ -52,19 +52,6 @@ void warn(Scan *s, const char *m)
|
||||
for (int i = 0; i < s->nwarnings; i++) if (same(s->warnings[i],m)) return;
|
||||
if (s->nwarnings < 24) s->warnings[s->nwarnings++] = m;
|
||||
}
|
||||
/* The head path (where the user stood and walked) for the refinement's walked-volume pass. Bounded: every
|
||||
* head_stride-th tracked frame, halving the kept points and doubling the stride when the buffer is full. */
|
||||
static void record_head(Scan *s, Vec eye)
|
||||
{
|
||||
if(!s->head_stride) s->head_stride=1;
|
||||
if(s->head_frames++%s->head_stride) return;
|
||||
if(s->nheads==MAX_HEADS) {
|
||||
for(int i=0;i<MAX_HEADS/2;i++) s->heads[i]=s->heads[2*i];
|
||||
s->nheads=MAX_HEADS/2;s->head_stride*=2;
|
||||
if((s->head_frames-1)%s->head_stride) return;
|
||||
}
|
||||
s->heads[s->nheads++]=eye;
|
||||
}
|
||||
static u32 u32le(const u8 *p) { return (u32)p[0]|(u32)p[1]<<8|(u32)p[2]<<16|(u32)p[3]<<24; }
|
||||
static float f32le(const u8 *p) { union { u32 u; float f; } v = {u32le(p)}; return v.f; }
|
||||
/* 1 = complete, 0 = clean EOF, -1 = I/O error or partial record. */
|
||||
@@ -220,7 +207,6 @@ const char *read_dump(Scan *s, int fuse)
|
||||
for (int i = 3; i < 7; i++) norm += pose[i]*pose[i];
|
||||
if (!finite(norm) || ab(norm-1) > QUATERNION_NORM_TOLERANCE) invalid=1;
|
||||
if(!invalid) for (int i = 3; i < 7; i++) pose[i] /= root(norm);
|
||||
if(!fuse && !invalid) record_head(s,vec(pose[0],pose[1],pose[2]));
|
||||
u32 nv = u32le(header+36), ni = u32le(header+40);
|
||||
if (nv > MAX_FRAME_VERTICES || ni > MAX_FRAME_INDICES || ni%3 || (!nv && ni)) {
|
||||
error = "invalid frame counts"; break;
|
||||
|
||||
@@ -1,10 +1,110 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include "refine.h"
|
||||
#include "scan.h"
|
||||
|
||||
typedef struct { char data[65536]; usize n; int ok; } Text;
|
||||
static void text(Text *t, const char *p)
|
||||
{
|
||||
while(*p) {
|
||||
if(t->n>=sizeof(t->data)) { t->ok=0;return; }
|
||||
t->data[t->n++]=*p++;
|
||||
}
|
||||
}
|
||||
static void integer(Text *t, u64 value)
|
||||
{
|
||||
char rev[24];int n=0;
|
||||
do { rev[n++]=(char)('0'+value%10);value/=10; } while(value);
|
||||
while(n) { char a[2]={rev[--n],0};text(t,a); }
|
||||
}
|
||||
static void decimal(Text *t, float value)
|
||||
{
|
||||
if(value<0) { text(t,"-");value=-value; }
|
||||
u64 scaled=(u64)(value*10000.f+.5f);
|
||||
integer(t,scaled/10000);text(t,".");
|
||||
u64 fraction=scaled%10000;
|
||||
if(fraction<1000) text(t,"0");
|
||||
if(fraction<100) text(t,"0");
|
||||
if(fraction<10) text(t,"0");
|
||||
integer(t,fraction);
|
||||
}
|
||||
static void space_number(Text *t, float n) { text(t," ");decimal(t,n); }
|
||||
static int uuid(Text *t)
|
||||
{
|
||||
u8 bytes[16];if(!io_random(bytes,sizeof(bytes))) return 0;
|
||||
bytes[6]=(bytes[6]&15)|64;bytes[8]=(bytes[8]&63)|128;
|
||||
const char *hex="0123456789abcdef";
|
||||
for(int i=0;i<16;i++) { char pair[3]={hex[bytes[i]>>4],hex[bytes[i]&15],0};text(t,pair); }
|
||||
return 1;
|
||||
}
|
||||
static int record(Text *t, const char *name)
|
||||
{
|
||||
text(t,name);text(t," ");return uuid(t);
|
||||
}
|
||||
static int path(char *out, const char *dir, const char *name)
|
||||
{
|
||||
usize n=0;
|
||||
while(*dir) { if(n>=3900) return 0;out[n++]=*dir++; }
|
||||
if(n && out[n-1]!='/') out[n++]='/';
|
||||
while(*name) { if(n>=4095) return 0;out[n++]=*name++; }
|
||||
out[n]=0;return 1;
|
||||
}
|
||||
static void le32(u8 *out,u32 v) { for(int i=0;i<4;i++) out[i]=(u8)(v>>(8*i)); }
|
||||
static const char *write_files(Scan *s)
|
||||
{
|
||||
Text t={.ok=1};text(&t,"frameport-room 1\nuniverse ");integer(&t,s->opt.universe);
|
||||
text(&t,"\nsource scan\ntime ");integer(&t,(u64)(io_time(0)/1000000000));text(&t,"\n");
|
||||
if(!record(&t,"room")) return "getrandom failed";
|
||||
text(&t,"\n");if(!record(&t,"floor")) return "getrandom failed";
|
||||
space_number(&t,s->floor);text(&t,"\n");if(!record(&t,"ceiling")) return "getrandom failed";
|
||||
space_number(&t,s->ceiling);text(&t,"\noutline ");integer(&t,(u64)s->noutline);
|
||||
for(int i=0;i<s->noutline;i++) { space_number(&t,s->outline[i].x);space_number(&t,s->outline[i].z); }
|
||||
text(&t,"\n");
|
||||
for(int i=0;i<s->noutline;i++) {
|
||||
Pt a=s->outline[i],b=s->outline[(i+1)%s->noutline];
|
||||
if(!record(&t,"wall")) return "getrandom failed";
|
||||
space_number(&t,a.x);space_number(&t,a.z);space_number(&t,b.x);space_number(&t,b.z);
|
||||
space_number(&t,s->floor);space_number(&t,s->ceiling);
|
||||
if(s->invisible[i]) text(&t," INVISIBLE_WALL_FACE");
|
||||
text(&t,"\n");
|
||||
}
|
||||
for(int i=0;i<s->nvolumes;i++) {
|
||||
Volume *v=&s->volumes[i];
|
||||
if(!record(&t,"volume")) return "getrandom failed";
|
||||
text(&t," ");text(&t,v->label);space_number(&t,v->p.x);space_number(&t,v->p.z);
|
||||
space_number(&t,v->p.y);space_number(&t,v->yaw);space_number(&t,v->w);space_number(&t,v->d);space_number(&t,v->h);
|
||||
text(&t,"\n");
|
||||
}
|
||||
if(!record(&t,"mesh")) return "getrandom failed";
|
||||
text(&t," room-mesh.bin ");integer(&t,(u64)s->nv);text(&t," ");integer(&t,(u64)s->ni);text(&t,"\n");
|
||||
if(!t.ok) return "room text exceeds output budget";
|
||||
Text rname={.ok=1},mname={.ok=1};
|
||||
text(&rname,"room.txt.");text(&mname,"room-mesh.bin.");
|
||||
if(!uuid(&rname) || !uuid(&mname)) return "getrandom failed";
|
||||
text(&rname,".tmp");text(&mname,".tmp");rname.data[rname.n]=0;mname.data[mname.n]=0;
|
||||
char room[4096],mesh[4096],rtmp[4096],mtmp[4096];
|
||||
if(!path(room,s->opt.out,"room.txt") || !path(mesh,s->opt.out,"room-mesh.bin") ||
|
||||
!path(rtmp,s->opt.out,rname.data) || !path(mtmp,s->opt.out,mname.data)) return "output path too long";
|
||||
if(!io_mkdir(s->opt.out)) return "cannot create output directory";
|
||||
int fd=io_open(rtmp,1),ok;
|
||||
if(fd<0) return "cannot open room temporary file";
|
||||
ok=io_write(fd,t.data,t.n);if(io_close(fd)) ok=0;
|
||||
if(!ok) { io_remove(rtmp);return "cannot write room temporary file"; }
|
||||
fd=io_open(mtmp,1);
|
||||
if(fd<0) { io_remove(rtmp);return "cannot open mesh temporary file"; }
|
||||
u8 header[16]={'F','P','R','M'};le32(header+4,1);le32(header+8,(u32)s->nv);le32(header+12,(u32)s->ni);
|
||||
ok=io_write(fd,header,16);
|
||||
/* Both supported targets are little-endian IEEE float32; parser always decodes explicitly. */
|
||||
ok=ok && io_write(fd,s->vertices,(usize)s->nv*sizeof(Vec));
|
||||
ok=ok && io_write(fd,s->indices,(usize)s->ni*sizeof(u32));
|
||||
if(io_close(fd)) ok=0;
|
||||
if(!ok) { io_remove(rtmp);io_remove(mtmp);return "cannot write mesh temporary file"; }
|
||||
if(io_rename(mtmp,mesh)) { io_remove(rtmp);io_remove(mtmp);return "cannot publish mesh"; }
|
||||
if(io_rename(rtmp,room)) { io_remove(rtmp);return "cannot publish room"; }
|
||||
return 0;
|
||||
}
|
||||
static int error_json(const char *error)
|
||||
{
|
||||
Buf t;buf_init(&t,4096);put(&t,"{\"ok\":false,\"error\":\"");put(&t,error);put(&t,"\"}\n");
|
||||
io_write(1,t.data,t.n);buf_free(&t);return 2;
|
||||
Text t={.ok=1};text(&t,"{\"ok\":false,\"error\":\"");text(&t,error);text(&t,"\"}\n");
|
||||
io_write(1,t.data,t.n);return 2;
|
||||
}
|
||||
static int parse_float(const char *p,float *out)
|
||||
{
|
||||
@@ -25,60 +125,21 @@ static int parse_universe(const char *p,u64 *out)
|
||||
}
|
||||
*out=n;return 1;
|
||||
}
|
||||
/* "x,y,z,r;x,y,z,r;..." (metres, STAGE space) */
|
||||
static int parse_erase(const char *value, Refine *o)
|
||||
{
|
||||
char *p=(char *)value;o->nerase=0;
|
||||
while(*p) {
|
||||
if(o->nerase==MAX_ERASE) return 0;
|
||||
float v[4];
|
||||
for(int i=0;i<4;i++) {
|
||||
if(!parse_number(&p,&v[i])) return 0;
|
||||
if(i<3) { if(*p!=',') return 0;p++; }
|
||||
}
|
||||
if(v[3]<=0 || v[3]>5) return 0;
|
||||
o->erase[o->nerase++]=(Sphere){v[0],v[1],v[2],v[3]};
|
||||
while(*p==' ') p++;
|
||||
if(*p==';') p++;
|
||||
else if(*p) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
static const char *options(Options *o,int argc,char **argv)
|
||||
{
|
||||
*o=(Options){.voxel=.05f,.ceiling_default=2.5f,.truncation=3,.max_edge=.6f,.grazing=.12f,.range=4.f,
|
||||
.depth_jump=.25f,.depth_ratio=.12f,.min_views=5,.patch_residual=.03f,
|
||||
.plane_distance=.045f,.min_plane_area=.45f,.wall_height=1.5f,.snap_degrees=5,.floor_band=.35f,
|
||||
.ceiling_min=1.8f,.min_volume=.05f,.cluster_gap=.10f,.gap=.65f,.horizontal=.85f,
|
||||
/* The owner's refinement settings (room-scan cleanup page, 2026-10-18). */
|
||||
.rf={.refine=1,.walls=2,.protect=0,.label_rules=1,.smooth=8,.band=.35f,.facing_deg=60,.band_in=.12f,
|
||||
.floor_in=.08f,.ceil_in=.25f,.top_strip=.5f,.near=.03f,.clip=.05f,.clip_open=.10f,.island=.20f,
|
||||
.tops=.10f,.tubes=3,.tube_thick=.15f,.walked=.40f}};
|
||||
.ceiling_min=1.8f,.min_volume=.05f,.cluster_gap=.10f,.gap=.65f,.horizontal=.85f};
|
||||
for(int i=1;i<argc;i++) {
|
||||
const char *arg=argv[i];
|
||||
if(same(arg,"--json")) continue;
|
||||
if(i+1==argc) return "option requires a value";
|
||||
const char *value=argv[++i];
|
||||
float number=0;
|
||||
if(same(arg,"--offline")) o->input=value;
|
||||
else if(same(arg,"--out")) o->out=value;
|
||||
else if(same(arg,"--hint")) o->hint=value;
|
||||
else if(same(arg,"--refine-only")) o->refine_only=value;
|
||||
else if(same(arg,"--universe")) { if(!parse_universe(value,&o->universe)) return "invalid universe"; }
|
||||
else if(same(arg,"--walls")) {
|
||||
if(same(value,"off")) o->rf.walls=0;
|
||||
else if(same(value,"snap")) o->rf.walls=1;
|
||||
else if(same(value,"panels")) o->rf.walls=2;
|
||||
else return "option outside supported range";
|
||||
}
|
||||
else if(same(arg,"--erase")) { if(!parse_erase(value,&o->rf)) return "invalid erase spheres"; }
|
||||
else if(same(arg,"--refine") || same(arg,"--protect") || same(arg,"--label-rules") || same(arg,"--smooth")) {
|
||||
int high=same(arg,"--smooth") ? 50 : 1;
|
||||
if(!parse_float(value,&number) || number!=(float)(int)number || number>high) return "option outside supported range";
|
||||
int *dest=same(arg,"--refine") ? &o->rf.refine : same(arg,"--protect") ? &o->rf.protect :
|
||||
same(arg,"--label-rules") ? &o->rf.label_rules : &o->rf.smooth;
|
||||
*dest=(int)number;
|
||||
}
|
||||
else {
|
||||
float *dest=0,low=0,high=100;
|
||||
if(same(arg,"--voxel")) { dest=&o->voxel;low=.04f;high=.2f; }
|
||||
@@ -101,265 +162,44 @@ static const char *options(Options *o,int argc,char **argv)
|
||||
else if(same(arg,"--cluster-gap")) { dest=&o->cluster_gap;low=.04f;high=.2f; }
|
||||
else if(same(arg,"--gap")) { dest=&o->gap;low=.1f;high=1.5f; }
|
||||
else if(same(arg,"--horizontal")) { dest=&o->horizontal;low=.6f;high=.99f; }
|
||||
else if(same(arg,"--band")) { dest=&o->rf.band;low=.02f;high=1; }
|
||||
else if(same(arg,"--facing-deg")) { dest=&o->rf.facing_deg;low=5;high=89; }
|
||||
else if(same(arg,"--band-in")) { dest=&o->rf.band_in;high=1; }
|
||||
else if(same(arg,"--floor-in")) { dest=&o->rf.floor_in;high=1; }
|
||||
else if(same(arg,"--ceil-in")) { dest=&o->rf.ceil_in;high=1; }
|
||||
else if(same(arg,"--top-strip")) { dest=&o->rf.top_strip;high=2; }
|
||||
else if(same(arg,"--near")) { dest=&o->rf.near;high=.3f; }
|
||||
else if(same(arg,"--clip")) { dest=&o->rf.clip;high=1; }
|
||||
else if(same(arg,"--clip-open")) { dest=&o->rf.clip_open;high=2; }
|
||||
else if(same(arg,"--island")) { dest=&o->rf.island;high=5; }
|
||||
else if(same(arg,"--tops")) { dest=&o->rf.tops;high=.5f; }
|
||||
else if(same(arg,"--tubes")) { dest=&o->rf.tubes;high=20; }
|
||||
else if(same(arg,"--tube-thick")) { dest=&o->rf.tube_thick;low=.01f;high=1; }
|
||||
else if(same(arg,"--walked")) { dest=&o->rf.walked;high=1.5f; }
|
||||
else return "unknown option";
|
||||
if(!parse_float(value,dest) || *dest<low || *dest>high) return "option outside supported range";
|
||||
}
|
||||
}
|
||||
if(o->rf.walked>0 && o->rf.walked<.05f) return "option outside supported range";
|
||||
if(o->refine_only) return o->input ? "--refine-only takes no --offline dump" : 0;
|
||||
return o->input && o->out ? 0 : "required: --offline dump.fprs --out directory";
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ refinement */
|
||||
typedef struct {
|
||||
Report rep;
|
||||
Label labels[MAX_VOLUMES];
|
||||
int nlabels, changed, removed, nsuggest, out_triangles, out_volumes;
|
||||
float ceiling_y;
|
||||
i64 ms;
|
||||
} Outcome;
|
||||
static const char *copy_name(char *dst, const char *src, usize cap)
|
||||
static void duration(Text *t,i64 a,i64 b) { decimal(t,(float)(b-a)/1000000); }
|
||||
static void summary(Scan *s)
|
||||
{
|
||||
usize i=0;while(src[i] && i+1<cap) { dst[i]=src[i];i++; }
|
||||
dst[i]=0;return dst;
|
||||
Text t={.ok=1};text(&t,"{\"ok\":true,\"frames\":");integer(&t,s->frames);
|
||||
text(&t,",\"skipped_frames\":");integer(&t,s->skipped_frames);
|
||||
text(&t,",\"far_vertices\":");integer(&t,s->far_vertices);
|
||||
text(&t,",\"discontinuity_triangles\":");integer(&t,s->discontinuities);
|
||||
text(&t,",\"walls\":");integer(&t,(u64)s->noutline);
|
||||
text(&t,",\"detected_walls\":");integer(&t,(u64)s->nplanes);
|
||||
text(&t,",\"volumes\":");integer(&t,(u64)s->nvolumes);
|
||||
text(&t,",\"floor_y\":");decimal(&t,s->floor);text(&t,",\"ceiling_y\":");decimal(&t,s->ceiling);
|
||||
text(&t,",\"coverage_percent\":");decimal(&t,s->coverage);
|
||||
text(&t,",\"hint_used\":");text(&t,s->hint_used ? "true" : "false");
|
||||
text(&t,",\"triangles\":");integer(&t,(u64)s->ni/3);
|
||||
text(&t,",\"accepted_triangles\":");integer(&t,s->accepted);
|
||||
text(&t,",\"rejected_triangles\":");integer(&t,s->rejected);
|
||||
text(&t,",\"warnings\":[");
|
||||
for(int i=0;i<s->nwarnings;i++) { if(i) text(&t,",");text(&t,"\"");text(&t,s->warnings[i]);text(&t,"\""); }
|
||||
text(&t,"],\"timings_ms\":{\"read\":");duration(&t,s->t0,s->tread);
|
||||
text(&t,",\"fusion\":");duration(&t,s->tread,s->tfuse);
|
||||
text(&t,",\"model\":");duration(&t,s->tfuse,s->tmodel);
|
||||
text(&t,",\"mesh\":");duration(&t,s->tmodel,s->tmesh);
|
||||
text(&t,",\"total\":");duration(&t,s->t0,io_time(1));text(&t,"}}\n");io_write(1,t.data,t.n);
|
||||
}
|
||||
static void box_json(Buf *b, const RVol *v)
|
||||
{
|
||||
put(b,"{\"cx\":");put_dec(b,v->cx);put(b,",\"cz\":");put_dec(b,v->cz);put(b,",\"top_y\":");put_dec(b,v->top);
|
||||
put(b,",\"yaw_deg\":");put_dec(b,v->yaw);put(b,",\"w\":");put_dec(b,v->w);put(b,",\"d\":");put_dec(b,v->d);
|
||||
put(b,",\"h\":");put_dec(b,v->h);put(b,"}");
|
||||
}
|
||||
static void scan_json(Buf *b, const Refine *o, const Vec *heads, int nheads, const Outcome *r)
|
||||
{
|
||||
put(b,"{\"version\":1,\"heads\":");heads_json(b,heads,nheads);
|
||||
put(b,",\"refine\":");settings_json(b,o);
|
||||
put(b,",\"report\":");report_json(b,&r->rep,r->nlabels,r->changed,r->removed,r->nsuggest);
|
||||
put(b,",\"labels\":[");
|
||||
for(int i=0;i<r->nlabels;i++) {
|
||||
const Label *l=&r->labels[i];
|
||||
if(i) put(b,",");
|
||||
put(b,"{\"from\":\"");put(b,l->from);put(b,"\",\"to\":");
|
||||
if(l->to[0]) { put(b,"\"");put(b,l->to);put(b,"\""); } else put(b,"null");
|
||||
put(b,",\"reason\":\"");put(b,l->reason);put(b,"\"");
|
||||
if(l->suggest[0]) { put(b,",\"suggest\":\"");put(b,l->suggest);put(b,"\""); }
|
||||
put(b,",\"id\":\"");put(b,l->box.id);put(b,"\",\"box\":");box_json(b,&l->box);put(b,"}");
|
||||
}
|
||||
put(b,"],\"suggestions\":[");
|
||||
int first=1;
|
||||
for(int i=0;i<r->nlabels;i++) {
|
||||
const Label *l=&r->labels[i];
|
||||
if(!l->suggest[0]) continue;
|
||||
if(!first) put(b,",");
|
||||
first=0;
|
||||
put(b,"{\"kind\":\"");put(b,l->suggest);put(b,"\",\"box\":");box_json(b,&l->box);
|
||||
put(b,",\"wall\":");put_int(b,l->wall);put(b,",\"along\":");put_dec(b,l->along);
|
||||
put(b,",\"reason\":\"");put(b,l->reason);put(b,"\"}");
|
||||
}
|
||||
put(b,"]}\n");
|
||||
}
|
||||
/* Refines `raw` (as read back from its own text) and publishes: [raw mesh, raw text,] scan.json, room-mesh.bin,
|
||||
* room.txt. Everything is computed before the first file is written, so a failure leaves the old files. */
|
||||
static const char *refine_publish(const char *dir, const Room *raw, const Vec *v, const u32 *idx, int nv, int ni,
|
||||
const Vec *heads, int nheads, const Refine *o, int write_raw, Outcome *res)
|
||||
{
|
||||
static Room refined;
|
||||
static RVol ruled[MAX_VOLUMES];
|
||||
const char *error=0;
|
||||
Prep prep={0};
|
||||
Vec *rv=0;u32 *ri=0;int rn=0;
|
||||
u8 *raw_mesh=0,*mesh=0;usize raw_size=0,mesh_size=0;
|
||||
Buf raw_text,text,json;
|
||||
buf_init(&raw_text,1u<<20);buf_init(&text,1u<<20);buf_init(&json,MAX_JSON_FILE);
|
||||
i64 t0=io_time(1);
|
||||
memset(res,0,sizeof(*res));
|
||||
refined=*raw;
|
||||
copy_name(refined.mesh_name,"room-mesh.bin",sizeof(refined.mesh_name));
|
||||
if(o->refine) {
|
||||
error=prep_mesh(&prep,v,idx,nv,ni);
|
||||
int nruled=raw->nvols;
|
||||
if(!error) {
|
||||
if(o->label_rules) res->nlabels=rules_apply(raw,&prep,ruled,&nruled,res->labels);
|
||||
else memcpy(ruled,raw->vols,sizeof(RVol)*(usize)raw->nvols);
|
||||
}
|
||||
if(!error) error=refine_run(o,raw,&prep,ruled,nruled,heads,nheads,&rv,&ri,&rn,&res->rep);
|
||||
if(!error) {
|
||||
refined.ceiling=res->rep.ceiling_y;
|
||||
for(int i=0;i<refined.nwalls;i++) if(refined.walls[i].y1==raw->ceiling) refined.walls[i].y1=refined.ceiling;
|
||||
memcpy(refined.vols,ruled,sizeof(RVol)*(usize)nruled);refined.nvols=nruled;
|
||||
refined.mesh_nv=refined.mesh_ni=(u32)rn;refined.has_mesh=1;
|
||||
for(int i=0;i<res->nlabels;i++) {
|
||||
Label *l=&res->labels[i];
|
||||
if(l->suggest[0]) res->nsuggest++;
|
||||
if(!l->to[0]) res->removed++;
|
||||
else if(!same(l->to,l->from)) res->changed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!error) error=mesh_bytes(rv ? rv : v,rv ? ri : idx,rv ? rn : nv,rv ? rn : ni,&mesh,&mesh_size);
|
||||
if(!error && write_raw) error=mesh_bytes(v,idx,nv,ni,&raw_mesh,&raw_size);
|
||||
if(!error && write_raw && !room_text(raw,&raw_text)) error="room text exceeds output budget";
|
||||
if(!error && !room_text(&refined,&text)) error="room text exceeds output budget";
|
||||
if(!error) { scan_json(&json,o,heads,nheads,res);if(!json.ok) error="scan.json exceeds output budget"; }
|
||||
res->ms=io_time(1)-t0;
|
||||
res->out_triangles=(int)refined.mesh_ni/3;res->out_volumes=refined.nvols;res->ceiling_y=refined.ceiling;
|
||||
if(!error && !io_mkdir(dir)) error="cannot create output directory";
|
||||
if(!error && write_raw) error=publish(dir,"room-mesh-raw.bin",raw_mesh,raw_size);
|
||||
if(!error && write_raw) error=publish(dir,"room-raw.txt",raw_text.data,raw_text.n);
|
||||
if(!error) error=publish(dir,"scan.json",json.data,json.n);
|
||||
if(!error) error=publish(dir,"room-mesh.bin",mesh,mesh_size);
|
||||
if(!error) error=publish(dir,"room.txt",text.data,text.n);
|
||||
prep_free(&prep);
|
||||
io_free(rv,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(ri,(usize)MAX_TRIANGLES*3*sizeof(u32));
|
||||
io_free(raw_mesh,raw_size);io_free(mesh,mesh_size);
|
||||
buf_free(&raw_text);buf_free(&text);buf_free(&json);
|
||||
return error;
|
||||
}
|
||||
static void refine_summary(Buf *t, const Outcome *r)
|
||||
{
|
||||
put(t,"\"refine\":");report_json(t,&r->rep,r->nlabels,r->changed,r->removed,r->nsuggest);
|
||||
if(t->ok && t->n) t->n--; /* reopen the object for the output counts */
|
||||
put(t,",\"output_triangles\":");put_int(t,r->out_triangles);
|
||||
put(t,",\"output_volumes\":");put_int(t,r->out_volumes);put(t,"}");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ summaries */
|
||||
static void duration(Buf *t,i64 a,i64 b) { put_dec(t,(float)(b-a)/1000000); }
|
||||
static void warnings_json(Buf *t, const char **w, int n)
|
||||
{
|
||||
put(t,",\"warnings\":[");
|
||||
for(int i=0;i<n;i++) { if(i) put(t,",");put(t,"\"");put(t,w[i]);put(t,"\""); }
|
||||
put(t,"]");
|
||||
}
|
||||
static void summary(Scan *s, const Outcome *r)
|
||||
{
|
||||
Buf t;buf_init(&t,1u<<16);put(&t,"{\"ok\":true,\"frames\":");put_u64(&t,s->frames);
|
||||
put(&t,",\"skipped_frames\":");put_u64(&t,s->skipped_frames);
|
||||
put(&t,",\"far_vertices\":");put_u64(&t,s->far_vertices);
|
||||
put(&t,",\"discontinuity_triangles\":");put_u64(&t,s->discontinuities);
|
||||
put(&t,",\"walls\":");put_u64(&t,(u64)s->noutline);
|
||||
put(&t,",\"detected_walls\":");put_u64(&t,(u64)s->nplanes);
|
||||
put(&t,",\"volumes\":");put_u64(&t,(u64)s->nvolumes);
|
||||
put(&t,",\"floor_y\":");put_dec(&t,s->floor);put(&t,",\"ceiling_y\":");put_dec(&t,s->ceiling);
|
||||
put(&t,",\"coverage_percent\":");put_dec(&t,s->coverage);
|
||||
put(&t,",\"hint_used\":");put(&t,s->hint_used ? "true" : "false");
|
||||
put(&t,",\"triangles\":");put_u64(&t,(u64)s->ni/3);
|
||||
put(&t,",\"accepted_triangles\":");put_u64(&t,s->accepted);
|
||||
put(&t,",\"rejected_triangles\":");put_u64(&t,s->rejected);
|
||||
put(&t,",\"heads\":");put_int(&t,s->nheads);put(&t,",");
|
||||
refine_summary(&t,r);
|
||||
warnings_json(&t,s->warnings,s->nwarnings);
|
||||
put(&t,",\"timings_ms\":{\"read\":");duration(&t,s->t0,s->tread);
|
||||
put(&t,",\"fusion\":");duration(&t,s->tread,s->tfuse);
|
||||
put(&t,",\"model\":");duration(&t,s->tfuse,s->tmodel);
|
||||
put(&t,",\"mesh\":");duration(&t,s->tmodel,s->tmesh);
|
||||
put(&t,",\"refine\":");duration(&t,s->tmesh,s->trefine);
|
||||
put(&t,",\"total\":");duration(&t,s->t0,io_time(1));put(&t,"}}\n");io_write(1,t.data,t.n);buf_free(&t);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ scan path */
|
||||
static const char *scan_outputs(Scan *s, Outcome *out)
|
||||
{
|
||||
static Room built,raw;
|
||||
static Vec heads[MAX_HEADS];
|
||||
const char *error=room_from_scan(s,&built);
|
||||
if(error) return error;
|
||||
copy_name(built.mesh_name,"room-mesh-raw.bin",sizeof(built.mesh_name));
|
||||
/* Refine what the raw files will hold, so --refine-only on them gives the same refined files. */
|
||||
Buf t;buf_init(&t,1u<<20);
|
||||
if(!room_text(&built,&t)) { buf_free(&t);return "room text exceeds output budget"; }
|
||||
error=room_parse(t.data,t.n,&raw);
|
||||
buf_free(&t);
|
||||
if(error) return error;
|
||||
Buf h;buf_init(&h,1u<<20);heads_json(&h,s->heads,s->nheads);put(&h,"}");
|
||||
Buf wrap;buf_init(&wrap,1u<<20);put(&wrap,"{\"heads\":");put_n(&wrap,h.data,h.n);
|
||||
int nheads=wrap.ok ? heads_parse(wrap.data,wrap.n,heads) : 0;
|
||||
buf_free(&h);buf_free(&wrap);
|
||||
return refine_publish(s->opt.out,&raw,s->vertices,s->indices,s->nv,s->ni,heads,nheads,&s->opt.rf,1,out);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ --refine-only */
|
||||
static int refine_only(Options *o)
|
||||
{
|
||||
static Room raw;
|
||||
static Vec heads[MAX_HEADS];
|
||||
static Outcome res;
|
||||
const char *warns[4];int nwarn=0;
|
||||
i64 t0=io_time(1);
|
||||
const char *dir=o->refine_only,*error=0;
|
||||
char path[4096];
|
||||
u8 *text=0,*bin=0,*json=0;usize text_n=0,bin_n=0,json_n=0;
|
||||
Vec *v=0;u32 *idx=0;int nv=0,ni=0,nheads=0,write_raw=0;
|
||||
if(!join_path(path,dir,"room-raw.txt")) return error_json("output path too long");
|
||||
error=read_file(path,1u<<20,&text,&text_n);
|
||||
if(error) {
|
||||
/* An older room folder: its room.txt + mesh are the unrefined scan. */
|
||||
write_raw=1;
|
||||
if(!join_path(path,dir,"room.txt")) return error_json("output path too long");
|
||||
error=read_file(path,1u<<20,&text,&text_n);
|
||||
if(error) return error_json("room folder has neither room-raw.txt nor room.txt");
|
||||
}
|
||||
error=room_parse((const char *)text,text_n,&raw);
|
||||
io_free(text,(1u<<20)+1);
|
||||
if(error) return error_json(error);
|
||||
if(!raw.has_mesh) return error_json("room has no mesh to refine");
|
||||
if(!join_path(path,dir,write_raw ? raw.mesh_name : "room-mesh-raw.bin")) return error_json("output path too long");
|
||||
error=read_file(path,MAX_RAW_FILE,&bin,&bin_n);
|
||||
if(!error) error=mesh_parse(bin,bin_n,&v,&idx,&nv,&ni);
|
||||
io_free(bin,MAX_RAW_FILE+1);
|
||||
if(error) return error_json(error);
|
||||
if((u32)nv!=raw.mesh_nv || (u32)ni!=raw.mesh_ni) {
|
||||
io_free(v,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(idx,(usize)MAX_TRIANGLES*3*sizeof(u32));
|
||||
return error_json("mesh record does not match the mesh file");
|
||||
}
|
||||
copy_name(raw.mesh_name,"room-mesh-raw.bin",sizeof(raw.mesh_name));
|
||||
if(join_path(path,dir,"scan.json") && !read_file(path,MAX_JSON_FILE,&json,&json_n)) {
|
||||
nheads=heads_parse((const char *)json,json_n,heads);
|
||||
io_free(json,MAX_JSON_FILE+1);
|
||||
}
|
||||
if(!nheads && o->rf.walked>0) warns[nwarn++]="no head path; walked volume skipped";
|
||||
if(write_raw) warns[nwarn++]="no raw files; the current room was taken as the unrefined scan";
|
||||
error=refine_publish(dir,&raw,v,idx,nv,ni,heads,nheads,&o->rf,write_raw,&res);
|
||||
io_free(v,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(idx,(usize)MAX_TRIANGLES*3*sizeof(u32));
|
||||
if(error) return error_json(error);
|
||||
if(res.rep.truncated) warns[nwarn++]="refined mesh truncated to the triangle budget";
|
||||
Buf t;buf_init(&t,1u<<16);
|
||||
put(&t,"{\"ok\":true,\"refine_only\":true,\"raw_triangles\":");put_int(&t,ni/3);
|
||||
put(&t,",\"volumes\":");put_int(&t,raw.nvols);put(&t,",\"floor_y\":");put_dec(&t,raw.floor);
|
||||
put(&t,",\"ceiling_y\":");put_dec(&t,res.ceiling_y);put(&t,",\"heads\":");put_int(&t,nheads);put(&t,",");
|
||||
refine_summary(&t,&res);
|
||||
warnings_json(&t,warns,nwarn);
|
||||
put(&t,",\"timings_ms\":{\"refine\":");put_dec(&t,(float)res.ms/1000000);
|
||||
put(&t,",\"total\":");duration(&t,t0,io_time(1));put(&t,"}}\n");
|
||||
io_write(1,t.data,t.n);buf_free(&t);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc,char **argv)
|
||||
{
|
||||
if(argc==2 && same(argv[1],"--help")) {
|
||||
const char usage[]="fp_roomscan --offline dump.fprs --out directory [--universe N] [--voxel 0.05]\n"
|
||||
" [--ceiling-default 2.5] [--hint outline.txt] [--json]\n"
|
||||
"fp_roomscan --refine-only room-directory [refinement options] [--json]\n"
|
||||
"Tuning: --truncation --max-edge --grazing --range --depth-jump --depth-ratio --plane-distance --min-plane-area\n"
|
||||
" --min-views --patch-residual --wall-height --snap-degrees --floor-band --ceiling-min\n"
|
||||
" --min-volume --cluster-gap --gap --horizontal\n"
|
||||
"Refinement: --refine 0|1 --walls off|snap|panels --band --facing-deg --band-in --floor-in --ceil-in\n"
|
||||
" --top-strip --near --clip --clip-open --island --smooth --tops --protect 0|1 --tubes --tube-thick\n"
|
||||
" --walked --label-rules 0|1 --erase \"x,y,z,r;...\"\n"
|
||||
"Always prints one JSON summary; errors exit 2. See docs/mr/scan.md for units and ranges.\n";
|
||||
io_write(1,usage,sizeof(usage)-1);return 0;
|
||||
}
|
||||
@@ -367,11 +207,6 @@ int main(int argc,char **argv)
|
||||
if(!s) return error_json("scan allocation failed");
|
||||
s->rng=0x714ce3u;s->t0=io_time(1);
|
||||
const char *error=options(&s->opt,argc,argv);
|
||||
if(!error && s->opt.refine_only) {
|
||||
int result=refine_only(&s->opt);
|
||||
io_free(s,sizeof(Scan));return result;
|
||||
}
|
||||
static Outcome outcome;
|
||||
if(!error) error=read_dump(s,0);
|
||||
s->tread=io_time(1);
|
||||
if(!error) error=init_grid(s);
|
||||
@@ -382,13 +217,10 @@ int main(int argc,char **argv)
|
||||
s->tmodel=io_time(1);
|
||||
if(!error) error=make_mesh(s);
|
||||
s->tmesh=io_time(1);
|
||||
/* The grid is not needed after the mesh: freeing it keeps the refinement inside the same memory budget. */
|
||||
io_free(s->cells,(usize)s->count*sizeof(Cell));s->cells=0;
|
||||
if(!error) error=scan_outputs(s,&outcome);
|
||||
s->trefine=io_time(1);
|
||||
if(!error && outcome.rep.truncated) warn(s,"refined mesh truncated to the triangle budget");
|
||||
if(!error) error=write_files(s);
|
||||
int result=0;
|
||||
if(error) result=error_json(error);else summary(s,&outcome);
|
||||
if(error) result=error_json(error);else summary(s);
|
||||
io_free(s->cells,(usize)s->count*sizeof(Cell));
|
||||
io_free(s->vertices,(usize)MAX_MESH_VERTICES*sizeof(Vec));
|
||||
io_free(s->indices,(usize)MAX_TRIANGLES*3*sizeof(u32));
|
||||
io_free(s,sizeof(Scan));return result;
|
||||
|
||||
@@ -596,7 +596,7 @@ const char *make_model(Scan *s)
|
||||
return error;
|
||||
}
|
||||
|
||||
int parse_number(char **cursor, float *value)
|
||||
static int number(char **cursor, float *value)
|
||||
{
|
||||
char *p=*cursor;while (*p==' ' || *p=='\t' || *p=='\r') p++;
|
||||
int sign=1,digits=0;if (*p=='-' || *p=='+') { if (*p=='-') sign=-1;p++; }
|
||||
@@ -631,7 +631,7 @@ static const char *hint(Scan *s)
|
||||
while (*p==' ' || *p=='\t' || *p=='\r' || *p=='\n') p++;
|
||||
if (!*p) break;
|
||||
if (*p=='#') { while (*p && *p!='\n') p++;continue; }
|
||||
if (n==MAX_OUTLINE || !parse_number(&p,&pts[n].x) || !parse_number(&p,&pts[n].z)) return "invalid outline hint";
|
||||
if (n==MAX_OUTLINE || !number(&p,&pts[n].x) || !number(&p,&pts[n].z)) return "invalid outline hint";
|
||||
n++;while (*p==' ' || *p=='\t' || *p=='\r') p++;
|
||||
if (*p && *p!='\n' && *p!='#') return "invalid outline hint";
|
||||
if (*p=='#') while (*p && *p!='\n') p++;
|
||||
|
||||
@@ -1,548 +0,0 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Mesh refinement: the passes of the owner's tuned cleanup page (2026-10-18), in its order and with its rules:
|
||||
* plane offsets re-estimated from the scan, structural (wall/floor/ceiling) triangles classified with one-sided
|
||||
* bands, erase spheres, the walked volume, flat panels or snapping, furniture tops, clipping outside the outline,
|
||||
* small islands, thin floating pieces, Taubin smoothing. docs/mr/scan.md "Refinement" describes each pass. */
|
||||
#include "refine.h"
|
||||
|
||||
#define WELD_MM 1000.0 /* vertices closer than 1 mm (quantized) are one vertex */
|
||||
#define WELD_BIAS 16384 /* key offset, as the reference: +-16 m */
|
||||
#define FURNITURE_PAD .1 /* m around a box that still counts as the box */
|
||||
#define MIN_SHIFT_SAMPLES 50 /* plane offsets move only with more facing triangles than this */
|
||||
#define WALKED_BELOW .2 /* m above the floor: the walked column starts here (feet, rugs stay) */
|
||||
#define WALKED_ABOVE .25 /* m above the head: the column ends here */
|
||||
#define TOP_NORMAL_Y .8 /* a triangle this upward-facing near a box top is part of the top */
|
||||
#define TUBE_MIN_TRIANGLES 10
|
||||
#define TUBE_MAX_AREA 1.0 /* m^2: bigger pieces are never thin floating pieces */
|
||||
#define TUBE_MAX_FURNITURE .5 /* share of a piece inside furniture boxes that keeps it */
|
||||
#define TAUBIN_LAMBDA .5
|
||||
#define TAUBIN_MU -.53
|
||||
|
||||
double dfloor(double x) { i64 i=(i64)x; return x<(double)i ? (double)(i-1) : (double)i; }
|
||||
double droot(double x) { return x>0 ? __builtin_sqrt(x) : 0; }
|
||||
/* Double-precision sine (range reduction + Taylor series to x^19, error < 1e-15), as Math.sin. */
|
||||
double dsin(double x)
|
||||
{
|
||||
const double pi=3.14159265358979323846;
|
||||
x-=dfloor(x/(2*pi)+.5)*(2*pi);
|
||||
if(x>pi/2) x=pi-x;
|
||||
if(x<-pi/2) x=-pi-x;
|
||||
double q=x*x,term=x,sum=x;
|
||||
for(int k=1;k<10;k++) { term*=-q/((2*k)*(2*k+1));sum+=term; }
|
||||
return sum;
|
||||
}
|
||||
double dcos(double x) { return dsin(x+3.14159265358979323846/2); }
|
||||
/* Math.hypot as V8 computes it (scaled, Kahan-compensated): distance ties at outline corners break the same way. */
|
||||
double dhypot(double a, double b)
|
||||
{
|
||||
a=a<0 ? -a : a;b=b<0 ? -b : b;
|
||||
double m=a>b ? a : b;
|
||||
if(m==0) return 0;
|
||||
double sum=0,comp=0,v[2]={a/m,b/m};
|
||||
for(int i=0;i<2;i++) { double s=v[i]*v[i]-comp,p=sum+s;comp=(p-sum)-s;sum=p; }
|
||||
return droot(sum)*m;
|
||||
}
|
||||
static double dab(double x) { return x<0 ? -x : x; }
|
||||
|
||||
static void *alloc(usize n) { return n ? io_alloc(n) : 0; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ welding */
|
||||
static u64 mix(u64 k) { k^=k>>33;k*=0xff51afd7ed558ccdull;k^=k>>33;k*=0xc4ceb9fe1a85ec53ull;k^=k>>33;return k; }
|
||||
static i64 quant(float v) { return (i64)dfloor((double)v*WELD_MM+.5)+WELD_BIAS; }
|
||||
const char *prep_mesh(Prep *p, const Vec *v, const u32 *idx, int nv, int ni)
|
||||
{
|
||||
*p=(Prep){0};
|
||||
int T=ni/3;
|
||||
usize cap=1024;while(cap<(usize)nv*2) cap*=2;
|
||||
i64 *keys=alloc(cap*sizeof(i64));
|
||||
int *slot=alloc(cap*sizeof(int)),*wid=alloc((usize)(nv ? nv : 1)*sizeof(int));
|
||||
double *sums=alloc((usize)(nv ? nv : 1)*4*sizeof(double));
|
||||
const char *error=0;
|
||||
if(!keys || !slot || !wid || !sums) { error="refine allocation failed";goto done; }
|
||||
int W=0;
|
||||
for(int i=0;i<nv;i++) {
|
||||
i64 key=(quant(v[i].x)*32768+quant(v[i].y))*32768+quant(v[i].z);
|
||||
usize h=(usize)mix((u64)key)&(cap-1);
|
||||
while(slot[h] && keys[h]!=key) h=(h+1)&(cap-1);
|
||||
if(!slot[h]) { keys[h]=key;slot[h]=++W; }
|
||||
int w=slot[h]-1;wid[i]=w;
|
||||
sums[4*w]+=v[i].x;sums[4*w+1]+=v[i].y;sums[4*w+2]+=v[i].z;sums[4*w+3]+=1;
|
||||
}
|
||||
p->T=T;p->W=W;
|
||||
p->pos=alloc((usize)(W ? W : 1)*3*sizeof(double));
|
||||
p->F=alloc((usize)(T ? T : 1)*3*sizeof(int));
|
||||
p->fn=alloc((usize)(T ? T : 1)*3*sizeof(float));
|
||||
p->cen=alloc((usize)(T ? T : 1)*3*sizeof(float));
|
||||
p->area=alloc((usize)(T ? T : 1)*sizeof(float));
|
||||
if(!p->pos || !p->F || !p->fn || !p->cen || !p->area) { error="refine allocation failed";goto done; }
|
||||
for(int w=0;w<W;w++) for(int k=0;k<3;k++) p->pos[3*w+k]=sums[4*w+k]/sums[4*w+3];
|
||||
for(int k=0;k<T*3;k++) p->F[k]=wid[idx[k]];
|
||||
for(int t=0;t<T;t++) {
|
||||
const double *a=p->pos+3*p->F[3*t],*b=p->pos+3*p->F[3*t+1],*c=p->pos+3*p->F[3*t+2];
|
||||
double ux=b[0]-a[0],uy=b[1]-a[1],uz=b[2]-a[2],wx=c[0]-a[0],wy=c[1]-a[1],wz=c[2]-a[2];
|
||||
double nx=uy*wz-uz*wy,ny=uz*wx-ux*wz,nz=ux*wy-uy*wx,l=droot(nx*nx+ny*ny+nz*nz);
|
||||
if(!(l>0)) l=1e-12;
|
||||
p->fn[3*t]=(float)(nx/l);p->fn[3*t+1]=(float)(ny/l);p->fn[3*t+2]=(float)(nz/l);p->area[t]=(float)(l/2);
|
||||
for(int k=0;k<3;k++) p->cen[3*t+k]=(float)((a[k]+b[k]+c[k])/3);
|
||||
}
|
||||
done:
|
||||
io_free(keys,cap*sizeof(i64));io_free(slot,cap*sizeof(int));
|
||||
io_free(wid,(usize)(nv ? nv : 1)*sizeof(int));io_free(sums,(usize)(nv ? nv : 1)*4*sizeof(double));
|
||||
if(error) prep_free(p);
|
||||
return error;
|
||||
}
|
||||
void prep_free(Prep *p)
|
||||
{
|
||||
int T=p->T ? p->T : 1,W=p->W ? p->W : 1;
|
||||
io_free(p->pos,(usize)W*3*sizeof(double));io_free(p->F,(usize)T*3*sizeof(int));
|
||||
io_free(p->fn,(usize)T*3*sizeof(float));io_free(p->cen,(usize)T*3*sizeof(float));io_free(p->area,(usize)T*sizeof(float));
|
||||
*p=(Prep){0};
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ helpers */
|
||||
typedef struct {
|
||||
int kind, wall; /* 0 wall (index into room->walls), 1 floor, 2 ceiling */
|
||||
double n[3], off, shift;
|
||||
double ox, oz, ux, uz, L;
|
||||
} RPlane;
|
||||
typedef struct { double cs, sn; const RVol *v; } Box;
|
||||
static void box_local(const Box *b, double x, double z, double *lx, double *lz)
|
||||
{
|
||||
double dx=x-b->v->cx,dz=z-b->v->cz;
|
||||
*lx=b->cs*dx-b->sn*dz;*lz=b->sn*dx+b->cs*dz;
|
||||
}
|
||||
static double s_dist(const RPlane *pl, const float *c) { return pl->n[0]*c[0]+pl->n[1]*c[1]+pl->n[2]*c[2]-pl->off; }
|
||||
static int in_ext(const RPlane *pl, const float *c, double m)
|
||||
{
|
||||
if(pl->kind) return 1;
|
||||
double s=(c[0]-pl->ox)*pl->ux+(c[2]-pl->oz)*pl->uz;
|
||||
return s>-m && s<pl->L+m;
|
||||
}
|
||||
static int point_in_poly(double x, double z, const DPt *poly, int n)
|
||||
{
|
||||
int inside=0;
|
||||
for(int i=0,j=n-1;i<n;j=i++) {
|
||||
double xi=poly[i].x,zi=poly[i].z,xj=poly[j].x,zj=poly[j].z;
|
||||
if((zi>z)!=(zj>z) && x<(xj-xi)*(z-zi)/(zj-zi+1e-12)+xi) inside=!inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
/* Distance outside the outline (0 inside) and the extra allowance of the nearest wall (unseen walls). */
|
||||
static double outside_info(const Room *r, double x, double z, double open_extra, double *allow)
|
||||
{
|
||||
double best=1e300;*allow=0;
|
||||
for(int i=0;i<r->nwalls;i++) {
|
||||
const RWall *w=&r->walls[i];
|
||||
double abx=w->x1-w->x0,abz=w->z1-w->z0,l2=abx*abx+abz*abz;
|
||||
double t=l2>0 ? ((x-w->x0)*abx+(z-w->z0)*abz)/l2 : 0;
|
||||
t=t<0 ? 0 : t>1 ? 1 : t;
|
||||
double dx=x-(w->x0+t*abx),dz=z-(w->z0+t*abz),d=dhypot(dx,dz);
|
||||
if(d<best) { best=d;*allow=w->invisible ? open_extra : 0; }
|
||||
}
|
||||
return point_in_poly(x,z,r->outline,r->noutline) ? 0 : best;
|
||||
}
|
||||
static void nth(float *a, int n, int k)
|
||||
{
|
||||
int lo=0,hi=n-1;
|
||||
while(lo<hi) {
|
||||
float pivot=a[(lo+hi)/2];int i=lo,j=hi;
|
||||
while(i<=j) {
|
||||
while(a[i]<pivot) i++;
|
||||
while(a[j]>pivot) j--;
|
||||
if(i<=j) { float t=a[i];a[i++]=a[j];a[j--]=t; }
|
||||
}
|
||||
if(k<=j) hi=j;else if(k>=i) lo=i;else break;
|
||||
}
|
||||
}
|
||||
static int find(int *parent, int x)
|
||||
{
|
||||
while(parent[x]!=x) { parent[x]=parent[parent[x]];x=parent[x]; }
|
||||
return x;
|
||||
}
|
||||
static void unite(int *parent, const int *F, const u8 *keep, int T, int W)
|
||||
{
|
||||
for(int i=0;i<W;i++) parent[i]=i;
|
||||
for(int t=0;t<T;t++) if(keep[t]) {
|
||||
int r=find(parent,F[3*t]);
|
||||
parent[find(parent,F[3*t+1])]=r;parent[find(parent,F[3*t+2])]=r;
|
||||
}
|
||||
}
|
||||
/* Eigenvalues of a symmetric 3x3 matrix by Jacobi rotations, as the reference's eigSym3. */
|
||||
static void eig3(double a[3][3], double out[3])
|
||||
{
|
||||
static const int pairs[3][2]={{0,1},{0,2},{1,2}};
|
||||
for(int sweep=0;sweep<20;sweep++) {
|
||||
double off=dab(a[0][1])+dab(a[0][2])+dab(a[1][2]);
|
||||
if(off<1e-14) break;
|
||||
for(int k=0;k<3;k++) {
|
||||
int p=pairs[k][0],q=pairs[k][1];
|
||||
if(dab(a[p][q])<1e-18) continue;
|
||||
double th=(a[q][q]-a[p][p])/(2*a[p][q]);
|
||||
double t=(th<0 ? -1 : 1)/(dab(th)+droot(th*th+1)),c=1/droot(t*t+1),s=t*c;
|
||||
for(int m=0;m<3;m++) { double akp=a[m][p],akq=a[m][q];a[m][p]=c*akp-s*akq;a[m][q]=s*akp+c*akq; }
|
||||
for(int m=0;m<3;m++) { double apk=a[p][m],aqk=a[q][m];a[p][m]=c*apk-s*aqk;a[q][m]=s*apk+c*aqk; }
|
||||
}
|
||||
}
|
||||
out[0]=a[0][0];out[1]=a[1][1];out[2]=a[2][2];
|
||||
}
|
||||
/* Ear clipping of the (positively wound) outline, exactly the reference's order. */
|
||||
static int ear_clip(const DPt *poly, int n, int tris[][3])
|
||||
{
|
||||
int idx[MAX_OUTLINE],m=n,count=0,guard=0;
|
||||
for(int i=0;i<n;i++) idx[i]=i;
|
||||
#define A2(a,b,c) ((poly[b].x-poly[a].x)*(poly[c].z-poly[a].z)-(poly[b].z-poly[a].z)*(poly[c].x-poly[a].x))
|
||||
while(m>3 && guard++<10000) {
|
||||
int cut=0;
|
||||
for(int k=0;k<m;k++) {
|
||||
int i0=idx[(k-1+m)%m],i1=idx[k],i2=idx[(k+1)%m];
|
||||
if(A2(i0,i1,i2)<=0) continue;
|
||||
int blocked=0;
|
||||
for(int q=0;q<m && !blocked;q++) {
|
||||
int j=idx[q];
|
||||
if(j!=i0 && j!=i1 && j!=i2 && A2(i0,i1,j)>0 && A2(i1,i2,j)>0 && A2(i2,i0,j)>0) blocked=1;
|
||||
}
|
||||
if(blocked) continue;
|
||||
tris[count][0]=i0;tris[count][1]=i1;tris[count][2]=i2;count++;
|
||||
for(int q=k;q+1<m;q++) idx[q]=idx[q+1];
|
||||
m--;cut=1;break;
|
||||
}
|
||||
if(!cut) break;
|
||||
}
|
||||
#undef A2
|
||||
if(m==3) { tris[count][0]=idx[0];tris[count][1]=idx[1];tris[count][2]=idx[2];count++; }
|
||||
return count;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ passes */
|
||||
typedef struct {
|
||||
u8 *furn, *keep;
|
||||
signed char *owner;
|
||||
int *parent, *start, *nb, *fill, *comp, *stamp;
|
||||
double *pos, *tmp, *acc;
|
||||
float *ds;
|
||||
int nb_n, comp_n;
|
||||
} Work;
|
||||
static void work_free(Work *w, int T, int W)
|
||||
{
|
||||
usize t=(usize)(T ? T : 1),v=(usize)(W ? W : 1);
|
||||
io_free(w->furn,t);io_free(w->keep,t);io_free(w->owner,t);io_free(w->parent,v*sizeof(int));
|
||||
io_free(w->start,(v+1)*sizeof(int));io_free(w->nb,(usize)(w->nb_n ? w->nb_n : 1)*sizeof(int));
|
||||
io_free(w->fill,v*sizeof(int));io_free(w->comp,v*sizeof(int));io_free(w->stamp,v*sizeof(int));
|
||||
io_free(w->pos,v*3*sizeof(double));io_free(w->tmp,v*3*sizeof(double));
|
||||
io_free(w->acc,(usize)(w->comp_n ? w->comp_n : 1)*12*sizeof(double));io_free(w->ds,t*sizeof(float));
|
||||
}
|
||||
|
||||
/* Heads binned on a horizontal grid with cells of at least the walked radius: the 3 x 3 cells around a point hold
|
||||
* every head within the radius, so this gives the same result as testing every head, with far fewer tests. */
|
||||
typedef struct { double x0, z0, cell; int nx, nz; int *start, *list; } HeadGrid;
|
||||
static int head_cell(const HeadGrid *g, double v, double v0) { return (int)dfloor((v-v0)/g->cell); }
|
||||
static int head_grid(HeadGrid *g, const Vec *heads, int n, double radius)
|
||||
{
|
||||
double x0=1e300,x1=-1e300,z0=1e300,z1=-1e300;
|
||||
for(int i=0;i<n;i++) { x0=heads[i].x<x0 ? heads[i].x : x0;x1=heads[i].x>x1 ? heads[i].x : x1;
|
||||
z0=heads[i].z<z0 ? heads[i].z : z0;z1=heads[i].z>z1 ? heads[i].z : z1; }
|
||||
g->cell=radius;
|
||||
if((x1-x0)/256>g->cell) g->cell=(x1-x0)/256;
|
||||
if((z1-z0)/256>g->cell) g->cell=(z1-z0)/256;
|
||||
g->x0=x0;g->z0=z0;g->nx=head_cell(g,x1,x0)+1;g->nz=head_cell(g,z1,z0)+1;
|
||||
g->start=alloc((usize)(g->nx*g->nz+1)*sizeof(int));g->list=alloc((usize)n*sizeof(int));
|
||||
int *fill=alloc((usize)(g->nx*g->nz)*sizeof(int));
|
||||
if(!g->start || !g->list || !fill) { io_free(fill,(usize)(g->nx*g->nz)*sizeof(int));return 0; }
|
||||
for(int i=0;i<n;i++) g->start[head_cell(g,heads[i].z,z0)*g->nx+head_cell(g,heads[i].x,x0)+1]++;
|
||||
for(int c=0;c<g->nx*g->nz;c++) g->start[c+1]+=g->start[c];
|
||||
for(int i=0;i<n;i++) {
|
||||
int c=head_cell(g,heads[i].z,z0)*g->nx+head_cell(g,heads[i].x,x0);
|
||||
g->list[g->start[c]+fill[c]++]=i;
|
||||
}
|
||||
io_free(fill,(usize)(g->nx*g->nz)*sizeof(int));
|
||||
return 1;
|
||||
}
|
||||
static void head_grid_free(HeadGrid *g, int n)
|
||||
{
|
||||
io_free(g->start,(usize)(g->nx*g->nz+1)*sizeof(int));io_free(g->list,(usize)n*sizeof(int));
|
||||
}
|
||||
static int walked_hit(const HeadGrid *g, const Vec *heads, double x, double y, double z, double r2)
|
||||
{
|
||||
int cx=head_cell(g,x,g->x0),cz=head_cell(g,z,g->z0);
|
||||
for(int v=cz-1;v<=cz+1;v++) for(int u=cx-1;u<=cx+1;u++) {
|
||||
if(u<0 || v<0 || u>=g->nx || v>=g->nz) continue;
|
||||
for(int k=g->start[v*g->nx+u];k<g->start[v*g->nx+u+1];k++) {
|
||||
const Vec *h=&heads[g->list[k]];
|
||||
if(y<h->y+WALKED_ABOVE && (x-h->x)*(x-h->x)+(z-h->z)*(z-h->z)<=r2) return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *refine_run(const Refine *o, const Room *room, const Prep *P, const RVol *vols, int nvols,
|
||||
const Vec *heads, int nheads, Vec **out_v, u32 **out_i, int *out_n, Report *rep)
|
||||
{
|
||||
int T=P->T,W=P->W;
|
||||
const float *cen=P->cen,*fn=P->fn,*area=P->area;
|
||||
const int *F=P->F;
|
||||
Work w={0};
|
||||
const char *error=0;
|
||||
*out_v=0;*out_i=0;*out_n=0;
|
||||
memset(rep,0,sizeof(*rep));rep->triangles=T;
|
||||
usize tb=(usize)(T ? T : 1),wb=(usize)(W ? W : 1);
|
||||
w.furn=alloc(tb);w.keep=alloc(tb);w.owner=alloc(tb);w.ds=alloc(tb*sizeof(float));
|
||||
w.parent=alloc(wb*sizeof(int));w.pos=alloc(wb*3*sizeof(double));w.stamp=alloc(wb*sizeof(int));
|
||||
if(!w.furn || !w.keep || !w.owner || !w.ds || !w.parent || !w.pos || !w.stamp) { error="refine allocation failed";goto done; }
|
||||
memcpy(w.pos,P->pos,(usize)W*3*sizeof(double));
|
||||
double *pos=w.pos;
|
||||
|
||||
/* furniture flags: the ruled boxes (+10 cm) */
|
||||
Box boxes[MAX_VOLUMES];
|
||||
for(int i=0;i<nvols;i++) {
|
||||
double a=vols[i].yaw*3.14159265358979323846/180;
|
||||
boxes[i]=(Box){dcos(a),dsin(a),&vols[i]};
|
||||
}
|
||||
for(int i=0;i<nvols;i++) {
|
||||
const RVol *v=&vols[i];
|
||||
for(int t=0;t<T;t++) {
|
||||
double lx,lz,y=cen[3*t+1];
|
||||
box_local(&boxes[i],cen[3*t],cen[3*t+2],&lx,&lz);
|
||||
if(dab(lx)<=v->w/2.+FURNITURE_PAD && dab(lz)<=v->d/2.+FURNITURE_PAD &&
|
||||
y<=v->top+FURNITURE_PAD && y>=v->top-v->h-FURNITURE_PAD) w.furn[t]=1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 1. planes: seen walls, floor, ceiling; offsets re-estimated (median of facing triangles within the band) */
|
||||
RPlane planes[MAX_OUTLINE+2];int np=0;
|
||||
for(int i=0;i<room->nwalls;i++) {
|
||||
const RWall *wl=&room->walls[i];
|
||||
if(wl->invisible) continue;
|
||||
double dx=wl->x1-wl->x0,dz=wl->z1-wl->z0,L=dhypot(dx,dz);
|
||||
if(!(L>0)) continue;
|
||||
RPlane *pl=&planes[np++];
|
||||
*pl=(RPlane){0,i,{-dz/L,0,dx/L},0,0,wl->x0,wl->z0,dx/L,dz/L,L};
|
||||
pl->off=pl->n[0]*wl->x0+pl->n[2]*wl->z0;
|
||||
}
|
||||
planes[np++]=(RPlane){1,-1,{0,1,0},room->floor,0,0,0,0,0,0};
|
||||
planes[np++]=(RPlane){2,-1,{0,-1,0},-(double)room->ceiling,0,0,0,0,0,0};
|
||||
double cos_lim=dcos(o->facing_deg*3.14159265358979323846/180);
|
||||
#define FACING(pl,t) ((pl)->n[0]*fn[3*(t)]+(pl)->n[1]*fn[3*(t)+1]+(pl)->n[2]*fn[3*(t)+2]>cos_lim)
|
||||
for(int k=0;k<np;k++) {
|
||||
RPlane *pl=&planes[k];int n=0;
|
||||
for(int t=0;t<T;t++) {
|
||||
if(w.furn[t]) continue;
|
||||
double d=s_dist(pl,cen+3*t);
|
||||
if(dab(d)<o->band && FACING(pl,t) && in_ext(pl,cen+3*t,o->band)) w.ds[n++]=(float)d;
|
||||
}
|
||||
if(n>MIN_SHIFT_SAMPLES) { nth(w.ds,n,n>>1);pl->shift=w.ds[n>>1]; }
|
||||
pl->off+=pl->shift;
|
||||
rep->shifts[rep->nshifts++]=(Shift){pl->kind,pl->wall,(float)pl->shift};
|
||||
}
|
||||
double floor_y=planes[np-2].off,ceil_y=-planes[np-1].off;
|
||||
rep->floor_y=(float)floor_y;rep->ceiling_y=(float)ceil_y;
|
||||
|
||||
/* 2. structural triangles: behind a surface = band; in front = band_in (walls; the full band in the wall's top
|
||||
* strip), floor_in, ceil_in */
|
||||
for(int t=0;t<T;t++) w.owner[t]=-1;
|
||||
if(o->walls) for(int t=0;t<T;t++) {
|
||||
if(w.furn[t]) continue;
|
||||
double best=1e300;
|
||||
for(int k=0;k<np;k++) {
|
||||
const RPlane *pl=&planes[k];double d=s_dist(pl,cen+3*t);
|
||||
double front=!pl->kind ? (cen[3*t+1]>ceil_y-o->top_strip ? o->band : o->band_in) :
|
||||
pl->kind==2 ? o->ceil_in : o->floor_in;
|
||||
if(d>-o->band && d<front && dab(d)<best && FACING(pl,t) && in_ext(pl,cen+3*t,o->band)) {
|
||||
best=dab(d);w.owner[t]=(signed char)k;
|
||||
}
|
||||
}
|
||||
}
|
||||
#undef FACING
|
||||
for(int t=0;t<T;t++) w.keep[t]=1;
|
||||
/* erase spheres */
|
||||
for(int e=0;e<o->nerase;e++) {
|
||||
const Sphere *s=&o->erase[e];
|
||||
for(int t=0;t<T;t++) {
|
||||
double dx=cen[3*t]-s->x,dy=cen[3*t+1]-s->y,dz=cen[3*t+2]-s->z;
|
||||
if(w.keep[t] && dx*dx+dy*dy+dz*dz<=(double)s->r*s->r) { w.keep[t]=0;rep->erased++; }
|
||||
}
|
||||
}
|
||||
/* the walked volume: a column under each head position, never inside furniture */
|
||||
if(o->walked>0 && nheads) {
|
||||
HeadGrid g={0};
|
||||
if(!head_grid(&g,heads,nheads,o->walked)) { head_grid_free(&g,nheads);error="refine allocation failed";goto done; }
|
||||
double r2=(double)o->walked*o->walked;
|
||||
for(int t=0;t<T;t++) {
|
||||
if(!w.keep[t] || w.furn[t]) continue;
|
||||
double y=cen[3*t+1];
|
||||
if(y<room->floor+WALKED_BELOW) continue;
|
||||
if(walked_hit(&g,heads,cen[3*t],y,cen[3*t+2],r2)) { w.keep[t]=0;rep->walked++; }
|
||||
}
|
||||
head_grid_free(&g,nheads);
|
||||
} else if(o->walked>0) rep->walked_skipped=1;
|
||||
for(int t=0;t<T;t++) if(w.owner[t]>=0) rep->structural++;
|
||||
if(o->walls==2) {
|
||||
for(int t=0;t<T;t++) if(w.owner[t]>=0) w.keep[t]=0;
|
||||
if(o->near>0) for(int t=0;t<T;t++) {
|
||||
if(!w.keep[t] || w.furn[t]) continue;
|
||||
for(int k=0;k<np;k++) if(dab(s_dist(&planes[k],cen+3*t))<o->near && in_ext(&planes[k],cen+3*t,o->near)) {
|
||||
w.keep[t]=0;rep->near++;break;
|
||||
}
|
||||
}
|
||||
} else if(o->walls==1) {
|
||||
/* snap: a vertex goes onto the plane of the last structural triangle that uses it */
|
||||
int *vown=w.stamp;
|
||||
for(int i=0;i<W;i++) vown[i]=-1;
|
||||
for(int t=0;t<T;t++) if(w.owner[t]>=0) for(int j=0;j<3;j++) vown[F[3*t+j]]=w.owner[t];
|
||||
for(int i=0;i<W;i++) {
|
||||
if(vown[i]<0) continue;
|
||||
const RPlane *pl=&planes[vown[i]];
|
||||
double d=pl->n[0]*pos[3*i]+pl->n[1]*pos[3*i+1]+pl->n[2]*pos[3*i+2]-pl->off;
|
||||
for(int k=0;k<3;k++) pos[3*i+k]-=d*pl->n[k];
|
||||
}
|
||||
}
|
||||
/* 2b. furniture tops: upward scan near a box's top, inside its footprint, goes onto the top */
|
||||
if(o->tops>0) {
|
||||
for(int i=0;i<W;i++) w.stamp[i]=0;
|
||||
for(int i=0;i<nvols;i++) {
|
||||
const RVol *v=&vols[i];int mark=i+1;
|
||||
for(int t=0;t<T;t++) {
|
||||
if(!w.keep[t]) continue;
|
||||
const double *a=pos+3*F[3*t],*b=pos+3*F[3*t+1],*c=pos+3*F[3*t+2];
|
||||
double cy=(a[1]+b[1]+c[1])/3;
|
||||
if(dab(cy-v->top)>=o->tops) continue;
|
||||
double lx,lz;
|
||||
box_local(&boxes[i],(a[0]+b[0]+c[0])/3,(a[2]+b[2]+c[2])/3,&lx,&lz);
|
||||
if(dab(lx)>v->w/2. || dab(lz)>v->d/2.) continue;
|
||||
double ux=b[0]-a[0],uy=b[1]-a[1],uz=b[2]-a[2],wx=c[0]-a[0],wy=c[1]-a[1],wz=c[2]-a[2];
|
||||
double nx=uy*wz-uz*wy,ny=uz*wx-ux*wz,nz=ux*wy-uy*wx,l=droot(nx*nx+ny*ny+nz*nz);
|
||||
if(ny/(l>0 ? l : 1)<=TOP_NORMAL_Y) continue;
|
||||
for(int j=0;j<3;j++) w.stamp[F[3*t+j]]=mark;
|
||||
rep->tops++;
|
||||
}
|
||||
for(int k=0;k<W;k++) if(w.stamp[k]==mark) pos[3*k+1]=v->top;
|
||||
}
|
||||
}
|
||||
/* 3. clip outside the room (unseen walls allow clip_open more) and above/below the planes */
|
||||
if(o->clip<1 || o->clip_open<2) for(int t=0;t<T;t++) {
|
||||
if(!w.keep[t]) continue;
|
||||
double allow,out=outside_info(room,cen[3*t],cen[3*t+2],o->clip_open,&allow),y=cen[3*t+1];
|
||||
if(out>o->clip+allow || y>ceil_y+o->clip || y<floor_y-o->clip) { w.keep[t]=0;rep->outside++; }
|
||||
}
|
||||
/* 4. islands: connected pieces below the minimum area */
|
||||
if(o->island>0) {
|
||||
double *comp=w.tmp=alloc(wb*3*sizeof(double));
|
||||
if(!comp) { error="refine allocation failed";goto done; }
|
||||
unite(w.parent,F,w.keep,T,W);
|
||||
for(int t=0;t<T;t++) if(w.keep[t]) comp[find(w.parent,F[3*t])]+=area[t];
|
||||
for(int t=0;t<T;t++)
|
||||
if(w.keep[t] && comp[find(w.parent,F[3*t])]<o->island && !(o->protect && w.furn[t])) { w.keep[t]=0;rep->islands++; }
|
||||
io_free(w.tmp,wb*3*sizeof(double));w.tmp=0;
|
||||
}
|
||||
/* 4b. thin floating pieces: elongated (PCA), thin, small, not furniture */
|
||||
if(o->tubes>0) {
|
||||
unite(w.parent,F,w.keep,T,W);
|
||||
w.comp=alloc(wb*sizeof(int));
|
||||
if(!w.comp) { error="refine allocation failed";goto done; }
|
||||
for(int i=0;i<W;i++) w.comp[i]=-1;
|
||||
int nc=0;
|
||||
for(int t=0;t<T;t++) if(w.keep[t]) { int r=find(w.parent,F[3*t]);if(w.comp[r]<0) w.comp[r]=nc++; }
|
||||
w.comp_n=nc;
|
||||
w.acc=alloc((usize)(nc ? nc : 1)*12*sizeof(double));
|
||||
if(!w.acc) { error="refine allocation failed";goto done; }
|
||||
for(int t=0;t<T;t++) {
|
||||
if(!w.keep[t]) continue;
|
||||
double *a=w.acc+12*w.comp[find(w.parent,F[3*t])];
|
||||
for(int j=0;j<3;j++) {
|
||||
const double *q=pos+3*F[3*t+j];double x=q[0],y=q[1],z=q[2];
|
||||
a[0]++;a[1]+=x;a[2]+=y;a[3]+=z;a[4]+=x*x;a[5]+=x*y;a[6]+=x*z;a[7]+=y*y;a[8]+=y*z;a[9]+=z*z;
|
||||
}
|
||||
a[10]+=area[t];a[11]+=w.furn[t] ? 1 : 0;
|
||||
}
|
||||
u8 *drop=(u8 *)w.stamp; /* nc <= T bytes fit in the W ints */
|
||||
for(int c=0;c<nc;c++) {
|
||||
double *a=w.acc+12*c,n=a[0],tris=n/3;drop[c]=0;
|
||||
if(tris<TUBE_MIN_TRIANGLES || a[11]/tris>TUBE_MAX_FURNITURE || a[10]>=TUBE_MAX_AREA) continue;
|
||||
double mx_=a[1]/n,my=a[2]/n,mz=a[3]/n;
|
||||
double C[3][3]={{a[4]/n-mx_*mx_,a[5]/n-mx_*my,a[6]/n-mx_*mz},{0,a[7]/n-my*my,a[8]/n-my*mz},{0,0,a[9]/n-mz*mz}};
|
||||
C[1][0]=C[0][1];C[2][0]=C[0][2];C[2][1]=C[1][2];
|
||||
double ev[3];eig3(C,ev);
|
||||
for(int k=0;k<3;k++) ev[k]=droot(ev[k]>1e-12 ? ev[k] : 1e-12);
|
||||
for(int i=0;i<2;i++) for(int j=0;j<2-i;j++) if(ev[j]>ev[j+1]) { double s=ev[j];ev[j]=ev[j+1];ev[j+1]=s; }
|
||||
if(ev[2]/(ev[1]>1e-6 ? ev[1] : 1e-6)>=o->tubes && ev[1]<=o->tube_thick) drop[c]=1;
|
||||
}
|
||||
for(int t=0;t<T;t++) if(w.keep[t] && drop[w.comp[find(w.parent,F[3*t])]]) { w.keep[t]=0;rep->tubes++; }
|
||||
}
|
||||
/* 5. Taubin smoothing over the kept triangles (lambda/mu pairs keep the volume) */
|
||||
if(o->smooth>0) {
|
||||
w.start=alloc((wb+1)*sizeof(int));w.fill=alloc(wb*sizeof(int));w.tmp=alloc(wb*3*sizeof(double));
|
||||
if(!w.start || !w.fill || !w.tmp) { error="refine allocation failed";goto done; }
|
||||
for(int t=0;t<T;t++) if(w.keep[t]) for(int j=0;j<3;j++) w.start[F[3*t+j]+1]+=2;
|
||||
for(int i=0;i<W;i++) w.start[i+1]+=w.start[i];
|
||||
w.nb_n=w.start[W];
|
||||
w.nb=alloc((usize)(w.nb_n ? w.nb_n : 1)*sizeof(int));
|
||||
if(!w.nb) { error="refine allocation failed";goto done; }
|
||||
for(int i=0;i<W;i++) w.fill[i]=w.start[i];
|
||||
for(int t=0;t<T;t++) if(w.keep[t]) for(int j=0;j<3;j++) {
|
||||
int a=F[3*t+j],b=F[3*t+(j+1)%3],c=F[3*t+(j+2)%3];
|
||||
w.nb[w.fill[a]++]=b;w.nb[w.fill[a]++]=c;
|
||||
}
|
||||
for(int it=0;it<o->smooth;it++) for(int pass=0;pass<2;pass++) {
|
||||
double f=pass ? TAUBIN_MU : TAUBIN_LAMBDA;
|
||||
for(int i=0;i<W;i++) {
|
||||
int s0=w.start[i],e=w.start[i+1];
|
||||
if(s0==e) { for(int k=0;k<3;k++) w.tmp[3*i+k]=pos[3*i+k];continue; }
|
||||
double x=0,y=0,z=0;
|
||||
for(int k=s0;k<e;k++) { const double *q=pos+3*w.nb[k];x+=q[0];y+=q[1];z+=q[2]; }
|
||||
int n=e-s0;
|
||||
w.tmp[3*i]=pos[3*i]+f*(x/n-pos[3*i]);w.tmp[3*i+1]=pos[3*i+1]+f*(y/n-pos[3*i+1]);
|
||||
w.tmp[3*i+2]=pos[3*i+2]+f*(z/n-pos[3*i+2]);
|
||||
}
|
||||
memcpy(pos,w.tmp,(usize)W*3*sizeof(double));
|
||||
}
|
||||
}
|
||||
/* 6. output: the kept scan, then flat panels for seen walls, the floor and the ceiling */
|
||||
for(int t=0;t<T;t++) rep->kept+=w.keep[t];
|
||||
Vec panel[(MAX_OUTLINE*2+MAX_OUTLINE*2)*3];int npanel=0;
|
||||
if(o->walls==2) {
|
||||
for(int k=0;k<np;k++) {
|
||||
const RPlane *pl=&planes[k];
|
||||
if(pl->kind) continue;
|
||||
const RWall *wl=&room->walls[pl->wall];double sh=pl->shift;
|
||||
double a0x=wl->x0+pl->n[0]*sh,a0z=wl->z0+pl->n[2]*sh,a1x=wl->x1+pl->n[0]*sh,a1z=wl->z1+pl->n[2]*sh;
|
||||
Vec q[4]={vec((float)a0x,(float)floor_y,(float)a0z),vec((float)a1x,(float)floor_y,(float)a1z),
|
||||
vec((float)a1x,(float)ceil_y,(float)a1z),vec((float)a0x,(float)ceil_y,(float)a0z)};
|
||||
double ux=a1x-a0x,uz=a1z-a0z,vy=ceil_y-floor_y;
|
||||
int ok=(-uz*vy)*pl->n[0]+(ux*vy)*pl->n[2]>0;
|
||||
int order[2][6]={{0,1,2,0,2,3},{1,0,3,1,3,2}};
|
||||
for(int j=0;j<6;j++) panel[npanel*3+j]=q[order[!ok][j]];
|
||||
npanel+=2;
|
||||
}
|
||||
int ears[MAX_OUTLINE][3];
|
||||
int ne=ear_clip(room->outline,room->noutline,ears);
|
||||
for(int e=0;e<ne;e++) {
|
||||
DPt A=room->outline[ears[e][0]],B=room->outline[ears[e][1]],C=room->outline[ears[e][2]];
|
||||
panel[npanel*3]=vec((float)A.x,(float)floor_y,(float)A.z);panel[npanel*3+1]=vec((float)C.x,(float)floor_y,(float)C.z);
|
||||
panel[npanel*3+2]=vec((float)B.x,(float)floor_y,(float)B.z);npanel++;
|
||||
panel[npanel*3]=vec((float)A.x,(float)ceil_y,(float)A.z);panel[npanel*3+1]=vec((float)B.x,(float)ceil_y,(float)B.z);
|
||||
panel[npanel*3+2]=vec((float)C.x,(float)ceil_y,(float)C.z);npanel++;
|
||||
}
|
||||
}
|
||||
rep->panels=npanel;
|
||||
int room_left=MAX_TRIANGLES-npanel,kept_out=rep->kept<room_left ? rep->kept : room_left;
|
||||
rep->truncated=rep->kept-kept_out;
|
||||
*out_v=io_alloc((usize)MAX_MESH_VERTICES*sizeof(Vec));
|
||||
*out_i=io_alloc((usize)MAX_TRIANGLES*3*sizeof(u32));
|
||||
if(!*out_v || !*out_i) { error="refine allocation failed";goto done; }
|
||||
int o_n=0;
|
||||
for(int t=0;t<T && o_n<kept_out*3;t++) {
|
||||
if(!w.keep[t]) continue;
|
||||
for(int j=0;j<3;j++) {
|
||||
const double *q=pos+3*F[3*t+j];
|
||||
(*out_v)[o_n]=vec((float)q[0],(float)q[1],(float)q[2]);(*out_i)[o_n]=(u32)o_n;o_n++;
|
||||
}
|
||||
}
|
||||
for(int k=0;k<npanel*3;k++) { (*out_v)[o_n]=panel[k];(*out_i)[o_n]=(u32)o_n;o_n++; }
|
||||
*out_n=o_n;
|
||||
done:
|
||||
if(error) {
|
||||
io_free(*out_v,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(*out_i,(usize)MAX_TRIANGLES*3*sizeof(u32));
|
||||
*out_v=0;*out_i=0;*out_n=0;
|
||||
}
|
||||
work_free(&w,T,W);
|
||||
return error;
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#ifndef FP_ROOMSCAN_REFINE_H
|
||||
#define FP_ROOMSCAN_REFINE_H
|
||||
#include "scan.h"
|
||||
|
||||
/* Room cleanup after the mesh (docs/mr/scan.md "Refinement"): the scanner's unrefined output is kept as
|
||||
* room-raw.txt + room-mesh-raw.bin, the refined one replaces room.txt + room-mesh.bin, scan.json holds the head
|
||||
* path, the settings used, the per-pass report, the label-rule decisions and door/closet suggestions. */
|
||||
|
||||
#define MAX_EXTRA_TEXT 8192
|
||||
#define MAX_LABEL 24
|
||||
#define MAX_REASON 240
|
||||
#define MAX_RAW_FILE (16u+(usize)MAX_MESH_VERTICES*12u+(usize)MAX_TRIANGLES*12u)
|
||||
#define MAX_JSON_FILE (1u<<20)
|
||||
|
||||
/* Growable text buffer (io_alloc backed). ok=0 after an allocation failure or the cap. */
|
||||
typedef struct { char *data; usize n, cap, limit; int ok; } Buf;
|
||||
void buf_init(Buf *b, usize limit);
|
||||
void buf_free(Buf *b);
|
||||
void put(Buf *b, const char *s);
|
||||
void put_n(Buf *b, const char *s, usize n);
|
||||
void put_int(Buf *b, i64 v);
|
||||
void put_u64(Buf *b, u64 v);
|
||||
void put_dec(Buf *b, float v); /* 4 decimals, the room.txt number format */
|
||||
void put_fixed(Buf *b, double v, int places); /* JavaScript's toFixed */
|
||||
void put_round(Buf *b, double v); /* JavaScript's Math.round */
|
||||
|
||||
typedef struct { double x, z; } DPt;
|
||||
/* Room numbers are doubles parsed exactly from their decimal text, as the reference page reads them. */
|
||||
typedef struct { double x0, z0, x1, z1, y0, y1; int invisible; char id[33]; } RWall;
|
||||
typedef struct { double cx, cz, top, yaw, w, d, h; char label[MAX_LABEL]; char id[33]; } RVol;
|
||||
typedef struct {
|
||||
u64 universe, time;
|
||||
char source[16];
|
||||
char room_id[33], floor_id[33], ceiling_id[33], mesh_id[33];
|
||||
double floor, ceiling;
|
||||
DPt outline[MAX_OUTLINE];
|
||||
int noutline;
|
||||
RWall walls[MAX_OUTLINE];
|
||||
int nwalls;
|
||||
RVol vols[MAX_VOLUMES];
|
||||
int nvols;
|
||||
char mesh_name[64];
|
||||
u32 mesh_nv, mesh_ni;
|
||||
int has_mesh;
|
||||
char extra[MAX_EXTRA_TEXT]; /* records this scanner does not model (plane, ...), kept verbatim */
|
||||
usize nextra;
|
||||
} Room;
|
||||
|
||||
/* The mesh with its vertices welded at 1 mm (as the reference page does), per-triangle normals/centroids/areas. */
|
||||
typedef struct {
|
||||
int T, W;
|
||||
double *pos;
|
||||
int *F;
|
||||
float *fn, *cen, *area;
|
||||
} Prep;
|
||||
|
||||
typedef struct { int kind, wall; float shift; } Shift; /* kind: 0 wall, 1 floor, 2 ceiling */
|
||||
typedef struct {
|
||||
int triangles, kept, structural, near, outside, islands, tubes, erased, walked, tops, panels, truncated;
|
||||
int walked_skipped;
|
||||
float floor_y, ceiling_y;
|
||||
Shift shifts[MAX_OUTLINE+2];
|
||||
int nshifts;
|
||||
} Report;
|
||||
typedef struct {
|
||||
char from[MAX_LABEL], to[MAX_LABEL], suggest[8], reason[MAX_REASON];
|
||||
RVol box;
|
||||
int wall;
|
||||
float along;
|
||||
} Label;
|
||||
|
||||
/* roomio.c */
|
||||
const char *room_from_scan(Scan *s, Room *r);
|
||||
const char *room_parse(const char *text, usize n, Room *r);
|
||||
int room_text(const Room *r, Buf *b);
|
||||
const char *read_file(const char *path, usize limit, u8 **data, usize *size);
|
||||
const char *publish(const char *dir, const char *name, const void *data, usize size);
|
||||
int join_path(char *out, const char *dir, const char *name);
|
||||
const char *mesh_parse(const u8 *data, usize size, Vec **vertices, u32 **indices, int *nv, int *ni);
|
||||
const char *mesh_bytes(const Vec *vertices, const u32 *indices, int nv, int ni, u8 **data, usize *size);
|
||||
void heads_json(Buf *b, const Vec *heads, int n);
|
||||
int heads_parse(const char *text, usize n, Vec *heads);
|
||||
void settings_json(Buf *b, const Refine *o);
|
||||
void report_json(Buf *b, const Report *r, int nlabels, int changed, int removed, int nsuggest);
|
||||
|
||||
/* refine.c */
|
||||
const char *prep_mesh(Prep *p, const Vec *vertices, const u32 *indices, int nv, int ni);
|
||||
void prep_free(Prep *p);
|
||||
const char *refine_run(const Refine *o, const Room *room, const Prep *p, const RVol *vols, int nvols,
|
||||
const Vec *heads, int nheads, Vec **out_v, u32 **out_i, int *out_n, Report *rep);
|
||||
double dfloor(double x);
|
||||
double droot(double x);
|
||||
double dsin(double x);
|
||||
double dcos(double x);
|
||||
double dhypot(double a, double b);
|
||||
|
||||
/* rules.c */
|
||||
int rules_apply(const Room *room, const Prep *p, RVol *out, int *nout, Label *labels);
|
||||
|
||||
#endif
|
||||
@@ -1,476 +0,0 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Room files: the room.txt writer/reader (docs/mr/ROOM_FORMAT.md), mesh sidecars, atomic publishing and the JSON
|
||||
* pieces of scan.json. Every number goes through put_dec, so a room read back from its own text is bit-identical
|
||||
* to the one the scan path refines (that is what makes --refine-only reproduce a scan's refined output). */
|
||||
#include "refine.h"
|
||||
|
||||
void buf_init(Buf *b, usize limit) { *b=(Buf){0};b->limit=limit;b->ok=1; }
|
||||
void buf_free(Buf *b) { io_free(b->data,b->cap);*b=(Buf){0}; }
|
||||
void put_n(Buf *b, const char *s, usize n)
|
||||
{
|
||||
if(!b->ok) return;
|
||||
if(b->n+n+1>b->cap) {
|
||||
usize cap=b->cap ? b->cap : 256;
|
||||
while(cap<b->n+n+1) cap*=2;
|
||||
if(cap>b->limit) cap=b->limit;
|
||||
if(cap<b->n+n+1) { b->ok=0;return; }
|
||||
char *data=io_alloc(cap);
|
||||
if(!data) { b->ok=0;return; }
|
||||
if(b->n) memcpy(data,b->data,b->n);
|
||||
io_free(b->data,b->cap);b->data=data;b->cap=cap;
|
||||
}
|
||||
memcpy(b->data+b->n,s,n);b->n+=n;b->data[b->n]=0;
|
||||
}
|
||||
void put(Buf *b, const char *s) { usize n=0;while(s[n]) n++;put_n(b,s,n); }
|
||||
void put_u64(Buf *b, u64 value)
|
||||
{
|
||||
char rev[24];int n=0;
|
||||
do { rev[n++]=(char)('0'+value%10);value/=10; } while(value);
|
||||
char out[24];for(int i=0;i<n;i++) out[i]=rev[n-1-i];
|
||||
put_n(b,out,(usize)n);
|
||||
}
|
||||
void put_int(Buf *b, i64 v) { if(v<0) { put(b,"-");v=-v; } put_u64(b,(u64)v); }
|
||||
void put_dec(Buf *b, float value)
|
||||
{
|
||||
/* Same arithmetic as the scanner's original writer: the --refine 0 output stays byte-for-byte unchanged. */
|
||||
if(value<0) { put(b,"-");value=-value; }
|
||||
u64 scaled=(u64)(value*10000.f+.5f);
|
||||
put_u64(b,scaled/10000);put(b,".");
|
||||
u64 fraction=scaled%10000;
|
||||
if(fraction<1000) put(b,"0");
|
||||
if(fraction<100) put(b,"0");
|
||||
if(fraction<10) put(b,"0");
|
||||
put_u64(b,fraction);
|
||||
}
|
||||
void put_fixed(Buf *b, double v, int places)
|
||||
{
|
||||
/* Exact, like Number.prototype.toFixed: v*10^places from the double's bits in 128-bit integers, ties up. */
|
||||
double scale=1;for(int i=0;i<places;i++) scale*=10;
|
||||
int negative=v<0;if(negative) v=-v;
|
||||
if(!(v<1e9)) v=0;
|
||||
union { double d; u64 u; } bits={v};
|
||||
int e=(int)((bits.u>>52)&0x7ff);
|
||||
u64 m=bits.u&((1ull<<52)-1);
|
||||
if(e) m|=1ull<<52;else e=1;
|
||||
e-=1075; /* v = m * 2^e */
|
||||
unsigned __int128 big=(unsigned __int128)m*(u64)scale;
|
||||
u64 scaled;
|
||||
if(e>=0) scaled=(u64)(big<<e);
|
||||
else if(e<=-127) scaled=0;
|
||||
else {
|
||||
unsigned __int128 half=(unsigned __int128)1<<(-e-1),rest=big&(((unsigned __int128)1<<-e)-1);
|
||||
scaled=(u64)(big>>-e)+(rest>=half);
|
||||
}
|
||||
if(negative && scaled) put(b,"-");
|
||||
put_u64(b,(u64)(scaled/(u64)scale));
|
||||
if(places) {
|
||||
put(b,".");u64 fraction=scaled%(u64)scale;
|
||||
for(u64 d=(u64)scale/10;d>1 && fraction<d;d/=10) put(b,"0");
|
||||
put_u64(b,fraction);
|
||||
}
|
||||
}
|
||||
void put_round(Buf *b, double v) { put_int(b,(i64)dfloor(v+.5)); }
|
||||
|
||||
static int uuid_hex(char out[33])
|
||||
{
|
||||
u8 bytes[16];if(!io_random(bytes,sizeof(bytes))) return 0;
|
||||
bytes[6]=(bytes[6]&15)|64;bytes[8]=(bytes[8]&63)|128;
|
||||
const char *hex="0123456789abcdef";
|
||||
for(int i=0;i<16;i++) { out[2*i]=hex[bytes[i]>>4];out[2*i+1]=hex[bytes[i]&15]; }
|
||||
out[32]=0;return 1;
|
||||
}
|
||||
static void copy(char *dst, const char *src, usize cap)
|
||||
{
|
||||
usize i=0;while(src[i] && i+1<cap) { dst[i]=src[i];i++; }
|
||||
dst[i]=0;
|
||||
}
|
||||
const char *room_from_scan(Scan *s, Room *r)
|
||||
{
|
||||
memset(r,0,sizeof(*r));
|
||||
r->universe=s->opt.universe;r->time=(u64)(io_time(0)/1000000000);copy(r->source,"scan",sizeof(r->source));
|
||||
if(!uuid_hex(r->room_id) || !uuid_hex(r->floor_id) || !uuid_hex(r->ceiling_id)) return "getrandom failed";
|
||||
r->floor=s->floor;r->ceiling=s->ceiling;r->noutline=s->noutline;
|
||||
for(int i=0;i<s->noutline;i++) {
|
||||
Pt a=s->outline[i],b=s->outline[(i+1)%s->noutline];
|
||||
r->outline[i]=(DPt){a.x,a.z};
|
||||
r->walls[i]=(RWall){a.x,a.z,b.x,b.z,s->floor,s->ceiling,s->invisible[i],{0}};
|
||||
if(!uuid_hex(r->walls[i].id)) return "getrandom failed";
|
||||
}
|
||||
r->nwalls=s->noutline;
|
||||
for(int i=0;i<s->nvolumes;i++) {
|
||||
Volume *v=&s->volumes[i];RVol *o=&r->vols[i];
|
||||
*o=(RVol){v->p.x,v->p.z,v->p.y,v->yaw,v->w,v->d,v->h,{0},{0}};
|
||||
copy(o->label,v->label,sizeof(o->label));
|
||||
if(!uuid_hex(o->id)) return "getrandom failed";
|
||||
}
|
||||
r->nvols=s->nvolumes;
|
||||
if(!uuid_hex(r->mesh_id)) return "getrandom failed";
|
||||
copy(r->mesh_name,"room-mesh.bin",sizeof(r->mesh_name));
|
||||
r->has_mesh=1;r->mesh_nv=(u32)s->nv;r->mesh_ni=(u32)s->ni;
|
||||
return 0;
|
||||
}
|
||||
static void sp(Buf *b, double v) { put(b," ");put_dec(b,(float)v); }
|
||||
int room_text(const Room *r, Buf *b)
|
||||
{
|
||||
put(b,"frameport-room 1\nuniverse ");put_u64(b,r->universe);
|
||||
put(b,"\nsource ");put(b,r->source);put(b,"\ntime ");put_u64(b,r->time);
|
||||
put(b,"\nroom ");put(b,r->room_id);
|
||||
put(b,"\nfloor ");put(b,r->floor_id);sp(b,r->floor);
|
||||
put(b,"\nceiling ");put(b,r->ceiling_id);sp(b,r->ceiling);
|
||||
put(b,"\noutline ");put_u64(b,(u64)r->noutline);
|
||||
for(int i=0;i<r->noutline;i++) { sp(b,r->outline[i].x);sp(b,r->outline[i].z); }
|
||||
put(b,"\n");
|
||||
for(int i=0;i<r->nwalls;i++) {
|
||||
const RWall *w=&r->walls[i];
|
||||
put(b,"wall ");put(b,w->id);sp(b,w->x0);sp(b,w->z0);sp(b,w->x1);sp(b,w->z1);sp(b,w->y0);sp(b,w->y1);
|
||||
if(w->invisible) put(b," INVISIBLE_WALL_FACE");
|
||||
put(b,"\n");
|
||||
}
|
||||
for(int i=0;i<r->nvols;i++) {
|
||||
const RVol *v=&r->vols[i];
|
||||
put(b,"volume ");put(b,v->id);put(b," ");put(b,v->label);
|
||||
sp(b,v->cx);sp(b,v->cz);sp(b,v->top);sp(b,v->yaw);sp(b,v->w);sp(b,v->d);sp(b,v->h);put(b,"\n");
|
||||
}
|
||||
put_n(b,r->extra,r->nextra);
|
||||
if(r->has_mesh) {
|
||||
put(b,"mesh ");put(b,r->mesh_id);put(b," ");put(b,r->mesh_name);put(b," ");
|
||||
put_u64(b,r->mesh_nv);put(b," ");put_u64(b,r->mesh_ni);put(b,"\n");
|
||||
}
|
||||
return b->ok;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ reading */
|
||||
typedef struct { char *p[64]; int n; } Fields;
|
||||
static int is_space(char c) { return c==' ' || c=='\t' || c=='\r'; }
|
||||
/* Semantic labels are Meta's upper-case names; anything else would also be unsafe in scan.json strings. */
|
||||
static int field_label(const char *p)
|
||||
{
|
||||
int n=0;
|
||||
for(;p[n];n++) if(!((p[n]>='A' && p[n]<='Z') || (p[n]>='0' && p[n]<='9') || p[n]=='_')) return 0;
|
||||
return n>0 && n<MAX_LABEL;
|
||||
}
|
||||
static int field_uuid(const char *p)
|
||||
{
|
||||
int n=0;
|
||||
for(;p[n];n++) if(!((p[n]>='0' && p[n]<='9') || (p[n]>='a' && p[n]<='f'))) return 0;
|
||||
return n==32;
|
||||
}
|
||||
/* Decimal text to double, correctly rounded for the room format's numbers (<= 17 significant digits): the
|
||||
* mantissa and the power of ten are exact doubles, so one division/multiplication rounds once, as parseFloat. */
|
||||
static int field_double(const char *p, double *out)
|
||||
{
|
||||
int negative=0,digits=0,exponent=0;u64 mantissa=0;
|
||||
if(*p=='-' || *p=='+') negative=*p++=='-';
|
||||
for(;*p>='0' && *p<='9';p++,digits++) { if(mantissa<100000000000000000ull) mantissa=mantissa*10+(u64)(*p-'0');else exponent++; }
|
||||
if(*p=='.') for(p++;*p>='0' && *p<='9';p++,digits++)
|
||||
if(mantissa<100000000000000000ull) { mantissa=mantissa*10+(u64)(*p-'0');exponent--; }
|
||||
if(!digits) return 0;
|
||||
if(*p=='e' || *p=='E') {
|
||||
p++;int sign=1,e=0,ed=0;
|
||||
if(*p=='-' || *p=='+') sign=*p++=='-' ? -1 : 1;
|
||||
for(;*p>='0' && *p<='9';p++,ed++) { e=e*10+*p-'0';if(e>400) return 0; }
|
||||
if(!ed) return 0;
|
||||
exponent+=sign*e;
|
||||
}
|
||||
if(*p) return 0;
|
||||
double v=(double)mantissa,scale=1;
|
||||
int e=exponent<0 ? -exponent : exponent;
|
||||
for(int i=0;i<e && scale<1e300;i++) scale*=10;
|
||||
v=exponent<0 ? v/scale : v*scale;
|
||||
if(!(v<=1e7)) return 0; /* also rejects NaN; room coordinates are metres */
|
||||
*out=negative ? -v : v;return 1;
|
||||
}
|
||||
static int field_u64(const char *p, u64 *out)
|
||||
{
|
||||
u64 n=0;if(!*p) return 0;
|
||||
for(;*p;p++) {
|
||||
if(*p<'0' || *p>'9') return 0;
|
||||
unsigned digit=(unsigned)(*p-'0');
|
||||
if(n>(~(u64)0-digit)/10) return 0;
|
||||
n=n*10+digit;
|
||||
}
|
||||
*out=n;return 1;
|
||||
}
|
||||
static int floats(Fields *f, int from, double *out, int n)
|
||||
{
|
||||
if(f->n<from+n) return 0;
|
||||
for(int i=0;i<n;i++) if(!field_double(f->p[from+i],&out[i])) return 0;
|
||||
return 1;
|
||||
}
|
||||
const char *room_parse(const char *text, usize n, Room *r)
|
||||
{
|
||||
static char line[16384];
|
||||
memset(r,0,sizeof(*r));
|
||||
int have_floor=0,have_ceiling=0,first=1;
|
||||
usize at=0;
|
||||
while(at<n) {
|
||||
usize start=at,len=0;
|
||||
while(at<n && text[at]!='\n') at++;
|
||||
len=at-start;at++;
|
||||
if(len>=sizeof(line)) return "room line too long";
|
||||
memcpy(line,text+start,len);line[len]=0;
|
||||
usize raw_len=len;
|
||||
for(usize i=0;i<len;i++) if(line[i]=='#') { line[i]=0;len=i;break; }
|
||||
Fields f={{0},0};
|
||||
char *p=line;
|
||||
while(*p) {
|
||||
while(is_space(*p)) *p++=0;
|
||||
if(!*p) break;
|
||||
if(f.n==64) return "room line has too many fields";
|
||||
f.p[f.n++]=p;
|
||||
while(*p && !is_space(*p)) p++;
|
||||
}
|
||||
if(first) {
|
||||
if(f.n!=2 || !same(f.p[0],"frameport-room") || !same(f.p[1],"1")) return "not a frameport-room 1 file";
|
||||
first=0;continue;
|
||||
}
|
||||
if(!f.n) continue;
|
||||
const char *k=f.p[0];
|
||||
if(same(k,"universe")) { if(f.n<2 || !field_u64(f.p[1],&r->universe)) return "invalid universe record"; }
|
||||
else if(same(k,"time")) { if(f.n<2 || !field_u64(f.p[1],&r->time)) return "invalid time record"; }
|
||||
else if(same(k,"source")) { if(f.n<2) return "invalid source record";copy(r->source,f.p[1],sizeof(r->source)); }
|
||||
else if(same(k,"room")) { if(f.n<2 || !field_uuid(f.p[1])) return "invalid room record";copy(r->room_id,f.p[1],33); }
|
||||
else if(same(k,"floor") || same(k,"ceiling")) {
|
||||
int c=same(k,"ceiling");
|
||||
if(f.n<3 || !field_uuid(f.p[1]) || !field_double(f.p[2],c ? &r->ceiling : &r->floor)) return "invalid floor/ceiling record";
|
||||
copy(c ? r->ceiling_id : r->floor_id,f.p[1],33);
|
||||
if(c) have_ceiling=1;else have_floor=1;
|
||||
} else if(same(k,"outline")) {
|
||||
u64 count;
|
||||
if(f.n<2 || !field_u64(f.p[1],&count) || count<3 || count>MAX_OUTLINE || (u64)f.n!=2+2*count) return "invalid outline record";
|
||||
for(int i=0;i<(int)count;i++)
|
||||
if(!field_double(f.p[2+2*i],&r->outline[i].x) || !field_double(f.p[3+2*i],&r->outline[i].z)) return "invalid outline record";
|
||||
r->noutline=(int)count;
|
||||
} else if(same(k,"wall")) {
|
||||
double v[6];
|
||||
if(r->nwalls==MAX_OUTLINE) return "too many walls";
|
||||
if(f.n<8 || !field_uuid(f.p[1]) || !floats(&f,2,v,6)) return "invalid wall record";
|
||||
RWall *w=&r->walls[r->nwalls++];
|
||||
*w=(RWall){v[0],v[1],v[2],v[3],v[4],v[5],f.n>8 && same(f.p[8],"INVISIBLE_WALL_FACE"),{0}};
|
||||
copy(w->id,f.p[1],33);
|
||||
} else if(same(k,"volume")) {
|
||||
double v[7];
|
||||
if(r->nvols==MAX_VOLUMES) return "too many volumes";
|
||||
if(f.n<10 || !field_uuid(f.p[1]) || !field_label(f.p[2]) || !floats(&f,3,v,7)) return "invalid volume record";
|
||||
RVol *o=&r->vols[r->nvols++];
|
||||
*o=(RVol){v[0],v[1],v[2],v[3],v[4],v[5],v[6],{0},{0}};
|
||||
copy(o->label,f.p[2],sizeof(o->label));copy(o->id,f.p[1],33);
|
||||
} else if(same(k,"mesh")) {
|
||||
u64 nv,ni;
|
||||
if(f.n<5 || !field_uuid(f.p[1]) || !field_u64(f.p[3],&nv) || !field_u64(f.p[4],&ni) ||
|
||||
nv>MAX_MESH_VERTICES || ni>(u64)MAX_TRIANGLES*3) return "invalid mesh record";
|
||||
for(const char *c=f.p[2];*c;c++) if(*c=='/' || *c=='\\') return "invalid mesh file name";
|
||||
copy(r->mesh_id,f.p[1],33);copy(r->mesh_name,f.p[2],sizeof(r->mesh_name));
|
||||
r->mesh_nv=(u32)nv;r->mesh_ni=(u32)ni;r->has_mesh=1;
|
||||
} else {
|
||||
/* Forward compatibility: unknown records (plane, ...) are kept as they were written. */
|
||||
if(r->nextra+raw_len+1>=MAX_EXTRA_TEXT) return "too many extra room records";
|
||||
memcpy(r->extra+r->nextra,text+start,raw_len);r->nextra+=raw_len;r->extra[r->nextra++]='\n';
|
||||
}
|
||||
}
|
||||
if(first) return "empty room file";
|
||||
if(!r->source[0]) copy(r->source,"scan",sizeof(r->source));
|
||||
if(!r->room_id[0] && !uuid_hex(r->room_id)) return "getrandom failed";
|
||||
if(!have_floor && !uuid_hex(r->floor_id)) return "getrandom failed";
|
||||
if(!have_ceiling) { if(!uuid_hex(r->ceiling_id)) return "getrandom failed";r->ceiling=r->floor+2.5; }
|
||||
if(!r->noutline && r->nwalls>=3) {
|
||||
double x0=1e10,x1=-1e10,z0=1e10,z1=-1e10;
|
||||
for(int i=0;i<r->nwalls;i++) {
|
||||
RWall *w=&r->walls[i];
|
||||
double lx=w->x0<w->x1 ? w->x0 : w->x1,hx=w->x0<w->x1 ? w->x1 : w->x0;
|
||||
double lz=w->z0<w->z1 ? w->z0 : w->z1,hz=w->z0<w->z1 ? w->z1 : w->z0;
|
||||
x0=lx<x0 ? lx : x0;x1=hx>x1 ? hx : x1;z0=lz<z0 ? lz : z0;z1=hz>z1 ? hz : z1;
|
||||
}
|
||||
r->outline[0]=(DPt){x0,z0};r->outline[1]=(DPt){x1,z0};r->outline[2]=(DPt){x1,z1};r->outline[3]=(DPt){x0,z1};
|
||||
r->noutline=4;
|
||||
}
|
||||
if(r->noutline<3) return "room has no outline";
|
||||
double twice=0;
|
||||
for(int i=0;i<r->noutline;i++) {
|
||||
DPt a=r->outline[i],b=r->outline[(i+1)%r->noutline];twice+=a.x*b.z-b.x*a.z;
|
||||
}
|
||||
if(twice<0) for(int i=0;i<r->noutline/2;i++) { /* readers reverse a negative outline (ROOM_FORMAT.md) */
|
||||
DPt t=r->outline[i];r->outline[i]=r->outline[r->noutline-1-i];r->outline[r->noutline-1-i]=t;
|
||||
}
|
||||
if(!r->nwalls) {
|
||||
for(int i=0;i<r->noutline;i++) {
|
||||
DPt a=r->outline[i],b=r->outline[(i+1)%r->noutline];
|
||||
r->walls[i]=(RWall){a.x,a.z,b.x,b.z,r->floor,r->ceiling,0,{0}};
|
||||
if(!uuid_hex(r->walls[i].id)) return "getrandom failed";
|
||||
}
|
||||
r->nwalls=r->noutline;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ files */
|
||||
int join_path(char *out, const char *dir, const char *name)
|
||||
{
|
||||
usize n=0;
|
||||
while(*dir) { if(n>=3900) return 0;out[n++]=*dir++; }
|
||||
if(n && out[n-1]!='/') out[n++]='/';
|
||||
while(*name) { if(n>=4095) return 0;out[n++]=*name++; }
|
||||
out[n]=0;return 1;
|
||||
}
|
||||
const char *read_file(const char *path, usize limit, u8 **data, usize *size)
|
||||
{
|
||||
*data=0;*size=0;
|
||||
int fd=io_open(path,0);
|
||||
if(fd<0) return "cannot open file";
|
||||
usize cap=limit+1;
|
||||
u8 *buf=io_alloc(cap);
|
||||
if(!buf) { io_close(fd);return "file allocation failed"; }
|
||||
usize n=0;
|
||||
for(;;) {
|
||||
long r=io_read(fd,buf+n,cap-n);
|
||||
if(r<0) { io_close(fd);io_free(buf,cap);return "cannot read file"; }
|
||||
if(!r) break;
|
||||
n+=(usize)r;
|
||||
if(n>limit) { io_close(fd);io_free(buf,cap);return "file too large"; }
|
||||
}
|
||||
io_close(fd);
|
||||
/* The caller frees with io_free(data, limit+1); the size is the buffer, not the content. */
|
||||
*data=buf;*size=n;return 0;
|
||||
}
|
||||
const char *publish(const char *dir, const char *name, const void *data, usize size)
|
||||
{
|
||||
char id[33],tmpname[256],final[4096],tmp[4096];
|
||||
if(!uuid_hex(id)) return "getrandom failed";
|
||||
usize n=0;
|
||||
for(const char *p=name;*p && n<180;p++) tmpname[n++]=*p;
|
||||
tmpname[n++]='.';
|
||||
for(int i=0;i<32;i++) tmpname[n++]=id[i];
|
||||
for(const char *p=".tmp";*p;p++) tmpname[n++]=*p;
|
||||
tmpname[n]=0;
|
||||
if(!join_path(final,dir,name) || !join_path(tmp,dir,tmpname)) return "output path too long";
|
||||
int fd=io_open(tmp,1);
|
||||
if(fd<0) return "cannot open temporary file";
|
||||
int ok=io_write(fd,data,size);
|
||||
if(io_close(fd)) ok=0;
|
||||
if(!ok) { io_remove(tmp);return "cannot write temporary file"; }
|
||||
if(io_rename(tmp,final)) { io_remove(tmp);return "cannot publish file"; }
|
||||
return 0;
|
||||
}
|
||||
static u32 rd32(const u8 *p) { return (u32)p[0]|(u32)p[1]<<8|(u32)p[2]<<16|(u32)p[3]<<24; }
|
||||
static void wr32(u8 *p, u32 v) { for(int i=0;i<4;i++) p[i]=(u8)(v>>(8*i)); }
|
||||
const char *mesh_parse(const u8 *data, usize size, Vec **vertices, u32 **indices, int *nv, int *ni)
|
||||
{
|
||||
*vertices=0;*indices=0;*nv=*ni=0;
|
||||
if(size<16 || data[0]!='F' || data[1]!='P' || data[2]!='R' || data[3]!='M' || rd32(data+4)!=1) return "invalid mesh header";
|
||||
u32 v=rd32(data+8),i=rd32(data+12);
|
||||
if(v>MAX_MESH_VERTICES || i>(u32)MAX_TRIANGLES*3 || i%3 || size!=16+(usize)v*12+(usize)i*4) return "invalid mesh size";
|
||||
Vec *pv=io_alloc((usize)MAX_MESH_VERTICES*sizeof(Vec));
|
||||
u32 *pi=io_alloc((usize)MAX_TRIANGLES*3*sizeof(u32));
|
||||
if(!pv || !pi) { io_free(pv,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(pi,(usize)MAX_TRIANGLES*3*sizeof(u32));return "mesh allocation failed"; }
|
||||
for(u32 k=0;k<v*3;k++) {
|
||||
union { u32 u; float f; } c={rd32(data+16+4*(usize)k)};
|
||||
if(!finite(c.f)) { io_free(pv,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(pi,(usize)MAX_TRIANGLES*3*sizeof(u32));return "non-finite mesh vertex"; }
|
||||
((float *)pv)[k]=c.f;
|
||||
}
|
||||
for(u32 k=0;k<i;k++) {
|
||||
pi[k]=rd32(data+16+(usize)v*12+4*(usize)k);
|
||||
if(pi[k]>=v) { io_free(pv,(usize)MAX_MESH_VERTICES*sizeof(Vec));io_free(pi,(usize)MAX_TRIANGLES*3*sizeof(u32));return "mesh index outside vertex array"; }
|
||||
}
|
||||
*vertices=pv;*indices=pi;*nv=(int)v;*ni=(int)i;return 0;
|
||||
}
|
||||
const char *mesh_bytes(const Vec *vertices, const u32 *indices, int nv, int ni, u8 **data, usize *size)
|
||||
{
|
||||
*size=16+(usize)nv*12+(usize)ni*4;
|
||||
*data=io_alloc(*size);
|
||||
if(!*data) return "mesh allocation failed";
|
||||
u8 *p=*data;
|
||||
p[0]='F';p[1]='P';p[2]='R';p[3]='M';wr32(p+4,1);wr32(p+8,(u32)nv);wr32(p+12,(u32)ni);
|
||||
/* Both supported targets are little-endian IEEE float32, like the scanner's original writer. */
|
||||
memcpy(p+16,vertices,(usize)nv*12);
|
||||
memcpy(p+16+(usize)nv*12,indices,(usize)ni*4);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ scan.json */
|
||||
void heads_json(Buf *b, const Vec *heads, int n)
|
||||
{
|
||||
put(b,"[");
|
||||
for(int i=0;i<n;i++) {
|
||||
if(i) put(b,",");
|
||||
put(b,"[");put_dec(b,heads[i].x);put(b,",");put_dec(b,heads[i].y);put(b,",");put_dec(b,heads[i].z);put(b,"]");
|
||||
}
|
||||
put(b,"]");
|
||||
}
|
||||
int heads_parse(const char *text, usize n, Vec *heads)
|
||||
{
|
||||
/* Finds "heads":[[x,y,z],...] in a scan.json written by heads_json; anything else gives no heads. */
|
||||
const char *key="\"heads\"";
|
||||
usize at=0;
|
||||
for(;at+7<=n;at++) {
|
||||
usize k=0;while(k<7 && text[at+k]==key[k]) k++;
|
||||
if(k==7) break;
|
||||
}
|
||||
if(at+7>n) return 0;
|
||||
char *p=(char *)text+at+7;const char *end=text+n;
|
||||
while(p<end && (is_space(*p) || *p=='\n' || *p==':')) p++;
|
||||
if(p>=end || *p!='[') return 0;
|
||||
p++;
|
||||
int count=0;
|
||||
while(p<end && count<MAX_HEADS) {
|
||||
while(p<end && (is_space(*p) || *p=='\n' || *p==',')) p++;
|
||||
if(p>=end || *p==']') break;
|
||||
if(*p!='[') return 0;
|
||||
p++;
|
||||
float v[3];
|
||||
for(int i=0;i<3;i++) {
|
||||
while(p<end && (is_space(*p) || *p==',')) p++;
|
||||
if(!parse_number(&p,&v[i])) return 0;
|
||||
}
|
||||
while(p<end && is_space(*p)) p++;
|
||||
if(p>=end || *p!=']') return 0;
|
||||
p++;
|
||||
heads[count++]=vec(v[0],v[1],v[2]);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
static void key_float(Buf *b, const char *k, float v) { put(b,",\"");put(b,k);put(b,"\":");put_dec(b,v); }
|
||||
static void key_int(Buf *b, const char *k, i64 v, int first)
|
||||
{
|
||||
if(!first) put(b,",");
|
||||
put(b,"\"");put(b,k);put(b,"\":");put_int(b,v);
|
||||
}
|
||||
void settings_json(Buf *b, const Refine *o)
|
||||
{
|
||||
static const char *walls[]={"off","snap","panels"};
|
||||
put(b,"{");key_int(b,"refine",o->refine,1);
|
||||
put(b,",\"walls\":\"");put(b,walls[o->walls]);put(b,"\"");
|
||||
key_float(b,"band",o->band);key_float(b,"facing_deg",o->facing_deg);key_float(b,"band_in",o->band_in);
|
||||
key_float(b,"floor_in",o->floor_in);key_float(b,"ceil_in",o->ceil_in);key_float(b,"top_strip",o->top_strip);
|
||||
key_float(b,"near",o->near);key_float(b,"clip",o->clip);key_float(b,"clip_open",o->clip_open);
|
||||
key_float(b,"island",o->island);key_int(b,"smooth",o->smooth,0);key_float(b,"tops",o->tops);
|
||||
key_int(b,"protect",o->protect,0);key_float(b,"tubes",o->tubes);key_float(b,"tube_thick",o->tube_thick);
|
||||
key_float(b,"walked",o->walked);key_int(b,"label_rules",o->label_rules,0);
|
||||
put(b,",\"erase\":[");
|
||||
for(int i=0;i<o->nerase;i++) {
|
||||
const Sphere *e=&o->erase[i];
|
||||
if(i) put(b,",");
|
||||
put(b,"[");put_dec(b,e->x);put(b,",");put_dec(b,e->y);put(b,",");put_dec(b,e->z);put(b,",");put_dec(b,e->r);put(b,"]");
|
||||
}
|
||||
put(b,"]}");
|
||||
}
|
||||
void report_json(Buf *b, const Report *r, int nlabels, int changed, int removed, int nsuggest)
|
||||
{
|
||||
static const char *kinds[]={"wall","floor","ceiling"};
|
||||
put(b,"{");key_int(b,"triangles",r->triangles,1);key_int(b,"kept",r->kept,0);
|
||||
key_int(b,"structural",r->structural,0);key_int(b,"near_walls",r->near,0);key_int(b,"outside",r->outside,0);
|
||||
key_int(b,"small_pieces",r->islands,0);key_int(b,"thin_pieces",r->tubes,0);key_int(b,"erased",r->erased,0);
|
||||
key_int(b,"walked",r->walked,0);key_int(b,"tops",r->tops,0);key_int(b,"panel_triangles",r->panels,0);
|
||||
key_int(b,"truncated",r->truncated,0);
|
||||
put(b,",\"walked_skipped\":");put(b,r->walked_skipped ? "true" : "false");
|
||||
key_float(b,"floor_y",r->floor_y);key_float(b,"ceiling_y",r->ceiling_y);
|
||||
key_int(b,"boxes",nlabels,0);key_int(b,"relabelled",changed,0);key_int(b,"removed_boxes",removed,0);
|
||||
key_int(b,"suggestions",nsuggest,0);
|
||||
put(b,",\"plane_shifts\":[");
|
||||
for(int i=0;i<r->nshifts;i++) {
|
||||
if(i) put(b,",");
|
||||
put(b,"{\"kind\":\"");put(b,kinds[r->shifts[i].kind]);put(b,"\"");
|
||||
if(!r->shifts[i].kind) { put(b,",\"wall\":");put_int(b,r->shifts[i].wall); }
|
||||
key_float(b,"shift",r->shifts[i].shift);put(b,"}");
|
||||
}
|
||||
put(b,"]}");
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Label rules on the scanner's furniture boxes (the owner's tuned rules, 2026-10-18): boxes outside the room,
|
||||
* boxes that merged several things, boxes nested in a bigger one are removed; the rest are relabelled BED, COUCH,
|
||||
* TABLE, STORAGE, SCREEN or OTHER by shape; tall thin slabs from the floor become door/closet suggestions (never
|
||||
* applied automatically). Every decision is reported with a reason in scan.json. */
|
||||
#include "refine.h"
|
||||
|
||||
#define R_INSIDE .1f /* m: a centre further outside the outline is behind a wall */
|
||||
#define R_NEAR_WALL .35f
|
||||
#define R_BED_AREA 2.f
|
||||
#define R_BED_LONG 1.8f
|
||||
#define R_BED_MAX 3.f
|
||||
#define R_BED_TOP .65f
|
||||
#define R_BED_MIN_TOP .25f
|
||||
#define R_COUCH_MIN_TOP .35f
|
||||
#define R_COUCH_TOP .6f
|
||||
#define R_COUCH_LONG 1.4f
|
||||
#define R_TABLE_MIN .6f
|
||||
#define R_TABLE_MAX 1.1f
|
||||
#define R_STORAGE_H 1.1f
|
||||
#define R_STORAGE_D .25f
|
||||
#define R_SCREEN_BOTTOM .3f
|
||||
#define R_SCREEN_TOP 2.2f
|
||||
#define R_SCREEN_ASPECT 1.2f
|
||||
#define R_SCREEN_PARALLEL 15.f /* degrees off the nearest wall */
|
||||
#define R_SCREEN_FLAT .03f /* m: spread of the scan's depth inside the box */
|
||||
#define R_THIN .22f
|
||||
#define R_TALL_MIN 1.8f
|
||||
#define R_TALL_FLOOR .3f /* m: a tall slab's bottom at most this far above the floor */
|
||||
#define R_TALL_BEHIND .5f
|
||||
#define R_DOOR_MAX 1.1f
|
||||
#define R_MERGE .6f /* share of a box's footprint inside another that counts as held by it */
|
||||
#define R_MERGE_APART 1.f
|
||||
#define R_NESTED .75f
|
||||
#define FLAT_MIN_TRIANGLES 30
|
||||
#define FLAT_DEPTH_PAD .05f
|
||||
#define FOOTPRINT_STEP .05
|
||||
|
||||
typedef struct { double cs, sn; } Rot;
|
||||
static Rot rot(const RVol *v) { double a=v->yaw*3.14159265358979323846/180; return (Rot){dcos(a),dsin(a)}; }
|
||||
static void local(const RVol *v, Rot r, double x, double z, double *lx, double *lz)
|
||||
{
|
||||
double dx=x-v->cx,dz=z-v->cz;
|
||||
*lx=r.cs*dx-r.sn*dz;*lz=r.sn*dx+r.cs*dz;
|
||||
}
|
||||
static double dabs(double x) { return x<0 ? -x : x; }
|
||||
static void corners(const RVol *v, Rot r, double out[4][2])
|
||||
{
|
||||
static const int sg[4][2]={{-1,-1},{1,-1},{1,1},{-1,1}};
|
||||
for(int k=0;k<4;k++) {
|
||||
double x=sg[k][0]*v->w/2.,z=sg[k][1]*v->d/2.;
|
||||
out[k][0]=v->cx+r.cs*x+r.sn*z;out[k][1]=v->cz-r.sn*x+r.cs*z;
|
||||
}
|
||||
}
|
||||
static int point_in_poly(double x, double z, const DPt *poly, int n)
|
||||
{
|
||||
int inside=0;
|
||||
for(int i=0,j=n-1;i<n;j=i++) {
|
||||
double xi=poly[i].x,zi=poly[i].z,xj=poly[j].x,zj=poly[j].z;
|
||||
if((zi>z)!=(zj>z) && x<(xj-xi)*(z-zi)/(zj-zi+1e-12)+xi) inside=!inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
typedef struct { double dist, t, L, ux, uz; int i; } Near;
|
||||
static Near nearest_wall(const Room *r, double x, double z)
|
||||
{
|
||||
Near best={1e300,0,0,1,0,-1};
|
||||
for(int i=0;i<r->nwalls;i++) {
|
||||
const RWall *w=&r->walls[i];
|
||||
double abx=w->x1-w->x0,abz=w->z1-w->z0,l2=abx*abx+abz*abz,L=dhypot(abx,abz);
|
||||
if(!(l2>0)) continue;
|
||||
double t=((x-w->x0)*abx+(z-w->z0)*abz)/l2;
|
||||
t=t<0 ? 0 : t>1 ? 1 : t;
|
||||
double dx=x-(w->x0+t*abx),dz=z-(w->z0+t*abz),d=dhypot(dx,dz);
|
||||
if(d<best.dist) best=(Near){d,t,L,abx/L,abz/L,i};
|
||||
}
|
||||
return best;
|
||||
}
|
||||
static double outside(const Room *r, double x, double z)
|
||||
{
|
||||
return point_in_poly(x,z,r->outline,r->noutline) ? 0 : nearest_wall(r,x,z).dist;
|
||||
}
|
||||
/* Spread (std) of the scan's depth inside a thin upright box; <0 = too little scan to tell. */
|
||||
static double flatness(const Prep *p, const RVol *v)
|
||||
{
|
||||
Rot r=rot(v);
|
||||
double sum=0,sum2=0;int n=0;
|
||||
for(int pass=0;pass<2;pass++) {
|
||||
double mean=n ? sum/n : 0;
|
||||
for(int t=0;t<p->T;t++) {
|
||||
double y=p->cen[3*t+1];
|
||||
if(y>v->top || y<v->top-v->h) continue;
|
||||
double lx,lz;local(v,r,p->cen[3*t],p->cen[3*t+2],&lx,&lz);
|
||||
double a=lx,b=lz,half=v->w/2.,depth=v->d/2.+FLAT_DEPTH_PAD;
|
||||
if(v->d>v->w) { a=lz;b=lx;half=v->d/2.;depth=v->w/2.+FLAT_DEPTH_PAD; }
|
||||
if(dabs(a)<=half && dabs(b)<=depth) {
|
||||
if(!pass) { sum+=b;n++; } else sum2+=(b-mean)*(b-mean);
|
||||
}
|
||||
}
|
||||
if(!pass && n<FLAT_MIN_TRIANGLES) return -1;
|
||||
}
|
||||
return droot(sum2/n);
|
||||
}
|
||||
static void cm(Buf *b, double x) { put_round(b,x*100);put(b," cm"); }
|
||||
static void lower(Buf *b, const char *s)
|
||||
{
|
||||
char c[2]={0,0};
|
||||
for(;*s;s++) { c[0]=*s>='A' && *s<='Z' ? (char)(*s+32) : *s;put(b,c); }
|
||||
}
|
||||
static void set_text(char *dst, usize cap, Buf *b)
|
||||
{
|
||||
usize n=b->ok ? b->n : 0;
|
||||
if(n>=cap) { /* cut at a UTF-8 boundary */
|
||||
n=cap-1;
|
||||
while(n && ((u8)b->data[n]&0xc0)==0x80) n--;
|
||||
}
|
||||
if(n) memcpy(dst,b->data,n);
|
||||
dst[n]=0;
|
||||
}
|
||||
static void copy(char *dst, const char *src, usize cap)
|
||||
{
|
||||
usize i=0;while(src[i] && i+1<cap) { dst[i]=src[i];i++; }
|
||||
dst[i]=0;
|
||||
}
|
||||
/* label "" = remove, suggest "door"/"closet" or "" */
|
||||
static void classify(const Room *room, const Prep *p, const RVol *v, char *label, char *suggest, Buf *why)
|
||||
{
|
||||
double out=outside(room,v->cx,v->cz);
|
||||
double top=v->top-room->floor,bottom=top-v->h,lng=v->w>v->d ? v->w : v->d,shrt=v->w<v->d ? v->w : v->d;
|
||||
label[0]=suggest[0]=0;
|
||||
if(shrt<R_THIN && v->h>=R_TALL_MIN && bottom<=R_TALL_FLOOR && out<=R_TALL_BEHIND) {
|
||||
int door=lng<=R_DOOR_MAX;
|
||||
copy(suggest,door ? "door" : "closet",8);
|
||||
put(why,"tall thin slab from the floor (");put_fixed(why,v->h,2);put(why," m)");
|
||||
if(out>0) { put(why,", ");cm(why,out);put(why," behind the wall line"); }
|
||||
put(why,door ? ": a door or closet door?" : ": a closet front?");
|
||||
return;
|
||||
}
|
||||
if(out>R_INSIDE) { put(why,"centre ");cm(why,out);put(why," outside the room (behind a wall)");return; }
|
||||
Near wall=nearest_wall(room,v->cx,v->cz);
|
||||
if(shrt<R_THIN) {
|
||||
Rot r=rot(v);
|
||||
double ax=v->w>=v->d ? r.cs : r.sn,az=v->w>=v->d ? -r.sn : r.cs;
|
||||
double c=dabs(ax*wall.ux+az*wall.uz);c=c<1 ? c : 1;
|
||||
double off=angle((float)droot(1-c*c),(float)c)*180/PI;
|
||||
double fl=flatness(p,v);
|
||||
Buf b;buf_init(&b,1024);int n=0;
|
||||
if(bottom<R_SCREEN_BOTTOM) { put(&b,"starts ");cm(&b,bottom>0 ? bottom : 0);put(&b," off the floor");n++; }
|
||||
if(top>R_SCREEN_TOP) { if(n++) put(&b,", ");put(&b,"top at ");put_fixed(&b,top,2);put(&b," m"); }
|
||||
if(lng/v->h<R_SCREEN_ASPECT) { if(n++) put(&b,", ");put(&b,"taller than wide (");put_fixed(&b,lng/v->h,2);put(&b,")"); }
|
||||
if(wall.dist<R_NEAR_WALL && off>R_SCREEN_PARALLEL) { if(n++) put(&b,", ");put_round(&b,off);put(&b,"\xc2\xb0 off the wall"); }
|
||||
if(fl>=0 && fl>R_SCREEN_FLAT) { if(n++) put(&b,", ");put(&b,"not flat (");cm(&b,fl);put(&b," spread)"); }
|
||||
if(!n) {
|
||||
copy(label,"SCREEN",MAX_LABEL);put(why,"thin, raised, screen-shaped");
|
||||
if(fl>=0) { put(why,", flat (");cm(why,fl);put(why,")"); }
|
||||
} else { put(why,"thin upright slab: ");put_n(why,b.data,b.n); }
|
||||
buf_free(&b);
|
||||
return;
|
||||
}
|
||||
if(top<=R_BED_TOP && v->w*v->d>=R_BED_AREA && lng>=R_BED_LONG && lng<=R_BED_MAX && top>=R_BED_MIN_TOP) {
|
||||
copy(label,"BED",MAX_LABEL);put_fixed(why,(double)v->w*v->d,1);put(why," m\xc2\xb2, ");put_fixed(why,lng,1);
|
||||
put(why," m long, top ");put_fixed(why,top,2);put(why," m");return;
|
||||
}
|
||||
if(top>=R_COUCH_MIN_TOP && top<=R_COUCH_TOP && lng>=R_COUCH_LONG) {
|
||||
copy(label,"COUCH",MAX_LABEL);put(why,"seat-height top ");put_fixed(why,top,2);put(why," m, ");
|
||||
put_fixed(why,lng,1);put(why," m long");return;
|
||||
}
|
||||
if(top>=R_TABLE_MIN && top<=R_TABLE_MAX) { copy(label,"TABLE",MAX_LABEL);put(why,"top ");put_fixed(why,top,2);put(why," m");return; }
|
||||
if(v->h>=R_STORAGE_H && shrt>=R_STORAGE_D && wall.dist<R_NEAR_WALL) {
|
||||
copy(label,"STORAGE",MAX_LABEL);put_fixed(why,v->h,2);put(why," m tall by a wall");return;
|
||||
}
|
||||
if(top>R_TABLE_MAX) { put_fixed(why,top,2);put(why," m tall, matches nothing");return; }
|
||||
copy(label,"OTHER",MAX_LABEL);put(why,"top ");put_fixed(why,top,2);put(why," m, ");put_fixed(why,lng,1);
|
||||
put(why," \xc3\x97 ");put_fixed(why,shrt,1);put(why," m");
|
||||
}
|
||||
/* Share of a's and b's footprint they have in common, sampled on a 5 cm grid over both (as the reference). */
|
||||
static void footprint_overlap(const RVol *a, const RVol *b, double *fa, double *fb)
|
||||
{
|
||||
Rot ra=rot(a),rb=rot(b);
|
||||
double ca[4][2],cb[4][2],x0=1e300,x1=-1e300,z0=1e300,z1=-1e300;
|
||||
corners(a,ra,ca);corners(b,rb,cb);
|
||||
for(int k=0;k<4;k++) {
|
||||
x0=ca[k][0]<x0 ? ca[k][0] : x0;x1=ca[k][0]>x1 ? ca[k][0] : x1;z0=ca[k][1]<z0 ? ca[k][1] : z0;z1=ca[k][1]>z1 ? ca[k][1] : z1;
|
||||
x0=cb[k][0]<x0 ? cb[k][0] : x0;x1=cb[k][0]>x1 ? cb[k][0] : x1;z0=cb[k][1]<z0 ? cb[k][1] : z0;z1=cb[k][1]>z1 ? cb[k][1] : z1;
|
||||
}
|
||||
double na=0,nb=0,both=0;
|
||||
for(double x=x0;x<=x1;x+=FOOTPRINT_STEP) for(double z=z0;z<=z1;z+=FOOTPRINT_STEP) {
|
||||
double ax,az,bx,bz;local(a,ra,x,z,&ax,&az);local(b,rb,x,z,&bx,&bz);
|
||||
int ia=dabs(ax)<=a->w/2. && dabs(az)<=a->d/2.,ib=dabs(bx)<=b->w/2. && dabs(bz)<=b->d/2.;
|
||||
na+=ia;nb+=ib;both+=ia && ib;
|
||||
}
|
||||
*fa=both/(na>1 ? na : 1);*fb=both/(nb>1 ? nb : 1);
|
||||
}
|
||||
static int boxes_apart(const RVol *a, const RVol *b)
|
||||
{
|
||||
/* footprints whose bounding circles don't touch share nothing (skips the sampling, same result) */
|
||||
double ra=droot((double)a->w*a->w+(double)a->d*a->d)/2,rb=droot((double)b->w*b->w+(double)b->d*b->d)/2;
|
||||
double dx=a->cx-b->cx,dz=a->cz-b->cz;
|
||||
return dx*dx+dz*dz>(ra+rb+.1)*(ra+rb+.1);
|
||||
}
|
||||
int rules_apply(const Room *room, const Prep *p, RVol *out, int *nout, Label *labels)
|
||||
{
|
||||
int n=room->nvols;const RVol *box=room->vols;
|
||||
typedef struct { int i, j; double fa, fb; } Ov;
|
||||
static Ov ov[MAX_VOLUMES*(MAX_VOLUMES-1)/2];
|
||||
int nov=0,removed[MAX_VOLUMES]={0};
|
||||
static char reason[MAX_VOLUMES][MAX_REASON];
|
||||
for(int i=0;i<n;i++) for(int j=i+1;j<n;j++) {
|
||||
if(boxes_apart(&box[i],&box[j])) continue;
|
||||
double fa,fb;footprint_overlap(&box[i],&box[j],&fa,&fb);
|
||||
if(fa>0 || fb>0) ov[nov++]=(Ov){i,j,fa,fb};
|
||||
}
|
||||
/* merged: a box that holds most of two boxes far apart covers several things */
|
||||
for(int k=0;k<n;k++) {
|
||||
int held[MAX_VOLUMES],nh=0;
|
||||
for(int q=0;q<nov;q++) if(ov[q].i==k && ov[q].fb>=R_MERGE) held[nh++]=ov[q].j;
|
||||
for(int q=0;q<nov;q++) if(ov[q].j==k && ov[q].fa>=R_MERGE) held[nh++]=ov[q].i;
|
||||
for(int x=0;x<nh && !removed[k];x++) for(int y=0;y<nh && !removed[k];y++) {
|
||||
const RVol *a=&box[held[x]],*b=&box[held[y]];
|
||||
double dx=a->cx-b->cx,dz=a->cz-b->cz,d=dhypot(dx,dz);
|
||||
if(held[x]<held[y] && d>=R_MERGE_APART) {
|
||||
Buf t;buf_init(&t,1024);
|
||||
put(&t,"holds a ");lower(&t,a->label);put(&t," and a ");lower(&t,b->label);put(&t," ");
|
||||
put_fixed(&t,d,1);put(&t," m apart: several things merged");
|
||||
set_text(reason[k],MAX_REASON,&t);buf_free(&t);removed[k]=1;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* nested: mostly inside a bigger box with a lower top = part of it */
|
||||
for(int q=0;q<nov;q++) for(int side=0;side<2;side++) {
|
||||
int s=side ? ov[q].j : ov[q].i,g=side ? ov[q].i : ov[q].j;double f=side ? ov[q].fb : ov[q].fa;
|
||||
const RVol *S=&box[s],*G=&box[g];
|
||||
if(removed[s] || removed[g] || f<R_NESTED || (double)S->w*S->d>=(double)G->w*G->d || S->top>G->top+.05) continue;
|
||||
Buf t;buf_init(&t,1024);
|
||||
put_round(&t,f*100);put(&t,"% inside the bigger ");lower(&t,G->label);put(&t," box: part of it");
|
||||
set_text(reason[s],MAX_REASON,&t);buf_free(&t);removed[s]=1;
|
||||
}
|
||||
int count=0;
|
||||
for(int i=0;i<n;i++) {
|
||||
Label *l=&labels[i];
|
||||
memset(l,0,sizeof(*l));
|
||||
copy(l->from,box[i].label,MAX_LABEL);l->box=box[i];l->wall=-1;
|
||||
if(removed[i]) { copy(l->reason,reason[i],MAX_REASON);continue; }
|
||||
Buf why;buf_init(&why,1024);
|
||||
classify(room,p,&box[i],l->to,l->suggest,&why);
|
||||
set_text(l->reason,MAX_REASON,&why);buf_free(&why);
|
||||
if(l->suggest[0]) {
|
||||
Near nw=nearest_wall(room,box[i].cx,box[i].cz);
|
||||
l->wall=nw.i;l->along=(float)nw.t;
|
||||
}
|
||||
if(l->to[0]) { out[count]=box[i];copy(out[count].label,l->to,MAX_LABEL);count++; }
|
||||
}
|
||||
*nout=count;
|
||||
return n;
|
||||
}
|
||||
@@ -13,8 +13,6 @@
|
||||
#define MAX_VOLUMES 128
|
||||
#define MAX_TRIANGLES 200000
|
||||
#define MAX_MESH_VERTICES 600000
|
||||
#define MAX_HEADS 2000
|
||||
#define MAX_ERASE 64
|
||||
#define PI 3.14159265358979323846f
|
||||
|
||||
typedef struct { float x, y, z; } Vec;
|
||||
@@ -31,17 +29,8 @@ typedef struct {
|
||||
int used;
|
||||
} Plane;
|
||||
typedef struct { Vec p; float yaw, w, d, h; const char *label; } Volume;
|
||||
typedef struct { float x, y, z, r; } Sphere;
|
||||
/* Refinement + label rules (refine.c, rules.c); defaults = the owner's tuned settings (2026-10-18). */
|
||||
typedef struct {
|
||||
int refine, walls, protect, label_rules, smooth, nerase; /* walls: 0 off, 1 snap, 2 panels */
|
||||
float band, facing_deg, band_in, floor_in, ceil_in, top_strip, near, clip, clip_open, island, tops;
|
||||
float tubes, tube_thick, walked;
|
||||
Sphere erase[MAX_ERASE];
|
||||
} Refine;
|
||||
typedef struct {
|
||||
const char *input, *out, *hint, *refine_only;
|
||||
Refine rf;
|
||||
const char *input, *out, *hint;
|
||||
u64 universe;
|
||||
float voxel, ceiling_default, truncation, max_edge, grazing, range, depth_jump, depth_ratio, min_views;
|
||||
float patch_residual;
|
||||
@@ -67,13 +56,10 @@ typedef struct {
|
||||
const char *warnings[24];
|
||||
int nwarnings, hint_used;
|
||||
u32 rng;
|
||||
Vec heads[MAX_HEADS]; /* head path of tracked frames; the stride doubles whenever the buffer is full */
|
||||
int nheads;
|
||||
unsigned head_stride, head_frames;
|
||||
Vec *vertices;
|
||||
u32 *indices;
|
||||
int nv, ni;
|
||||
i64 t0, tread, tfuse, tmodel, tmesh, trefine;
|
||||
i64 t0, tread, tfuse, tmodel, tmesh;
|
||||
} Scan;
|
||||
|
||||
static inline float ab(float x) { return x < 0 ? -x : x; }
|
||||
@@ -103,6 +89,5 @@ void finish_fusion(Scan *s);
|
||||
const char *make_model(Scan *s);
|
||||
const char *make_mesh(Scan *s);
|
||||
int same(const char *a, const char *b);
|
||||
int parse_number(char **cursor, float *value);
|
||||
|
||||
#endif
|
||||
@@ -12,10 +12,6 @@ BOX_OUTLINE = [(-2, -1.5), (2, -1.5), (2, 1.5), (-2, 1.5)]
|
||||
L_OUTLINE = [(-2, -2), (2, -2), (2, 0), (0, 0), (0, 2), (-2, 2)]
|
||||
TABLE = ((-.6, 0, -.25), (1.2, .75, .7))
|
||||
COUCH = ((.6, 0, .95), (1.6, .65, .65))
|
||||
# Clutter for the refinement tests: a thin pole floating in the air (a stereo streak) and a column where the user
|
||||
# stood (a "ghost" of the user's own body or a passer-by in the depth stream).
|
||||
TUBE = ((1.3, 1.35, -.5), (.12, .12, 1.4))
|
||||
GHOST = ((.35, .45, 0), (.3, .9, .3))
|
||||
ROOMVIEW_WIDTH, ROOMVIEW_HEIGHT = 132, 128
|
||||
ROOMVIEW_NEAR, ROOMVIEW_FAR = .3, 4.0
|
||||
|
||||
@@ -108,7 +104,7 @@ def cast(eye, direction, polygon, ceiling, furniture, missing_wall):
|
||||
def render(path: Path, *, shape="box", frames=30, width=None, height=None, noise=.004,
|
||||
holes=.025, smear=.012, floor_bias=.008, no_ceiling=False, seed=7,
|
||||
yaw=0.0, offset=(0.0, 0.0, 0.0), missing_wall=-1, objects=True, profile="roomview",
|
||||
invalid_start=None, smoothing=3, extra=()):
|
||||
invalid_start=None, smoothing=3):
|
||||
"""Each frame has its own moving pose; no ground-truth room data is put in the dump."""
|
||||
rng = random.Random(seed)
|
||||
roomview = profile == "roomview"
|
||||
@@ -117,7 +113,7 @@ def render(path: Path, *, shape="box", frames=30, width=None, height=None, noise
|
||||
invalid_start = (2 if roomview else 0) if invalid_start is None else invalid_start
|
||||
polygon = BOX_OUTLINE if shape == "box" else L_OUTLINE
|
||||
ceiling = 2.6
|
||||
furniture = ([TABLE, COUCH] if objects and shape == "box" else []) + list(extra)
|
||||
furniture = [TABLE, COUCH] if objects and shape == "box" else []
|
||||
rotation = (0, math.sin(yaw / 2), 0, math.cos(yaw / 2))
|
||||
rays = list(fan(width, height, roomview))
|
||||
with path.open("wb") as out:
|
||||
|
||||
@@ -8,7 +8,6 @@ Quest builds are "installed" (the 8 largest).
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import threading
|
||||
@@ -31,98 +30,61 @@ def frame_state() -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def fake_room_mesh(room, cell: float = 0.2, seed: int = 7):
|
||||
def fake_room_mesh(room, cell: float = 0.25, seed: int = 7):
|
||||
"""A made-up scan of `room`: its floor, walls and furniture as noisy triangle grids, wound like a real scan's
|
||||
(counter-clockwise seen from inside the room). Like a real scan it has holes where the headset sees nothing:
|
||||
the floor under furniture that stands on it, and window glass."""
|
||||
from frameport.mr.room import Mesh, plane_corners
|
||||
(counter-clockwise seen from inside the room)."""
|
||||
from frameport.mr.room import Mesh
|
||||
from frameport.mr.view3d import box_corners
|
||||
|
||||
rnd = random.Random(seed)
|
||||
verts, idx = [], []
|
||||
|
||||
def surface(o, u, v, noise=0.012, keep=None): # o + s*u + t*v, normal = u x v; keep(point) False = a hole
|
||||
def surface(o, u, v): # o + s*u + t*v, normal = u x v
|
||||
lu = max(1, round((sum(c * c for c in u) ** 0.5) / cell))
|
||||
lv = max(1, round((sum(c * c for c in v) ** 0.5) / cell))
|
||||
base = len(verts)
|
||||
for i in range(lv + 1):
|
||||
for j in range(lu + 1):
|
||||
n = noise if 0 < i < lv and 0 < j < lu else 0.0
|
||||
n = 0.015 if 0 < i < lv and 0 < j < lu else 0.0
|
||||
verts.append(tuple(o[k] + u[k] * j / lu + v[k] * i / lv + rnd.uniform(-n, n) for k in range(3)))
|
||||
for i in range(lv):
|
||||
for j in range(lu):
|
||||
mid = tuple(o[k] + u[k] * (j + 0.5) / lu + v[k] * (i + 0.5) / lv for k in range(3))
|
||||
if keep is not None and not keep(mid):
|
||||
continue
|
||||
a = base + i * (lu + 1) + j
|
||||
b, c, d = a + 1, a + lu + 1, a + lu + 2
|
||||
idx.extend((a, b, d, a, d, c))
|
||||
|
||||
def inside(vol, x, z): # (x, z) on the box's footprint
|
||||
a = math.radians(vol.yaw_deg)
|
||||
dx, dz = x - vol.cx, z - vol.cz
|
||||
lx, lz = math.cos(a) * dx - math.sin(a) * dz, math.sin(a) * dx + math.cos(a) * dz
|
||||
return abs(lx) < vol.w / 2 and abs(lz) < vol.d / 2
|
||||
|
||||
y0 = room.floor
|
||||
standing = [v for v in room.volumes if v.top_y - v.h - y0 < 0.05 and v.h > 0.3] # on the floor, no gap below
|
||||
windows = [plane_corners(p) for p in room.planes if p.label == "WINDOW_FRAME"]
|
||||
|
||||
def floor_seen(p):
|
||||
return not any(inside(v, p[0], p[2]) for v in standing)
|
||||
|
||||
def wall_seen(p): # not through a window's glass
|
||||
for c in windows:
|
||||
xs, ys, zs = [q[0] for q in c], [q[1] for q in c], [q[2] for q in c]
|
||||
if (min(ys) + 0.05 < p[1] < max(ys) - 0.05 and min(xs) - 0.05 <= p[0] <= max(xs) + 0.05
|
||||
and min(zs) - 0.05 <= p[2] <= max(zs) + 0.05):
|
||||
return False
|
||||
return True
|
||||
|
||||
xs, zs = [x for x, _ in room.outline], [z for _, z in room.outline]
|
||||
surface((min(xs), y0, max(zs)), (max(xs) - min(xs), 0, 0), (0, 0, min(zs) - max(zs)), 0.008,
|
||||
floor_seen) # floor, facing up
|
||||
pts = room.outline
|
||||
xs, zs = [x for x, _ in pts], [z for _, z in pts]
|
||||
surface((min(xs), y0, max(zs)), (max(xs) - min(xs), 0, 0), (0, 0, min(zs) - max(zs))) # floor, facing up
|
||||
for w in room.walls:
|
||||
if w.label == "INVISIBLE_WALL_FACE":
|
||||
continue
|
||||
surface((w.x0, w.y0, w.z0), (w.x1 - w.x0, 0, w.z1 - w.z0), (0, w.y1 - w.y0, 0), 0.012,
|
||||
wall_seen) # facing into the room
|
||||
surface((w.x0, w.y0, w.z0), (w.x1 - w.x0, 0, w.z1 - w.z0), (0, w.y1 - w.y0, 0)) # facing into the room
|
||||
for vol in room.volumes:
|
||||
c = box_corners(vol.cx, vol.cz, vol.yaw_deg, vol.w, vol.d)
|
||||
top, bottom = vol.top_y, vol.top_y - vol.h
|
||||
soft = 0.03 if vol.label in ("BED", "COUCH") else 0.015 # (a duvet isn't flat)
|
||||
surface((c[0][0], top, c[0][1]), (c[3][0] - c[0][0], 0, c[3][1] - c[0][1]),
|
||||
(c[1][0] - c[0][0], 0, c[1][1] - c[0][1]), soft) # top, facing up
|
||||
(c[1][0] - c[0][0], 0, c[1][1] - c[0][1])) # top, facing up
|
||||
for i in range(4):
|
||||
a, b = c[i], c[(i + 1) % 4]
|
||||
surface((a[0], bottom, a[1]), (0, top - bottom, 0), (b[0] - a[0], 0, b[1] - a[1]), 0.012) # facing out
|
||||
surface((a[0], bottom, a[1]), (0, top - bottom, 0), (b[0] - a[0], 0, b[1] - a[1])) # facing out
|
||||
return Mesh(vertices=verts, indices=idx)
|
||||
|
||||
|
||||
def fake_room():
|
||||
"""The pretend Frame's scanned room, made up from scratch (no real scan): a 3.6 x 3.2 m bedroom with a bed and a
|
||||
nightstand under a wall-mounted TV, a closet, a desk with two monitors under the window, a door, a lamp and a
|
||||
plant. Walls: 0 = the bed's (-Z, forward), 1 = right (+X), 2 = behind (+Z), 3 = the desk's and the door's (-X);
|
||||
the 3D view looks at walls 0 and 3."""
|
||||
from frameport.mr.room import Volume, box_room, plane_on_wall
|
||||
"""The pretend Frame's scanned room: a 5 x 4 m living room with a couch, a table, a TV and shelves."""
|
||||
from frameport.mr.room import Volume, box_room
|
||||
|
||||
room = box_room(3.6, 3.2, 2.5)
|
||||
room = box_room(5, 4, 2.6)
|
||||
room.source, room.universe, room.time = "scan", 7, int(SHOTS_AT)
|
||||
room.walls = [type(w)(f"{0xa0 + i:032x}", w.x0, w.z0, w.x1, w.z1, w.y0, w.y1, w.label)
|
||||
for i, w in enumerate(room.walls)] # (fixed ids: the same room in every render)
|
||||
room.uuid, room.floor_uuid, room.ceiling_uuid = (f"{0xf0 + i:032x}" for i in range(3))
|
||||
room.walls[1].label = "INVISIBLE_WALL_FACE" # an open side (a doorway the scan closed)
|
||||
vid = iter(f"{i:032x}" for i in range(1, 99))
|
||||
room.volumes = [Volume(next(vid), "BED", 0.6, -0.58, 0.55, 0, 1.45, 2.05, 0.55),
|
||||
Volume(next(vid), "SCREEN", 0.6, -1.55, 1.78, 0, 1.12, 0.07, 0.66), # TV above the bed
|
||||
Volume(next(vid), "TABLE", -1.44, -0.25, 0.75, 90, 1.4, 0.7, 0.75), # desk
|
||||
Volume(next(vid), "SCREEN", -1.6, -0.56, 1.2, 82, 0.6, 0.06, 0.36), # monitors on it
|
||||
Volume(next(vid), "SCREEN", -1.6, 0.06, 1.2, 98, 0.6, 0.06, 0.36),
|
||||
Volume(next(vid), "STORAGE", -1.25, -1.3, 2.1, 0, 1.0, 0.6, 2.1), # closet
|
||||
Volume(next(vid), "TABLE", -0.42, -1.38, 0.52, 0, 0.45, 0.4, 0.52), # nightstand
|
||||
Volume(next(vid), "LAMP", -0.42, -1.42, 0.9, 0, 0.2, 0.2, 0.38),
|
||||
Volume(next(vid), "PLANT", 1.56, -1.37, 0.95, 0, 0.36, 0.36, 0.95)] # in the corner
|
||||
room.planes = [plane_on_wall(room, 3, 0.17, 0.0, 0.85, 2.05, "DOOR_FRAME", f"{0xd0:032x}"),
|
||||
plane_on_wall(room, 3, 0.58, 1.22, 1.05, 1.05, "WINDOW_FRAME", f"{0xd1:032x}")]
|
||||
room.volumes = [Volume(next(vid), "COUCH", -1.2, 1.3, 0.85, 0, 2.2, 0.9, 0.85),
|
||||
Volume(next(vid), "TABLE", -0.9, 0.1, 0.45, 0, 1.1, 0.6, 0.45),
|
||||
Volume(next(vid), "SCREEN", -1.0, -1.85, 1.5, 0, 1.4, 0.12, 0.8),
|
||||
Volume(next(vid), "STORAGE", 1.9, -1.4, 1.8, 90, 1.2, 0.4, 1.8),
|
||||
Volume(next(vid), "TABLE", 1.6, 1.2, 0.75, 20, 1.2, 0.7, 0.75)]
|
||||
return room
|
||||
|
||||
|
||||
@@ -137,7 +99,6 @@ class FakeRoom:
|
||||
self.home = home
|
||||
self.scan: dict = {"state": "none"}
|
||||
self.polls = 0
|
||||
self.mesh_triangles = 0
|
||||
if ROOM_DIR:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -145,17 +106,13 @@ class FakeRoom:
|
||||
src = Path(ROOM_DIR)
|
||||
self.room = parse((src / "room.txt").read_text())
|
||||
self.mesh = (src / self.room.mesh.filename).read_bytes() if self.room.mesh else None
|
||||
if self.room.mesh:
|
||||
self.mesh_triangles = self.room.mesh.index_count // 3
|
||||
else:
|
||||
from frameport.mr.room import Mesh
|
||||
|
||||
self.room = fake_room()
|
||||
m = fake_room_mesh(self.room)
|
||||
self.room.mesh = Mesh(uuid=f"{0xe0:032x}", filename="room-mesh.bin", vertex_count=len(m.vertices),
|
||||
index_count=len(m.indices))
|
||||
self.room.mesh = Mesh(filename="room-mesh.bin", vertex_count=len(m.vertices), index_count=len(m.indices))
|
||||
self.mesh = write_mesh(m)
|
||||
self.mesh_triangles = len(m.indices) // 3
|
||||
self.text = dumps(self.room)
|
||||
self.refine: dict = {} # the saved clean-up settings (non-defaults)
|
||||
self.edited = False
|
||||
@@ -173,57 +130,37 @@ class FakeRoom:
|
||||
with open(os.path.join(self._folder(), self.room.mesh.filename), "wb") as f:
|
||||
f.write(self.mesh)
|
||||
|
||||
# the scan clean-up (scan.json, docs/mr/ROOM_FORMAT.md): what the label rules decided for each of the scanner's
|
||||
# boxes (in its order; to = None: removed), and the door / closet suggestions (never applied by the scanner)
|
||||
# the scan clean-up (scan.json): what the label rules decided for the scanner's boxes, door / closet suggestions
|
||||
LABELS = [
|
||||
{"from": "BED", "to": "BED", "reason": "flat top at 0.55 m, 2.05 x 1.45 m against a wall: a bed",
|
||||
"box": {"w": 1.45, "d": 2.05, "h": 0.55}},
|
||||
{"from": "TABLE", "to": "SCREEN", "reason": "7 cm thin, flat on the wall, 1.1 m above the floor: a screen",
|
||||
"box": {"w": 1.12, "d": 0.07, "h": 0.66}},
|
||||
{"from": "TABLE", "to": "TABLE", "reason": "top at 0.75 m by a wall: a desk",
|
||||
"box": {"w": 1.4, "d": 0.7, "h": 0.75}},
|
||||
{"from": "OTHER", "to": "SCREEN", "reason": "6 cm thin and upright, standing on a table: a screen",
|
||||
"box": {"w": 0.6, "d": 0.06, "h": 0.36}},
|
||||
{"from": "SCREEN", "to": "SCREEN", "reason": "upright on a table, facing the room: a screen",
|
||||
"box": {"w": 0.6, "d": 0.06, "h": 0.36}},
|
||||
{"from": "STORAGE", "to": "STORAGE", "reason": "2.1 m high and 0.6 m deep in a corner: storage",
|
||||
"box": {"w": 1.0, "d": 0.6, "h": 2.1}},
|
||||
{"from": "STORAGE", "to": "TABLE", "reason": "top at 0.52 m, 0.45 m wide by the bed: a nightstand",
|
||||
"box": {"w": 0.45, "d": 0.4, "h": 0.52}},
|
||||
{"from": "LAMP", "to": "LAMP", "reason": "0.38 m high, standing on a table: a lamp",
|
||||
"box": {"w": 0.2, "d": 0.2, "h": 0.38}},
|
||||
{"from": "PLANT", "to": "PLANT", "reason": "0.95 m high and round from above: a plant",
|
||||
"box": {"w": 0.36, "d": 0.36, "h": 0.95}},
|
||||
{"from": "OTHER", "to": None, "reason": "0.5 m behind the wall: outside the room",
|
||||
"box": {"w": 0.6, "d": 0.4, "h": 0.8}},
|
||||
{"from": "TABLE", "to": None, "reason": "only 3 cm above the floor: a rug, not furniture",
|
||||
"box": {"w": 1.6, "d": 1.1, "h": 0.03}},
|
||||
{"from": "STORAGE", "to": None, "suggest": "door", "reason": "2.0 m high, 10 cm thin, flat on the wall",
|
||||
"box": {"w": 0.8, "d": 0.1, "h": 2.0}}]
|
||||
SUGGESTIONS = [{"kind": "door", "box": {"cx": 1.75, "cz": 1.0, "top_y": 2.0, "yaw_deg": 90, "w": 0.8, "d": 0.1,
|
||||
"h": 2.0}, "wall": 1, "along": 0.81,
|
||||
"reason": "A tall, thin slab on the wall, 0.8 m wide: a door, or a closet's front"}]
|
||||
{"from": "COUCH", "to": "COUCH", "reason": "seat top 0.85 m, 2.2 m long by a wall: a couch",
|
||||
"box": {"w": 2.2, "d": 0.9, "h": 0.85}},
|
||||
{"from": "COUCH", "to": "TABLE", "reason": "top at 0.45 m and only 1.1 m long: a low table, not a couch",
|
||||
"box": {"w": 1.1, "d": 0.6, "h": 0.45}},
|
||||
{"from": "TABLE", "to": "SCREEN", "reason": "thin, raised 0.7 m, parallel to the wall, flat (1 cm): a screen",
|
||||
"box": {"w": 1.4, "d": 0.12, "h": 0.8}},
|
||||
{"from": "STORAGE", "to": "STORAGE", "reason": "1.8 m high and 0.4 m deep by a wall: storage",
|
||||
"box": {"w": 1.2, "d": 0.4, "h": 1.8}},
|
||||
{"from": "TABLE", "to": "TABLE", "reason": "top at 0.75 m: a table", "box": {"w": 1.2, "d": 0.7, "h": 0.75}},
|
||||
{"from": "OTHER", "to": None, "reason": "0.6 m behind the wall: outside the room",
|
||||
"box": {"w": 0.5, "d": 0.4, "h": 0.9}},
|
||||
{"from": "TABLE", "to": None, "reason": "holds a couch and a table 1.4 m apart: several things merged",
|
||||
"box": {"w": 3.1, "d": 1.6, "h": 0.8}},
|
||||
{"from": "STORAGE", "to": None, "suggest": "door", "reason": "2.05 m high, 12 cm thin, flat on the wall",
|
||||
"box": {"w": 0.95, "d": 0.12, "h": 2.05}}]
|
||||
SUGGESTIONS = [{"kind": "door", "box": {"cx": 2.45, "cz": 0.9, "top_y": 2.05, "yaw_deg": 90, "w": 0.95, "d": 0.12,
|
||||
"h": 2.05}, "wall": 1, "along": 0.72,
|
||||
"reason": "A tall, thin slab on the wall, 0.95 m wide: a door, or a closet's front"}]
|
||||
|
||||
def report(self, settings: dict | None = None) -> dict:
|
||||
"""scan.json's report (its keys: docs/mr/ROOM_FORMAT.md), consistent with the room's own mesh: `kept` is
|
||||
the mesh's triangle count with the recommended settings, other settings remove more or less."""
|
||||
s = {**{"walked": 0.4, "smooth": 8, "island": 0.2, "walls": "panels"}, **(settings or {})}
|
||||
|
||||
def removed(s):
|
||||
return {"walked": int(1900 * s["walked"]), "small_pieces": int(3600 * s["island"]),
|
||||
"near_walls": 140 if s["walls"] == "panels" else 40}
|
||||
|
||||
now, default = removed(s), removed({"walked": 0.4, "island": 0.2, "walls": "panels"})
|
||||
fixed = {"outside": 1260, "thin_pieces": 184, "erased": 0}
|
||||
kept = self.mesh_triangles - (sum(now.values()) - sum(default.values()))
|
||||
return {"triangles": self.mesh_triangles + sum(default.values()) + sum(fixed.values()), "kept": kept,
|
||||
"structural": int(self.mesh_triangles * 0.62) if s["walls"] != "off" else 0, **now, **fixed,
|
||||
"tops": 410, "panel_triangles": 64 if s["walls"] == "panels" else 0, "truncated": 0,
|
||||
"walked_skipped": False, "floor_y": 0.0, "ceiling_y": 2.5,
|
||||
"boxes": sum(1 for lb in self.LABELS if lb["to"]),
|
||||
"relabelled": sum(1 for lb in self.LABELS if lb["to"] and lb["to"] != lb["from"]),
|
||||
"removed_boxes": sum(1 for lb in self.LABELS if not lb["to"]),
|
||||
"suggestions": len(self.SUGGESTIONS), "plane_shifts": [{"kind": "wall", "wall": 3, "shift": 0.02}]}
|
||||
removed = int(5200 * s["walked"]) + int(9000 * s["island"]) + (2400 if s["walls"] == "panels" else 600)
|
||||
# scan.json's report keys (docs/mr/ROOM_FORMAT.md)
|
||||
return {"triangles": 48210, "kept": 48210 - removed, "structural": 21480 if s["walls"] != "off" else 0,
|
||||
"near_walls": 310, "outside": 1840, "small_pieces": int(9000 * s["island"]), "thin_pieces": 212,
|
||||
"erased": 0, "walked": int(5200 * s["walked"]), "tops": 640,
|
||||
"panel_triangles": 96 if s["walls"] == "panels" else 0, "truncated": 0, "walked_skipped": False,
|
||||
"floor_y": 0.0, "ceiling_y": 2.6, "boxes": 7, "relabelled": 2, "removed_boxes": 2, "suggestions": 1,
|
||||
"plane_shifts": [{"kind": "wall", "wall": 0, "shift": 0.02}]}
|
||||
|
||||
def status(self) -> dict:
|
||||
from frameport.mr.room import summary
|
||||
|
||||
@@ -399,17 +399,6 @@ def _long_upload(session, package: str, seconds: float = 30.0, after: float = 2.
|
||||
app.jobs.submit(Job(f"Install {app._title(package)} on Frame", run, package, "install"))
|
||||
|
||||
|
||||
def _open_room(session) -> None:
|
||||
"""The Room tab with the pretend Frame's scanned room loaded and drawn (3D view, cards)."""
|
||||
fakes.open_room(session.app)
|
||||
|
||||
|
||||
def _room_editing(session, which: str = "box") -> None:
|
||||
"""The Room tab's editing cards scrolled into view (fakes.show_room_editing: cleanup, more, box = a box selected,
|
||||
doors, suggestion = the door suggestion taken)."""
|
||||
fakes.show_room_editing(session.app, which)
|
||||
|
||||
|
||||
def _press_keys(session, text: str, gap: float = 0.28) -> None:
|
||||
"""Keys typed on the Type on Frame tab (the pretend keyboard), one by one."""
|
||||
view = session.app.keyboard_view
|
||||
@@ -425,4 +414,4 @@ HOOKS = {"fake_install": _fake_install, "wait_jobs": _wait_jobs, "held_install":
|
||||
"frame_asks": _frame_asks,
|
||||
"easter_eggs": _easter_eggs, "fps_dip": _fps_dip, "perfect_pacing": _perfect_pacing,
|
||||
"battery": _battery, "long_upload": _long_upload, "press_keys": _press_keys,
|
||||
"monitor_settled": _monitor_settled, "open_room": _open_room, "room_editing": _room_editing}
|
||||
"monitor_settled": _monitor_settled}
|
||||
@@ -10,7 +10,7 @@ const site = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const repo = resolve(site, '..');
|
||||
|
||||
const SHOTS = ['library.png', 'game.png', 'monitor.png', 'live-view.png', 'files.png', 'type-on-frame.png',
|
||||
'screenshots.png', 'install-progress.png', 'patches.png', 'room.png'];
|
||||
'screenshots.png', 'install-progress.png', 'patches.png'];
|
||||
const PUBLIC = {
|
||||
'docs/images': ['tour-teaser.webp'],
|
||||
'docs/media': ['frameport-tour.mp4', 'frameport-tour.jpg', 'frameport-install.mp4', 'frameport-install.jpg'],
|
||||
|
||||
@@ -13,7 +13,6 @@ import keys from '../assets/media/type-on-frame.png';
|
||||
import screenshots from '../assets/media/screenshots.png';
|
||||
import progress from '../assets/media/install-progress.png';
|
||||
import patches from '../assets/media/patches.png';
|
||||
import room from '../assets/media/room.png';
|
||||
import { url } from '../data/site';
|
||||
|
||||
interface Props { games: MockGame[] }
|
||||
@@ -22,7 +21,6 @@ const shots = [
|
||||
{ img: library, title: 'Library', text: 'Your games with their artwork. Search, filter, install several at once.' },
|
||||
{ img: game, title: 'Game page', text: 'One click to install or play. Patches under Customize.' },
|
||||
{ img: progress, title: 'Installing', text: 'Each stage, with speed and time left.' },
|
||||
{ img: room, title: 'Room (experimental)', text: 'Your scanned room in 3D for mixed-reality games. Clean up the scan, fix the labels.' },
|
||||
{ img: monitor, title: 'Monitor', text: "Frame rate, load, temperature and battery, live." },
|
||||
{ img: live, title: 'Live view', text: 'What the headset shows, with sound, in your browser.' },
|
||||
{ img: screenshots, title: 'Screenshots', text: 'Your headset screenshots, by game and day.' },
|
||||
|
||||
@@ -112,10 +112,9 @@ class RoomView:
|
||||
left=0, right=0, bottom=0, bgcolor=T.soft(T.BG, 0.82),
|
||||
padding=ft.Padding(T.S3, T.S2, T.S3, T.S2),
|
||||
border_radius=ft.BorderRadius(0, 0, T.RADIUS_SM, T.RADIUS_SM))
|
||||
self.view = ft.Stack([self.canvas.control, legend, self.placeholder,
|
||||
ft.Container(self.reset_btn, right=T.S2, top=T.S2)], height=T.px(CANVAS_HEIGHT))
|
||||
self.canvas_card = C.card(self.view, padding=T.S3, col={"xs": 12, "lg": 8})
|
||||
self.canvas_card.on_size_change = self._card_sized
|
||||
view = ft.Stack([self.canvas.control, legend, self.placeholder,
|
||||
ft.Container(self.reset_btn, right=T.S2, top=T.S2)], height=T.px(CANVAS_HEIGHT))
|
||||
self.canvas_card = C.card(view, padding=T.S3, col={"xs": 12, "lg": 8})
|
||||
|
||||
# status
|
||||
self.checks = ft.Container(C.checklist([C.Check(None, tr("Checking the room…"))]))
|
||||
@@ -138,10 +137,8 @@ class RoomView:
|
||||
self.summary = C.body("", T.TEXT)
|
||||
self.source = C.meta("")
|
||||
self.furniture = ft.Row([], spacing=T.S2, run_spacing=T.S2, wrap=True)
|
||||
# the right column sizes to its content (never clipped: window width, UI scale and the furniture chips change
|
||||
# its height); level() then makes the top row end level: a short column's last card grows to the 3D view's
|
||||
# card (self.level_gap), a tall column makes the 3D view taller. Every card with the same padding.
|
||||
self.level_gap = ft.Container(height=0)
|
||||
# the right column: as tall as the 3D view's card (its last card takes what's left),
|
||||
# every card with the same padding
|
||||
side = ft.Column([
|
||||
C.card(ft.Column([self.checks, self.error], spacing=T.S2), padding=T.S4),
|
||||
C.card(ft.Column([C.section(tr("Scan"), self.scan_box,
|
||||
@@ -149,11 +146,10 @@ class RoomView:
|
||||
ft.Row([self.boundary_btn, self.delete_btn], spacing=T.S2, wrap=True),
|
||||
help="mr_room")], spacing=T.S3), padding=T.S4),
|
||||
C.card(ft.Column([C.meta(tr("In this room")), self.summary, self.source, self.furniture,
|
||||
self.level_gap, self._tester()], spacing=T.S2),
|
||||
padding=T.S4),
|
||||
ft.Container(expand=True), self._tester()], spacing=T.S2),
|
||||
padding=T.S4, expand=True),
|
||||
], spacing=T.S4, col={"xs": 12, "lg": 4}, horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
|
||||
on_size_change=self._side_sized)
|
||||
self._sizes = {"card": None, "side": None}
|
||||
height=T.px(CANVAS_HEIGHT) + 2 * T.S3 + 2)
|
||||
# editing: the scan clean-up, what the label rules decided, furniture boxes + doors / windows
|
||||
from .room_edit import CleanupCard, FurnitureEditor, LabelsCard
|
||||
|
||||
@@ -313,40 +309,6 @@ class RoomView:
|
||||
C.confirm(self.app.page, tr("Delete room?"), tr("Games will use a simple room until you save another room."),
|
||||
tr("Delete room"), lambda: RS.operate(self.app, "delete"), danger=True)
|
||||
|
||||
# ------------------------------------------------------------------ the top row ends level
|
||||
def _card_sized(self, e) -> None:
|
||||
self._sizes["card"] = (e.width, e.height)
|
||||
self.level()
|
||||
|
||||
def _side_sized(self, e) -> None:
|
||||
self._sizes["side"] = (e.width, e.height)
|
||||
self.level()
|
||||
|
||||
def level(self) -> None:
|
||||
"""The right column's height (its content's, measured) against the 3D view's card: a shorter column gets a
|
||||
gap in its last card, a taller one makes the 3D view taller. Stacked (narrow window): neither."""
|
||||
card, side = self._sizes["card"], self._sizes["side"]
|
||||
if not card or not side:
|
||||
return
|
||||
base = T.px(CANVAS_HEIGHT)
|
||||
base_card = base + 2 * T.S3 + 2 # (the card's padding and border)
|
||||
content = side[1] - (self.level_gap.height or 0)
|
||||
gap, view = 0, base
|
||||
if side[0] < card[0] * 0.9: # side by side
|
||||
if content <= base_card:
|
||||
gap = int(base_card - content)
|
||||
else:
|
||||
view = base + int(content - base_card + 0.999)
|
||||
changed = []
|
||||
if gap != (self.level_gap.height or 0):
|
||||
self.level_gap.height = gap
|
||||
changed.append(self.level_gap)
|
||||
if view != self.view.height:
|
||||
self.view.height = self.canvas.control.height = view
|
||||
changed.append(self.view)
|
||||
if changed:
|
||||
C.update(*changed)
|
||||
|
||||
async def show_editing(self) -> None:
|
||||
"""Scroll down to the editing cards (Clean-up, Furniture & screens, Labels)."""
|
||||
if self.root is not None:
|
||||
|
||||
@@ -324,7 +324,8 @@ class LabelsCard:
|
||||
@staticmethod
|
||||
def _size(box: dict) -> str:
|
||||
try:
|
||||
return size_text(float(box["w"]), float(box["d"]), float(box["h"]))
|
||||
return tr("{w}×{d}×{h} m").format(w=f"{float(box['w']):.1f}", d=f"{float(box['d']):.1f}",
|
||||
h=f"{float(box['h']):.1f}")
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return ""
|
||||
|
||||
@@ -399,15 +400,6 @@ def _panel(rows: list[ft.Control]) -> ft.Container:
|
||||
bgcolor=T.SURFACE_2, border_radius=T.RADIUS_SM, padding=T.S3, visible=False)
|
||||
|
||||
|
||||
def metres(value: float) -> str:
|
||||
"""A box size in metres: one decimal, two below 10 cm (a rug's 3 cm isn't "0.0")."""
|
||||
return f"{value:.2f}" if abs(value) < 0.095 else f"{value:.1f}"
|
||||
|
||||
|
||||
def size_text(w: float, d: float, h: float) -> str:
|
||||
return tr("{w}×{d}×{h} m").format(w=metres(w), d=metres(d), h=metres(h))
|
||||
|
||||
|
||||
def field_label(name: str) -> str:
|
||||
return {"Left / right": tr("Left / right"), "Front / back": tr("Front / back"), "Top height": tr("Top height"),
|
||||
"Turn": tr("Turn"), "Width": tr("Width"), "Depth": tr("Depth"), "Height": tr("Height"),
|
||||
@@ -537,7 +529,7 @@ class FurnitureEditor:
|
||||
# ------------------------------------------------------------------ lists
|
||||
@staticmethod
|
||||
def _box_size(v) -> str:
|
||||
return size_text(v.w, v.d, v.h)
|
||||
return tr("{w}×{d}×{h} m").format(w=f"{v.w:.1f}", d=f"{v.d:.1f}", h=f"{v.h:.1f}")
|
||||
|
||||
def _plane_info(self, i: int) -> str:
|
||||
p = self.room.planes[i]
|
||||
|
||||
@@ -372,10 +372,3 @@ class _local_dir:
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
|
||||
def test_box_sizes_keep_thin_sizes_readable():
|
||||
from frameport.ui.views.room_edit import metres, size_text
|
||||
|
||||
assert metres(0.03) == "0.03" and metres(0.07) == "0.07" and metres(0.5) == "0.5" and metres(2.05) == "2.0"
|
||||
assert size_text(1.6, 1.1, 0.03) == "1.6×1.1×0.03 m"
|
||||
@@ -54,9 +54,9 @@ def run(scanner, dump, directory, *args, success=True):
|
||||
return report
|
||||
|
||||
|
||||
def room(directory, name="room.txt"):
|
||||
def room(directory):
|
||||
records = {}
|
||||
lines = (directory / name).read_text().splitlines()
|
||||
lines = (directory / "room.txt").read_text().splitlines()
|
||||
assert lines[0] == "frameport-room 1"
|
||||
for line in lines:
|
||||
if not line or line.startswith("#"):
|
||||
@@ -95,8 +95,8 @@ def assert_walls(polygon, expected):
|
||||
(a[1] + b[1]) / 2 + .04 * dx / length, polygon)
|
||||
|
||||
|
||||
def mesh(directory, name="room-mesh.bin"):
|
||||
data = (directory / name).read_bytes()
|
||||
def mesh(directory):
|
||||
data = (directory / "room-mesh.bin").read_bytes()
|
||||
magic, version, nv, ni = struct.unpack_from("<4sIII", data)
|
||||
assert (magic, version) == (b"FPRM", 1)
|
||||
assert 0 < ni // 3 <= 200_000 and ni % 3 == 0
|
||||
@@ -115,8 +115,7 @@ def test_box_room_geometry_furniture_and_wire_format(scanner, dumps, tmp_path):
|
||||
assert report["coverage_percent"] == 100
|
||||
assert abs(report["floor_y"]) < .05
|
||||
assert abs(report["ceiling_y"] - 2.6) < .05
|
||||
# The scanner's own output is kept as the raw files; room.txt + room-mesh.bin are refined (tests further down).
|
||||
records = room(tmp_path, "room-raw.txt")
|
||||
records = room(tmp_path)
|
||||
assert records["universe"] == [["18446744073709551615"]]
|
||||
assert records["source"] == [["scan"]]
|
||||
assert_walls(outline(records), synth.BOX_OUTLINE)
|
||||
@@ -132,12 +131,8 @@ def test_box_room_geometry_furniture_and_wire_format(scanner, dumps, tmp_path):
|
||||
ids = [row[0] for kind in ("room", "floor", "ceiling", "wall", "volume", "mesh") for row in records[kind]]
|
||||
assert len(ids) == len(set(ids))
|
||||
assert all(re.fullmatch("[0-9a-f]{32}", value) for value in ids)
|
||||
vertices, indices = mesh(tmp_path, "room-mesh-raw.bin")
|
||||
assert records["mesh"][0][1:] == ["room-mesh-raw.bin", str(len(vertices)), str(len(indices))]
|
||||
refined = room(tmp_path)
|
||||
assert [row[0] for row in refined["room"] + refined["floor"] + refined["wall"]] == \
|
||||
[row[0] for row in records["room"] + records["floor"] + records["wall"]] # same anchors as the raw room
|
||||
assert refined["mesh"][0][1:3] == ["room-mesh.bin", str(len(mesh(tmp_path)[0]))]
|
||||
vertices, indices = mesh(tmp_path)
|
||||
assert records["mesh"][0][1:] == ["room-mesh.bin", str(len(vertices)), str(len(indices))]
|
||||
# Normals on the structural surfaces must face the room interior.
|
||||
correct = checked = 0
|
||||
for i in range(0, len(indices), 3):
|
||||
@@ -355,14 +350,11 @@ def test_roomview_style_scan_retains_room_and_furniture(scanner, roomview_dump,
|
||||
assert report["far_vertices"] > 1000
|
||||
assert report["detected_walls"] == 4
|
||||
assert abs(report["floor_y"]) < .05 and abs(report["ceiling_y"] - 2.6) < .05
|
||||
records = room(tmp_path, "room-raw.txt")
|
||||
records = room(tmp_path)
|
||||
assert_walls(outline(records), synth.BOX_OUTLINE)
|
||||
assert any(row[1] == "TABLE" and abs(float(row[4]) - .75) < .05 for row in records["volume"])
|
||||
assert any(row[1] == "COUCH" for row in records["volume"])
|
||||
mesh(tmp_path, "room-mesh-raw.bin")
|
||||
mesh(tmp_path)
|
||||
assert report["heads"] == 28 # the tracked frames' head path, for the walked-volume pass
|
||||
assert len(json.loads((tmp_path / "scan.json").read_text())["heads"]) == 28
|
||||
|
||||
|
||||
def test_depth_discontinuity_is_rejected_below_absolute_edge_limit(scanner, dumps, tmp_path):
|
||||
@@ -396,228 +388,3 @@ def test_small_ceiling_patch_does_not_override_default(scanner, dumps, tmp_path)
|
||||
report = run(scanner, path, tmp_path / "out", "--ceiling-default", "2.7")
|
||||
assert "ceiling not seen" in report["warnings"]
|
||||
assert abs(report["ceiling_y"] - report["floor_y"] - 2.7) < .001
|
||||
|
||||
|
||||
# --- Refinement + label rules (docs/mr/scan.md "Refinement") ---------------------------------------------------------
|
||||
|
||||
def refine_only(scanner, directory, *args, success=True):
|
||||
proc = subprocess.run([str(scanner), "--refine-only", str(directory), "--json", *args],
|
||||
text=True, capture_output=True, timeout=15)
|
||||
assert proc.returncode == (0 if success else 2), proc.stdout + proc.stderr
|
||||
report = json.loads(proc.stdout)
|
||||
assert report["ok"] is success
|
||||
return report
|
||||
|
||||
|
||||
def centroids(directory, name="room-mesh.bin"):
|
||||
vertices, indices = mesh(directory, name)
|
||||
return [[sum(vertices[indices[i + k]][a] for k in range(3)) / 3 for a in range(3)]
|
||||
for i in range(0, len(indices), 3)]
|
||||
|
||||
|
||||
def in_box(c, box, pad=.06):
|
||||
(cx, bottom, cz), (w, h, d) = box
|
||||
return abs(c[0] - cx) <= w / 2 + pad and bottom - pad <= c[1] <= bottom + h + pad and abs(c[2] - cz) <= d / 2 + pad
|
||||
|
||||
|
||||
def masked(text, mesh_name=None):
|
||||
text = re.sub(r"(?m)^time \d+", "time T", re.sub(r"[0-9a-f]{32}", "U", text))
|
||||
return text.replace(f" {mesh_name} ", " MESH ") if mesh_name else text
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def clutter_dump(tmp_path_factory):
|
||||
path = tmp_path_factory.mktemp("clutter") / "clutter.fprs"
|
||||
synth.render(path, profile="legacy", extra=[synth.TUBE, synth.GHOST])
|
||||
return path
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def box_scan(scanner, dumps, tmp_path_factory):
|
||||
out = tmp_path_factory.mktemp("box-scan") / "room"
|
||||
return out, run(scanner, dumps["box"], out)
|
||||
|
||||
|
||||
def test_floating_tube_and_walked_through_ghost_are_removed(scanner, clutter_dump, tmp_path):
|
||||
report = run(scanner, clutter_dump, tmp_path / "default")["refine"]
|
||||
raw, refined = centroids(tmp_path / "default", "room-mesh-raw.bin"), centroids(tmp_path / "default")
|
||||
for box in (synth.TUBE, synth.GHOST):
|
||||
assert sum(in_box(c, box) for c in raw) > 100 # the scanner meshed both
|
||||
assert not any(in_box(c, box) for c in refined)
|
||||
assert report["thin_pieces"] > 100 and report["walked"] > 100
|
||||
# Each piece goes for its own reason: without the walked volume the ghost stays, without tubes the pole stays.
|
||||
run(scanner, clutter_dump, tmp_path / "unwalked", "--walked", "0")
|
||||
assert sum(in_box(c, synth.GHOST) for c in centroids(tmp_path / "unwalked")) > 100
|
||||
run(scanner, clutter_dump, tmp_path / "untubed", "--tubes", "0")
|
||||
assert sum(in_box(c, synth.TUBE) for c in centroids(tmp_path / "untubed")) > 100
|
||||
# Real furniture survives: the table top is flattened onto its box, not removed.
|
||||
assert any(in_box(c, synth.TABLE) and abs(c[1] - .75) < .06 for c in refined)
|
||||
|
||||
|
||||
def test_panels_wind_into_the_room_on_the_reestimated_planes(box_scan):
|
||||
out, report = box_scan
|
||||
refine = report["refine"]
|
||||
assert refine["panel_triangles"] == 12 # four seen walls + floor and ceiling (two ears each)
|
||||
vertices, indices = mesh(out)
|
||||
records = room(out)
|
||||
ceiling = float(records["ceiling"][0][1])
|
||||
assert abs(ceiling - refine["ceiling_y"]) < 1e-4 and abs(ceiling - 2.6) < .05
|
||||
assert all(float(w[6]) == ceiling for w in records["wall"]) # wall tops follow the re-estimated ceiling
|
||||
panels = len(indices) // 3 - refine["panel_triangles"]
|
||||
for t in range(panels, len(indices) // 3):
|
||||
a, b, c = (vertices[indices[3 * t + k]] for k in range(3))
|
||||
u, v = [b[k] - a[k] for k in range(3)], [c[k] - a[k] for k in range(3)]
|
||||
normal = (u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0])
|
||||
centroid = [(a[k] + b[k] + c[k]) / 3 for k in range(3)]
|
||||
assert sum(n * (o - p) for n, o, p in zip(normal, (0, 1.3, 0), centroid, strict=True)) > 0
|
||||
ys = {round(p[1], 4) for p in (a, b, c)}
|
||||
assert ys <= {round(refine["floor_y"], 4), round(ceiling, 4)}
|
||||
|
||||
|
||||
def test_refine_only_reproduces_the_scan_and_refine_zero_is_the_old_output(scanner, dumps, box_scan, tmp_path):
|
||||
out, report = box_scan
|
||||
copy = tmp_path / "copy"
|
||||
shutil.copytree(out, copy)
|
||||
for name in ("room.txt", "room-mesh.bin"):
|
||||
(copy / name).unlink()
|
||||
again = refine_only(scanner, copy)
|
||||
assert again["refine"] == report["refine"]
|
||||
for name in ("room.txt", "room-mesh.bin", "scan.json", "room-raw.txt", "room-mesh-raw.bin"):
|
||||
assert (copy / name).read_bytes() == (out / name).read_bytes(), name
|
||||
# --refine 0 writes what the scanner wrote before refinement existed: its raw output, byte for byte.
|
||||
plain = tmp_path / "plain"
|
||||
run(scanner, dumps["box"], plain, "--refine", "0")
|
||||
assert (plain / "room-mesh.bin").read_bytes() == (out / "room-mesh-raw.bin").read_bytes()
|
||||
assert masked((plain / "room.txt").read_text(), "room-mesh.bin") == \
|
||||
masked((out / "room-raw.txt").read_text(), "room-mesh-raw.bin")
|
||||
# ... and switching refinement back on later gives the default output again (same anchors as its own raw room).
|
||||
refine_only(scanner, plain)
|
||||
assert (plain / "room-mesh.bin").read_bytes() == (out / "room-mesh.bin").read_bytes()
|
||||
assert masked((plain / "room.txt").read_text()) == masked((out / "room.txt").read_text())
|
||||
|
||||
|
||||
def test_scan_json_holds_settings_report_and_heads(box_scan):
|
||||
out, report = box_scan
|
||||
scan = json.loads((out / "scan.json").read_text())
|
||||
assert scan["version"] == 1 and len(scan["heads"]) == report["heads"] > 0
|
||||
assert all(len(h) == 3 for h in scan["heads"])
|
||||
assert scan["refine"] == {"refine": 1, "walls": "panels", "band": .35, "facing_deg": 60, "band_in": .12,
|
||||
"floor_in": .08, "ceil_in": .25, "top_strip": .5, "near": .03, "clip": .05,
|
||||
"clip_open": .1, "island": .2, "smooth": 8, "tops": .1, "protect": 0, "tubes": 3,
|
||||
"tube_thick": .15, "walked": .4, "label_rules": 1, "erase": []}
|
||||
summary = dict(report["refine"])
|
||||
assert summary.pop("output_triangles") == len(mesh(out)[1]) // 3 and summary.pop("output_volumes") == 2
|
||||
assert summary == scan["report"]
|
||||
assert scan["report"]["kept"] + scan["report"]["panel_triangles"] == len(mesh(out)[1]) // 3
|
||||
assert sorted((label["from"], label["to"]) for label in scan["labels"]) == [("COUCH", "TABLE"), ("TABLE", "TABLE")]
|
||||
assert scan["suggestions"] == []
|
||||
|
||||
|
||||
def test_head_path_is_decimated_to_two_thousand_points(scanner, dumps, tmp_path):
|
||||
data = dumps["one"].read_bytes()
|
||||
empty = b"".join(struct.pack("<d7fII", i, i * .001, 1.6, 0, 0, 0, 0, 1, 0, 0) for i in range(2500))
|
||||
dump = tmp_path / "long.fprs"
|
||||
dump.write_bytes(data + empty)
|
||||
report = run(scanner, dump, tmp_path / "out", "--min-views", "1") # one frame with depth, 2500 without
|
||||
heads = json.loads((tmp_path / "out" / "scan.json").read_text())["heads"]
|
||||
assert 1000 <= report["heads"] == len(heads) <= 2000
|
||||
assert heads[-1][0] > 2 # still spans the whole recording
|
||||
|
||||
|
||||
def test_erase_spheres_remove_the_scan_inside_them(scanner, box_scan, tmp_path):
|
||||
copy = tmp_path / "copy"
|
||||
shutil.copytree(box_scan[0], copy)
|
||||
report = refine_only(scanner, copy, "--erase", "-0.6,0.75,-0.25,0.3;1.5,1.0,-1.0,0.2")["refine"]
|
||||
assert report["erased"] > 0
|
||||
assert not any((c[0] + .6) ** 2 + (c[1] - .75) ** 2 + (c[2] + .25) ** 2 <= .09 for c in centroids(copy))
|
||||
assert json.loads((copy / "scan.json").read_text())["refine"]["erase"] == [[-.6, .75, -.25, .3], [1.5, 1, -1, .2]]
|
||||
|
||||
|
||||
ROOM_IDS = iter(f"{i:032x}" for i in range(1, 1000))
|
||||
|
||||
|
||||
def volume(label, cx, cz, top, yaw, w, d, h):
|
||||
return f"volume {next(ROOM_IDS)} {label} {cx} {cz} {top} {yaw} {w} {d} {h}\n"
|
||||
|
||||
|
||||
def test_label_rules_on_an_older_room_folder(scanner, tmp_path):
|
||||
"""Merged and nested boxes and boxes behind a wall go, shapes are relabelled, a door slab becomes a suggestion.
|
||||
The folder has no raw files (an older scan): its room.txt + mesh are taken as the unrefined scan."""
|
||||
walls = [(-2, -1.5, 2, -1.5), (2, -1.5, 2, 1.5), (2, 1.5, -2, 1.5), (-2, 1.5, -2, -1.5)]
|
||||
plane = f"plane {next(ROOM_IDS)} DOOR_FRAME 0.0000 1.0000 1.4900 0.0000 0.0000 0.0000 1.0000 0.9000 2.0000\n"
|
||||
text = (f"frameport-room 1\nuniverse 7\nsource scan\ntime 1\nroom {next(ROOM_IDS)}\nfloor {next(ROOM_IDS)} 0\n"
|
||||
f"ceiling {next(ROOM_IDS)} 2.6\noutline 4 -2 -1.5 2 -1.5 2 1.5 -2 1.5\n"
|
||||
+ "".join(f"wall {next(ROOM_IDS)} {a} {b} {c} {d} 0 2.6\n" for a, b, c, d in walls)
|
||||
+ volume("TABLE", -1.2, -.5, .75, 0, 1.0, .6, .75)
|
||||
+ volume("COUCH", .6, -.5, .5, 0, 1.6, .8, .5)
|
||||
+ volume("OTHER", -.3, -.5, .8, 0, 3.0, 1.0, .8) # holds the table and the couch: merged
|
||||
+ volume("BED", .6, .85, .55, 0, 2.0, 1.1, .55)
|
||||
+ volume("OTHER", .6, .85, .45, 0, .5, .4, .45) # inside the bed: part of it
|
||||
+ volume("OTHER", 1.85, -.8, 1.6, -90, 1.0, .06, .6) # a flat panel along the x = 2 wall: a screen
|
||||
+ volume("SCREEN", -1.0, -1.45, 2.0, 0, .9, .1, 2.0) # a tall slab by the z = -1.5 wall: a door?
|
||||
+ volume("STORAGE", 2.6, 0, 1.5, 0, .5, .5, 1.5) # behind a wall
|
||||
+ plane)
|
||||
vertices, indices = [], []
|
||||
|
||||
def quad(a, b, c, d):
|
||||
base = len(vertices)
|
||||
vertices.extend((a, b, c, d))
|
||||
indices.extend((base, base + 1, base + 2, base, base + 2, base + 3))
|
||||
for i in range(18): # the screen: a flat 0.9 x 0.5 m grid at x = 1.85
|
||||
for j in range(10):
|
||||
z, y = -1.25 + i * .05, 1.05 + j * .05
|
||||
quad((1.85, y, z), (1.85, y, z + .05), (1.85, y + .05, z + .05), (1.85, y + .05, z))
|
||||
for i in range(38): # some floor
|
||||
for j in range(28):
|
||||
x, z = -1.9 + i * .1, -1.4 + j * .1
|
||||
quad((x, 0, z), (x, 0, z + .1), (x + .1, 0, z + .1), (x + .1, 0, z))
|
||||
text += f"mesh {next(ROOM_IDS)} room-mesh.bin {len(vertices)} {len(indices)}\n"
|
||||
mesh_bytes = (struct.pack("<4sIII", b"FPRM", 1, len(vertices), len(indices))
|
||||
+ b"".join(struct.pack("<3f", *v) for v in vertices) + struct.pack(f"<{len(indices)}I", *indices))
|
||||
folder = tmp_path / "room"
|
||||
folder.mkdir()
|
||||
(folder / "room.txt").write_text(text)
|
||||
(folder / "room-mesh.bin").write_bytes(mesh_bytes)
|
||||
report = refine_only(scanner, folder)
|
||||
assert "no raw files; the current room was taken as the unrefined scan" in report["warnings"]
|
||||
assert "no head path; walked volume skipped" in report["warnings"]
|
||||
assert (folder / "room-mesh-raw.bin").read_bytes() == mesh_bytes
|
||||
scan = json.loads((folder / "scan.json").read_text())
|
||||
decisions = [(label["from"], label["to"], label["reason"]) for label in scan["labels"]]
|
||||
assert decisions == [
|
||||
("TABLE", "TABLE", "top 0.75 m"),
|
||||
("COUCH", "COUCH", "seat-height top 0.50 m, 1.6 m long"),
|
||||
("OTHER", None, "holds a table and a couch 1.8 m apart: several things merged"),
|
||||
("BED", "BED", "2.2 m², 2.0 m long, top 0.55 m"),
|
||||
("OTHER", None, "100% inside the bigger bed box: part of it"),
|
||||
("OTHER", "SCREEN", "thin, raised, screen-shaped, flat (0 cm)"),
|
||||
("SCREEN", None, "tall thin slab from the floor (2.00 m): a door or closet door?"),
|
||||
("STORAGE", None, "centre 60 cm outside the room (behind a wall)"),
|
||||
]
|
||||
assert scan["suggestions"] == [{"kind": "door", "box": {"cx": -1, "cz": -1.45, "top_y": 2, "yaw_deg": 0,
|
||||
"w": .9, "d": .1, "h": 2}, "wall": 0, "along": .25,
|
||||
"reason": decisions[6][2]}]
|
||||
raw = room(folder, "room-raw.txt")
|
||||
assert raw["mesh"][0][1] == "room-mesh-raw.bin" and len(raw["volume"]) == 8
|
||||
refined = room(folder)
|
||||
assert [row[1] for row in refined["volume"]] == ["TABLE", "COUCH", "BED", "SCREEN"]
|
||||
assert refined["universe"] == [["7"]] and refined["plane"] == [plane.split()[1:]] # unknown records stay
|
||||
assert report["refine"]["removed_boxes"] == 4 and report["refine"]["relabelled"] == 1
|
||||
assert report["refine"]["suggestions"] == 1
|
||||
first = {name: (folder / name).read_bytes() for name in ("room.txt", "room-mesh.bin", "scan.json")}
|
||||
refine_only(scanner, folder) # now from the raw files: the same result
|
||||
assert first == {name: (folder / name).read_bytes() for name in first}
|
||||
refine_only(scanner, folder, "--label-rules", "0")
|
||||
assert [row[1] for row in room(folder)["volume"]] == [
|
||||
"TABLE", "COUCH", "OTHER", "BED", "OTHER", "OTHER", "SCREEN", "STORAGE"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("args", [("--walls", "shell"), ("--smooth", "1.5"), ("--erase", "1,2,3"), ("--refine", "2"),
|
||||
("--band", "2"), ("--walked", ".01"), ("--erase", "0,0,0,-1")])
|
||||
def test_invalid_refinement_options_are_json_errors(scanner, dumps, tmp_path, args):
|
||||
run(scanner, dumps["one"], tmp_path / "out", *args, success=False)
|
||||
|
||||
|
||||
def test_refine_only_needs_a_room(scanner, tmp_path):
|
||||
report = refine_only(scanner, tmp_path, success=False)
|
||||
assert "room-raw.txt" in report["error"]
|
||||