Releases: lpalbou/MeshVault
Release list
MeshVault v0.8.0
Added
-
Symmetry healing (backlog 050) — repair a texture from the object's own mirror counterpart, the repair class
clone_paint's 45° same-orientation cone correctly refuses (a clean right eye healing a corrupted left):detect_symmetry {samples?, seed?}: finds the dominant mirror plane — 3 wrapper-local axis planes + PCA axes through the surface's area centroid (bbox centers are biased by one-sided features), scored by median + p90 reflected-point-to-surface distance (BVH over merged wrapper-local triangles, cached and keyed bygeometryRev) PLUS normal agreement (catches planes that map surface onto surface positionally but with wrong orientation). Deterministic (seeded); per-candidate numbers and a strong/moderate/weak verdict; axis planes win ties over PCA so the choice is predictable on near-perfect shapes (spheres).mirror_paint {center, radius|radius_rel, strength?, hardness?, falloff?, plane?, plane_origin?, texture_size?}: for each destination texel, samples the surface at its REFLECTED position throughW·R·W⁻¹(wrapper-local Householder — exact under any placement incl. non-uniform scale) with per-texel reflected-normal source filtering (kills two-sheet mis-sourcing at ears/eye sockets) and bilinear snapshot sampling. Content arrives intrinsically mirrored — there is deliberately NO 2D flip step (the reflection map's det −1 does it; an explicit flip would paste the donor un-mirrored — the adversarial review caught this chirality bug in the original design sketch). Auto-detects + caches the plane; refuses weak symmetry with the measured numbers and the override syntax; loud partial-coverage accounting ({healed, meanAlpha, skipped{noSource, normalReject}, selfSourceFraction}) and a cross-mesh teaching error naming the mesh that actually holds the mirror counterpart.
-
Human editing UI (backlog 054) — sculpt/paint/keyframe authoring for people; the app is now an editor, not just a review surface. Every MUTATION routes through the same control-API commands agents use (one
ViewerControlAPIinstance in the app), so the UI adds no behavioral fork and the E2E suites remain the single truth:- Edit panel (one toolbar button, Sculpt | Paint tabs = mode switch): brush picker (draw/inflate/smooth/flatten/pinch), radius (scale-free
radius_rel), per-tool strength memory, color/opacity/hardness/texture-tier, clear-paint, per-gesture sculpt undo (one slot, Cmd/Ctrl+Z — accessor-decoded position snapshot at gesture start), paint-budget line, world-radius ring cursor, inline pre-flight banners for engine-refused states (skinned, timeline playing, no-UV, GPU-only textures) instead of translated throws; paint entry auto-switches an overriding render mode back to textured. - Input arbitration (per the adversarial design review):
viewer._toolModeis the shared flag every input consumer checks; OrbitControls stays enabled and is disabled only AFTER a synchronous hit-test against the ACTIVE object's meshes — a hit captures the pointer on the canvas and streams radius/2-thinned points flushed at ≥16 points or 100 ms intosculpt_stroke/paint_stroke(never per-frame: normals recompute per command), a miss falls through to orbit naturally. Hover feedback uses a local raycaster — neverexecute()(which invalidates and would defeat demand rendering). Mutual exclusions: measure⟷edit, FPV forces edit off, gizmo hover/drag wins, scene-panel click-select disabled in tool mode, scene replacement mid-stroke ends the gesture, ESC exits. - Keyframe authoring on the timeline bar: the bar now shows whenever a model is loaded (the Key button needs a home BEFORE the first key exists); ● keys the active object's pose at the playhead (
set_keyframe {capture:true}); tick marks carry EXACT key times (display rounding never touches the deleted value — 1e-6 engine tolerance), click = seek, right-click = per-object delete menu; duration field clamps to the last key; the bar stacks above the clip-animation bar when both are visible instead of overlapping.
- Edit panel (one toolbar button, Sculpt | Paint tabs = mode switch): brush picker (draw/inflate/smooth/flatten/pinch), radius (scale-free
-
The abstract3d → MeshVault pipeline as one recipe (backlog 053) — text prompt → generate → intake → repair → adaptive-optimize → articulate → animate → export, executed end-to-end for the first time:
examples/t23d_pipeline.md: the agent recipe over the 13 MCP tools — every stage always inspects and conditionally mutates on ratio-based gates (density vs scene median, dihedral flatness, part-size fractions — never asset-derived constants), with the field-verified trap list inline (replace-vs-add loads, compare-before-split ordering, post-split active object, 360°-keyframe collapse, hollow cut faces, volatile split parts, QA budgets, GPU negotiation).examples/t23d_pipeline.py: the same flow library-shaped over the newHeadlessSessionfacade — the reference for embedding MeshVault's pipeline in another application. abstract3d runs as a CLI subprocess in its own env (bundle dir = the contract);--bundlereuses an existing generation (the resource-negotiation path); per-stage verdict records{numbers_before, decision, action, numbers_after}+ proof renders + INDEX.md.- The committed evidence run (
examples/t23d_pipeline_report.md+ contact sheet): a TripoSR treasure chest (131k triangles, watertight) through all stages in 28.9 s — repair and optimize honestly NO-OPPED with evidence (the gates correctly refused to invent work), articulation plane-cut the fused mesh (lid + base, back-edge hinge), animation keyed a −25° lid sweep over a 0/90/180/270/360 turntable, and the exported GLB reloaded with its clip playing and triangles conserved. One judgment point in the whole chain: the hinge line (parameterized, agent-decidable).
-
HeadlessSession(backend/headless_viewer.py): LocalModelServer + HeadlessViewer + harness composed into one embeddable unit — validated local loads with companion files,execute(), render presets,capture_png, andexport_glb_to_file(in-process data-URL fetch, no MCP truncation).mcp_server._Runtimenow composes it; the MCP server and library embedders share one load path. -
Browser E2E suites in the repo (backlog 048) —
tests/e2e/: 127 checks across 6 suites (sculpt/paint 33, articulation/timeline 35, symmetry healing 26, edit UI 20, brush-gesture semantics 12, GLB V-flip round-trip — now self-contained, no machine-local fixtures). Opt-inpytest -m e2emarker (plainpytest tests/self-skips), anmv_appfixture that targets a running app (MESHVAULT_E2E_URL) or spins up its own server,scripts/e2e.sh, and ane2erelease-workflow job gating publishes alongside the Python matrix. These are the suites that caught the sRGB blend bug, the stroke double-blend, the exporter V-flip, and the hierarchy-blind explode — previously one reboot away from deletion in/tmp. -
undo_paint— single-slot undo for texture brushes (paint/paint_stroke/blur_paint/clone_paint/mirror_paint): the pre-write canvas rects are stashed per brush call and restored exactly once. Requested by the 050 field gauntlet ("every stamp is a one-way door; clear_paint nukes ALL layers"); also wired into the human paint panel (Undo stroke, Cmd/Ctrl+Z).
Fixed
explode_viewwas meaningless on parented parts (fresh-agent gauntlet): a parent's wrapper box is the subtree UNION (child wrappers live inside it), so parent and child shared one centroid — same direction, zero relative separation, factor-independent negativeminGapWorld. Now model-scoped boxes, hierarchy-ordered application, and world→parent-frame delta conversion: a parented lid/base pair reports +1.66 at factor 1.6 and scales with it.mirror_paintcycle-2 gauntlet fixes (field-tested on the portrait's corrupted iris — verdict: genuinely repaired, anatomically mirrored, donor eye pixel-verified untouched):- a work budget + per-texel centroid prefilter replace the silent 6-minute scan a half-head brush triggered (now a teaching error advising passes/lower tier);
selfSourceFractionsemantics corrected — the old 10%-of-radius test capped it at ~0.06, so the documented on-plane warning could mathematically never fire; it now counts mirror sources landing inside the brush and warns above 0.4;- the zero-heal diagnostic no longer names the object's OWN mesh as a "cross-mesh counterpart" (a dead-end detour to clone_paint); sub-texel brushes get "radius smaller than one texel" instead;
- explicit plane overrides are scored and warned when weak or disagreeing with the detected winner (silent wrong-plane heals imported neck texture onto a cheek);
detect_symmetryrefines the plane origin along its normal (1D median-distance scan) — sample-seed changes moved the origin ~2.7 cm on a head, a smear at iris scale.
- The pipeline recipe + script absorbed the fresh-agent run's 7 doc defects and 6 doc-vs-script disagreements: explode-before-parent ordering (+ trap 11), reload verification via
pause_animation/set_animation_time(+ trap 12), the uniform-density → GLOBAL simplify branch in the optimize table, pick usage at the hinge judgment point, fixed-camera motion frames, the 45% chest-seam class prior (measured 43.3% vs the naive 62% default), and honest bundle-metadata keys. - Human UI cycle-2 gauntlet fixes (adversarial QA with trusted pointer events; 9/10 arbitration cells held, the failures clustered where mode owners didn't talk to each other):
setNavMode()now dispatchesnavmodechangeitself, both directions — only →orbit call sites notified before, so tool modes never learned about →fpv and a human could literally paint while flying; the stale nav icon after forced-orbit is fixed by the same change;- the edit panel docks top-LEFT of the viewer (sharing the lights-panel anchor buried whichever opened second — brush-vs-light iteration was modal);
- gesture-end feedback runs for EVERY gesture (the advisory + budget-line refresh previously required unflushed points at pointer-up, which th...
MeshVault v0.7.0
Added
- Texture forensics & proof tooling (backlog 047) — built through a 5-cycle adversarial program (3 agent personas) on repair quality, proofs and animation persistence:
- UV diagnosis:
pickreturns the surface point's.uv;get_textureMCP tool (13 tools) renders the texture in TEXTURE SPACE with the mesh's UV wireframe, crosshair markers at picked UVs (open-center, size/label controls), an orange outline of the chart under a point, and a zoom crop — texture-to-mesh misalignment becomes visible and measurable.get_uv_islandsreports chart statistics (a fragmented atlas warns that feature≠island up front). - UV repair:
transform_uv {offset, scale, pivot, island_of}(global or single-chart affine) withpreview_uv_transformdry-runs (bleed/out-of-bounds fractions against an island-occupancy grid; verdict thresholds calibrated by field test — 7% bleed on a face reads HIGH). project_paint— texture repair through SCREEN space for fragmented atlases where texel↔feature correspondence exists only on the surface: every texel in a world brush re-samples the current render at (screen + offset);surface_offsetgives camera-independent world-unit corrections. Field-proven on the portrait's baked-in eye misalignment where both UV-surgery approaches were falsified (measured: 4,424 non-semantic Voronoi islands; eye and mouth share one chart).- Articulation proofs:
explode_view {factor}separates all objects outward with exact restore (factor 0), per-object world displacements andminGapWorld(negative = pairs still overlap — numeric separation verdict, no vision pass);split_objectgainedside:"+"|"-"(the axis form silently extracted only the + half) andkeep_active(split→split batches). - Animation persistence proofs:
.mvscenev2 round-trips tracks/pivots/parents (requested Euler angles included), animated GLB round-trips as an auto-playing clip, and the app UI plays/pauses both (timeline bar for scenes, standard animation bar for GLB clips) — proven with browser screenshots. - Proof pack:
~/MeshVault_assets/proofs/— Jupiter texture-LoD ladder (4 GLBs, 347 KB→3.39 MB, densities halving per tier), articulated GLBs with exploded/pose renders, repaired portraits, animation round-trip files,INDEX.md.
- UV diagnosis:
- Articulation & keyframe animation (backlog 046) — agents (and the app) can now compose, articulate and ANIMATE scenes; designed against a 3-adversary review (engine architecture, geometry/texture repair, agent workflow) and validated by 3 live agent gauntlets (robot arm from primitives; generated-mesh starship/sportscar; portrait nod):
- Pivots & hierarchy:
set_pivot(world-point rotation origin, composed mathematically into plain wrapper TRS — the pivot-sub-group design was proven to corrupt bakes and was rejected),set_parent(world-pose-preserving re-parenting; cycles and non-uniformly-scaled ancestors refused; transforms become parent-relative with dual local+world returns), child re-parenting onremove_object, gizmo drags re-derive the logical placement. - Part extraction:
detect_parts(mesh partition → material groups → welded components, with honesty notes — image-to-3D outputs are usually ONE fused component and partial detection is normal; measured: the sportscar's rear wheels detect, its front wheels are fused) andsplit_object(partId selection with a stale-partition handshake, or plane cuts{axis, at}/{plane}; hollow cut faces disclosed in every return;suggestedPivot= the cut centroid; painted materials deep-copied; skinned/animated/instanced refused). - Scene timeline:
set_keyframe(explicit TRS or pose-capture,channelsfilter, per-key easing linear|step|ease_in|ease_out|ease_in_out),delete_keyframe/get_timeline/clear_timeline(restores pre-animation placements)/set_timeline/play/pause/seek_timeline. Hand-rolled interpolator (deterministic seek → screenshot; per-key easing; AnimationMixer's setTime×timeScale trap avoided); keyframes store logical local TRS so pivoted joints sweep true arcs; teaching notes for the slerp short-arc AND the silent 360°-identity collapse (requested-angle tracking — a naive 0→360→720 wheel spin now warns instead of silently not moving); sculpt/paint/pick refuse while playing; timeline cleared on scene replace; rig sized once per play from the swept volume. Timeline UI bar (play/scrub/keyframe ticks) in the app. - Animated GLB export:
export_glb {animation, texture_size}+ MCPexport_model {path, animation, texture_size}(12 tools) — TRS object nodes with node-relative geometry baking (GLTFExporter forces TRS mode when clips are present; the flat matrix-only export would collapse every mesh to the origin), 30 fps resampled tracks with pivots composed in, hierarchy preserved, emitted-channel verification. Round-trip verified: exported GLBs reload with thetimelineclip playing. - Motion contact sheet: MCP
screenshot {times: [...]}seeks the timeline and returns ONE auto-framed grid image (camera fits the whole swept motion; labeled tiles) — a 6-frame animation preview for ~1/10th the tokens of separate captures. - Manifest v2:
.mvscenepersists hierarchy (parent), pivots and the timeline with index-based object references (positional remapping breaks on partial loads), bounded server-side (≤192 tracks, ≤256 keys/track), two-pass loading in app + MCP.
- Pivots & hierarchy:
- Mesh & texture inspection/repair (backlog 046) — adaptive-resolution optimization by agent judgment:
inspect_region(probe or opportunity-sorted grid survey: triangles, density, edge lengths, dihedral roughness, TRUE welded open edges — same basis across all reporting tools) andsimplify_region: region-scoped decimation with a REAL boundary lock (a purpose-built constrained edge collapse — r170's SimplifyModifier has no locked vertices and its soft border cost erodes under averaged-cost selection), UV-seam welds locked (no tears), and open-boundary rims locked (split-cut edges cannot diverge into gashes — the final gauntlet's one silent-corruption finding, closed). Quantified returns (achievedRatio, locked{ring, seams, borders}). Validated: starship 120k→88k triangles with no visible change at the pinned hero angle; portrait 120k→94k.fix_mesh(degenerate-triangle drop + normal recompute by default; per-mesh winding flip opt-in, closed meshes only) with per-op counts and issue deltas.inspect_texture(per-material resolution/colorSpace/painted + area-weighted texel-density percentiles + lowest-density world spots) and repair brushes:blur_paint(footprint-masked Gaussian — atlas neighbors never bleed) andclone_paint(heal brush via world-space correspondence — UV-offset cloning is incoherent across islands; 45° normal-agreement guard; snapshot source so overlapping heals don't feed back). Both return quantified {*, meanAlpha}.- Texture LoD tiers
low/medium/high/xhigh(512→4096) accepted everywhere texture_size exists (validator aliases — numbers stay canonical),resize_texturefor paint layers, export-time downsampling for authored textures, 32M-texel budget (mipmaps off ≥2048 per SwiftShader memory math) reported inget_state().paintwith a 75% warning note.
- Composition ergonomics (backlog 046): world AABB
boundsinlist_objects(the observation that removes placement hand-math),clone_object(deep geometry + cloned paint canvases — the only duplication that keeps sculpt/paint deltas),ground_object(wrapper-based scene-floor drop),place_object(world-axis face-to-face placement with gap/align/offset),look_at(aim a local axis, returns quaternion + Euler for keyframing), terseadd_model/remove_objectreturns, wrong-active-object hints on missed brushes (the post-split trap: miss errors name the object the point actually sits on).
Fixed
- GLB→GLB export scrambled textures (critical, data loss): the exporter flipped UV V unconditionally — correct for GL-convention (loader/paint) textures, but GLB-sourced textures are ALREADY in glTF convention, so the double conversion rendered every glTF round-trip as a mosaic (found by a cycle-4 agent's reload proof). The flip (and the tangent-handedness negation) is now conditional on the material texture's
flipY; verified: re-exported portrait's TEXCOORD_0 V-range is bit-identical to the source, painted-primitive exports still convert correctly. - Full-turn keyframes silently collapsing to identity:
set_keyframe {rotation:[360,0,0]}quaternion-round-trips to 0° — the segment played no motion with no warning (the old >120° quaternion check mathematically cannot see exact 360° multiples). Requested Euler angles are now tracked per key (and round-trip through .mvscene manifests): identity collapses and >120° steps both warn with ≤120°-step advice, andget_timelineshows the REQUESTED angles. openEdgesmeant three different things acrossget_mesh_stats(welded),inspect_region(region-truncated — every perimeter edge counted "open") andfix_mesh(welded minus degenerates): all three now report TRUE welded cracks on the same basis (region-scoped counts filter against whole-mesh topology; degenerate triangles count toward edge multiplicity until actually dropped).- The
viewsbatch on the MCP screenshot tool frames the SCENE, not the active object (a split part being active put preset cameras inside the parent mesh). - AI dynamic sculpting & texture painting (backlog 045) — agents create, repair, and decorate 3D content through the control API / MCP, validated by three adversarial design reviews and three live artist-agent sessions whose friction reports each drove a fix cycle:
- Primitives (
add_primitive): box / sphere / cylinder / cone / torus / plane / capsule, with sculpt-friendly segment defaults, non-overlapping paint-safe UV atlases (stock three.js boxes map all six faces onto the same UV square — painting one face pa...
- Primitives (
MeshVault v0.6.0
Added
- Scene composition — multi-object scenes (backlog 042, stages 1–4). MeshVault grows from "one model at a time" into a scene workbench, designed against two adversarial reviews (architecture + product/security) whose must-fix lists are all implemented:
- Object registry (viewer core): N objects, each wrapped in a placement
Group; the_currentModelgetter keeps all ~90 single-object code paths and helper modules working unchanged against the ACTIVE object (invariant: a non-empty scene always has one). Replace-vs-add load semantics with a scene-generation counter (in-flight adds can never resurrect a replaced scene, adds never cancel each other); per-entry animation (deactivated objects freeze and resume without reset), reset snapshots (retaken after simplify/recompute — fixes the long-standing "reset after simplify" crash), scale/modified flags, and texture-janitor membership checks (co-loaded objects keep their janitor pass). - Placement vs geometry, kept honest:
set_object_transform/gizmo edits live ONLY on the wrapper (never baked, saved in manifests); vertex bakes (center/ground/rotate/auto-orient/simplify) operate wrapper-LOCALLY so a placed object's geometry never absorbs its scene position, and are refused for skinned models (they corrupt bind poses). The scene rig (lights, shadows, grid, fog, nav speed) re-sizes from the visible-union box on every composition change, so placed objects never lose shadows; clipping planes and measurement raycasts/markers are scene-wide; best-view/upright scoring hides non-active objects. - App UX: right-click → "Add to scene"; objects panel (select / show-hide / opacity / reset placement / remove) that auto-surfaces on composition; TransformControls gizmo (move/rotate/scale, T/R/S keys, screen-constant size, hidden in all captures); click-to-select in the viewport; dirty-scene confirm before any replace (sidebar click, drag-drop, scene open); scene save via
.mvscenewith overwrite protocol. - Persistence: version-1
.mvscenemanifests (per-object source descriptor file/archive/url + TRS transform + visibility/opacity + scene lighting/environment/background).POST /api/scene/savefollows the repo's safe-write contract (sanitized name, FORCED suffix, 409 withoutoverwrite, only-scene-files overwritten, 128-object/2 MB caps);GET /api/scene/loadvalidates and returns the manifest, objects re-resolve client-side through the guarded asset routes (per-object degradation, no server-side probe loop). Scene files list in the browser, load on click, and deep-link via?scene=. Volatile sources (drag-drops) are excluded from manifests and reported. - Agent/MCP parity:
load_model {add:true, transform}composes headless; newsave_scene/load_scenetools (11 total); new viewer commandsadd_model,list_objects,set_active_object,set_object_transform/visible/opacity,get_object_transform,reset_object_transform,remove_object,frame_all,get_scene_manifest;describe_scene/get_stategain additivescenesections with explicit active-object framing so agents never mistake one object's counts for the scene;compare_modelsrefuses to destroy a composed scene (save first). GLB export includes every visible object with placements applied, reading AUTHORED materials (never clay/ghost viewer overrides). - E2E-verified in both front-ends: browser (compose → place → gizmo → save →
?scene=reload → 2.6 MB composed GLB export) and MCP (compose with transforms → save → wipe →load_scene→ placements exact to 4 decimals → framed scene render under a pinned preset).
- Object registry (viewer core): N objects, each wrapped in a placement
Fixed
- Reset-after-simplify crash ("offset is out of bounds", documented since 2026-07-06): reset snapshots are per-object and retaken after geometry-replacing operations; Reset now honestly undoes transform bakes since the last geometry-modifying op.
- Exports no longer bake viewer display state into assets: GLB export reads the stashed original materials (a clay-mode export previously shipped clay materials) and authored opacity (a ghosted object no longer exports as transparent).
MeshVault v0.5.0
Fixed
- Multi-file models now load TEXTURED in the headless/MCP runtimes (confirmed defect from the adversarial review). Three coordinated root-cause fixes, none of them MCP-specific hacks: (1) the MCP loopback server now serves a registered model's directory companions under its unguessable token (
/models/<token>/<name>— relative MTL/texture/.binrefs resolve like normal sibling URLs; confined to the model's directory, traversal-checked); (2) the standalone viewer's default resolver resolves relative resource refs against the model's own URL directory (the platform behavior) instead of the host page, and the control-APIloadcommand acceptsrelatedFiles; (3)load_modeldiscovers companions server-side (OBJ→mtllibparsing with same-stem fallback, FBX→bounded texture scan). E2E-proven with a real MCP client: textured OBJ+MTL+PNG renders red (pixel-verified), external-buffer.gltf+.binloads. - Texture race stripped in-flight textures in ALL runtimes (root cause of the above, and a live race in the app for small models):
_isUsableTexturetreated still-loading http(s) textures as unusable, so_enhanceModeldropped them whenever the mesh parsed before its textures decoded — always the case over loopback. Textures are now classified pending vs definitively broken (completed with zero natural size), pending ones are preserved, and a one-shot janitor pass after load settles clears only genuinely failed slots (404/decode) so those materials fall back to base color.
Added
GET /api/screenshot— headless renders over plain HTTP (the external FR's third ask). One authenticated GET returns a PNG of a local model:view/azimuth+elevation/best_viewcamera control,preset=studio|neutral|dark|none(full lighting/background pinning, shared with MCP),width/height/transparent/hide_ground/fill. Render metadata in theX-MeshVault-Screenshotheader. Implementation per the adversarial review: harness on the app's own origin (PathGuard + multi-file resolution inherited from/api/asset/*), guard + 512 MB size cap BEFORE any browser work, single-flight lock (429 when busy), hard timeout (504,MESHVAULT_SCREENSHOT_TIMEOUT), per-request unload+reload+preset re-pin (no state bleed), distinct 503s for missing playwright vs missing Chromium, browser closed in app lifespan. Measured 8–17 s per call on software GL. New modules:backend/headless_viewer.py(shared Playwright runtime +LocalModelServer+ presets + companion discovery — also the seam for a future batch-render CLI, backlog 024) andbackend/screenshot_api.py; the MCP server now composes the same runtime.get_app_stateMCP tool +GET/POST /api/agent/state— the reverse co-review bridge. App tabs report what the human is looking at (asset path, name, camera; ~2 s cadence, deduplicated); an agent joining the session reads it back and continues headless (load_modelthe path,set_camerathe pose). E2E-verified live: tab camera moves were reflected within one reporting tick, and a real MCP client reproduced the human's exact view.
MeshVault v0.4.0
Added
- Shared session between headless agents and the app (agent bridge) — the MCP server and the browser app are separate processes; agents could inspect a model that the human co-reviewer couldn't see. Now: the app publishes
{url, token}to~/.meshvault/app_session.json(0600, pid-checked cleanup) at launch; the new MCP toolopen_in_appdiscovers it and POSTs the agent's current model path + camera pose to the newPOST /api/agent/openendpoint (PathGuard-confined, token-authenticated, camera payload validated at the boundary), which fans out to every open app tab over the newGET /api/eventsSSE stream (session-cookie auth —EventSourcecan't set headers; bounded per-client queues so a wedged tab can't grow server memory). The tab loads the same file through the normal asset flow (related files, recents, export state intact) and applies the agent's exact camera; if the model is already on screen, only the camera moves. Returns{clients, deep_link}— when no tab is connected, the deep link reproduces the push. E2E-verified: real MCP client → real server → real browser tab, camera position matching to the millimeter. Example:examples/mcp/co_review.py. - URL deep links in the local app (
?path=/?dir=) — URL parameters now win over the localStorage "last directory" default:/?dir=/abs/folderopens a folder;/?path=/abs/model.glbopens the parent folder, highlights the asset in the sidebar, and loads it;/?path=/abs/pack.zip!inner/model.objdoes the same for archive members (the app's composite asset key). The URL stays in sync while browsing (replaceState), so the address bar is always a shareable link to the current view. Paths are matched exactly first, then by basename within the browsed directory (the server canonicalizes paths —/tmpvs/private/tmpon macOS would otherwise break caller-spelled links). Invalid links toast an error and fall back to the normal start. New frontend modulefrontend/js/agent_link.js;FileBrowsergainedfindAsset/highlightAsset/setNavigateListenerand the sharedassetKey()helper (now also used by recents). - Reproducible screenshot presets over MCP (
screenshot { preset }) —"studio"/"neutral"/"dark"pin every pixel-affecting lighting/background variable (IBL enabled+intensity, key/fill/ambient intensities, key direction, exposure, background) to documented values before capture, so renders are comparable across sessions, machines, and agents even when a session tweaked its lights first. Measured cross-session reproducibility on SwiftShader: mean per-channel diff 0.03/255 after deliberately sabotaging the second session's lighting. Preset values documented indocs/mcp.md. set_cameraviewer command (viaviewer_execute) now acceptsfov(degrees, 1–179) so a camera pose captured withget_camerain one session can be reproduced exactly in another (used by the agent bridge).- Real CLI for the
meshvaultentry point —--helpprints usage + the environment-variable reference instead of starting the server (external tester finding),--versionprints the version,--portoverrides$PORT.
Fixed
- Stale session file after an unclean death (external tester finding) — a SIGKILLed app can't remove
~/.meshvault/app_session.json, and the leftover file pointed agents at a port where an older instance answered (404 chase). Two-sided fix: (1) the file is now published only after the server has actually bound its port (a launch that fails to bind publishes nothing and can't clobber the live instance's file), and (2)open_in_appdiscovery pid-probes the file's publisher (os.kill(pid, 0), POSIX) — a dead publisher is reported asstale session file (pid N dead), the file is cleaned up, and the tool tells the human to restart the app. Pushes that reach an older MeshVault without the bridge now get a specific "older MeshVault (< 0.4) still running — restart it" error instead of a bare 404. - Graceful shutdown with open SSE tabs — uvicorn's default shutdown waits for active connections indefinitely, so Ctrl-C hung while an app tab was connected to
/api/events; bounded withtimeout_graceful_shutdown=3(verified live). docs/api.mddrift: removed the stale claims that/docsSwagger UI is available (it is deliberately disabled, along with/openapi.json— now stated explicitly for script agents) and that?token=query auth is accepted (deliberately rejected; use headers).
MeshVault v0.3.1
Fixed
- MCP extra installs a working server: the optional
mcpdependency floor is now>=1.28— older SDK versions (e.g. 1.12) that pip could previously resolve crash on import withTypeError: issubclass() arg 1 must be a classwhen registering tools withdict | Noneparameters. Found by verifyingpip install "meshvault[mcp]"from PyPI in a clean environment.
MeshVault v0.3.0
Added
-
MCP server (
meshvault-mcp) — agents on Claude/Cursor/any MCP client can now drive the viewer natively. A thin 6-tool surface (load_model,describe_scene,viewer_execute,list_viewer_commands,get_state,screenshot) routed through the existing control API, hosted in a headless Chromium page behind a loopback file server.load_modelaccepts an http(s) URL or an absolute local file path (local files are served under unguessable tokens; CORS-blocked URLs fall back to a size-capped server-side download) and returns the load result plus a full scene description in one call. Screenshots come back as real MCP image content, with abest_viewoption for one-call hero shots. Optional install:pip install "meshvault[mcp]"+playwright install chromium; docs indocs/mcp.md. Adversarially reviewed (protocol-clean stdio, crash-free under abuse, no orphan processes, path-confined loopback server). -
Part-level exploration (
focus) —describe_scene/get_scene_infonow give every mesh a stableidplus its world-spacecenter/size, and the newfocus { id | name | point }command frames that part: it keeps the current view direction, retargets the orbit controls, and rescales the clip planes/zoom limits so even a 1 cm part on a 10 m assembly is visible (previously it vanished behind the near plane — verified pixel-counted). Name matching is tiered (exact > case-insensitive > substring, meshes + groups) with candidate-listing errors;reset_camerareturns to the whole-model view. Designed down from two adversarial reviews: ids over names (real-world mesh names are mostly meaningless), no auto view-direction/occlusion magic (delegated toset_clip/wireframe, documented). -
Shape comparison in the app — deviation heatmap (backlog 041 v1) — right-click any asset → "Compare to loaded model": MeshVault registers the two shapes (reusing the same Python engine as the MCP
compare_modelstool, via a newPOST /api/compare) and paints a per-vertex deviation heatmap on the loaded model (blue = matches, red = differs), with a verdict panel (identical/near-identical/modified/different, shape-difference %, scale, rotation, missing-region and borderline/mirror warnings). The candidate is sampled in a short-lived offscreen viewer — only one model is displayed. Heatmap usesthree-mesh-bvh(app bundle only; the standalone/agent bundle is unaffected). The multi-object co-loaded scene (v2) remains proposed. -
compare_models— geometric 1-vs-N model comparison via shape registration — compares one reference against up to 8 candidates by REGISTERING their surfaces (deterministic area-weighted sampling → PCA-initialized trimmed ICP → symmetric chamfer/Hausdorff normalized by the reference bbox diagonal, floor-corrected for sampling noise), not by screenshots. Reports per candidate: alignment (scale ratio — catches unit mismatches, rotation, translation), distances (+ asymmetry flagging missing/extra regions), a classification (identical / near_identical / same_shape_modified / different) with aborderlineflag andwarnings(mirror detection, near-boundary, partial-overlap), structural deltas, and a similarity ranking. Backed by the newsample_pointsviewer command andbackend/mesh_compare.py(numpy only). Adversarially reviewed by 4 agents (18/18 correct on real assets; transform recovery exact) with found issues fixed: floor-input validation, p95-tail classification for local edits, trimmed ICP for partial overlap, ICP off the event loop, mirror flagging. Unit-tested intests/test_mesh_compare.py; example inexamples/mcp/compare_shapes.py. -
get_mesh_stats— numeric surface-quality statistics — per-mesh + total surface area, volume (null for open meshes, where signed-volume sums are origin-dependent), edge-length distribution, sliver %, dihedral roughness, defect counts, andissuePoints: world-space defect locations an agent canfocuson. Verified against trimesh ground truth (exact to the emitted precision on closed meshes). Budget-bounded at 300k triangles. -
Material truth for agents —
describe_scenematerials now report per-slot texture facts ({width, height, colorSpace}) and, whenever the viewer's preview clamps altered PBR values, the asset's ORIGINAL values (authored+modifiedByViewer). Viewer-created STL/PLY default materials are excluded (they have no authored data). Material names now survive the upgrade path. -
MCP multi-view capture —
screenshotacceptsviews: ["front","left","45,20", ...](max 12/call) and returns the images in order behind a JSON metadata block;best_view: truenow returns the chosen azimuth/elevation/score instead of discarding them. -
MCP usage examples —
examples/mcp/: four runnable agent workflows (inspect, compare iterations, explore parts, hero shots) plus the shared 40-line client helper, with a README documenting a real multi-model investigation session.
Fixed
- MCP: concurrent
load_modelcalls no longer cross-contaminate — the load→describe pair is serialized per session, so parallel loads each return the description of their own model (found by the multi-agent field test). - Measurement overlay is now clearable — new
clear_measurementcommand, andset_measure_mode {enabled:false}always removes the markers/line/label (a programmaticmeasurepreviously polluted every subsequent screenshot). - A failed load no longer discards the current model —
loadModelnow fetches and parses the new file first and only then swaps models, so a bad URL/path leaves the loaded scene intact (previously the viewer cleared immediately and a failed load ended in an empty scene). - Transforms no longer corrupt quantized glTF —
rotate/center/groundbake world transforms into vertex buffers; on KHR_mesh_quantization models (normalized Int16/Uint16 positions, common with KTX2/Meshopt assets) that wrote world-scale floats into integer arrays and destroyed the geometry. Attributes are now dequantized to Float32 before baking. describe_scene— structured scene snapshot for AI agents — one command returns everything a text-only agent needs to reason about the loaded model without screenshots: a plain-language summary, live inventory (meshes/materials/textures/triangles — recomputed from the current buffers, so correct after simplify/rotate), world bounds + a format-aware size hint (glTF units are meters per spec), a capped hierarchy outline, the largest meshes, asset materials (accurate even while a solid/normals render-mode override is active), and the current camera/render state. Includes geometry QA (backlog 031 folded in): missing normals/UVs, degenerate faces (relative sliver test), watertightness and non-manifold edges computed on position-welded vertices (UV seams don't false-positive), flipped normals via signed volume, NaN positions, and scale sanity — skipped with an explicitchecks_skippednote above a 300k-triangle budget so the call stays sub-millisecond on huge scenes. Verified against trimesh ground truth (zero false positives), token-bounded (~2 KB), deterministic, and mutation-free (pixel-identical viewer state before/after).- Compressed glTF support (Draco / KTX2-Basis / Meshopt) —
GLTFLoadernow runs with all three decoders wired, so compressed.glb/.gltf(a large share of real-world models, especially pasted-URL loads in the web viewer) open correctly instead of failing. Decoder assets are vendored locally (frontend/vendor/, ~1.3 MB, served at/static/vendor/, copied into the Pages site) — no CDN, verified fully offline in the app, the standalone bundle, and a/repo/base-path simulation. Decoders are created once per viewer (bounded worker pools, disposed ondestroy()), and grid thumbnails share them so compressed models get previews too. Loader failures now produce readable messages (no more[object Object]). - Image-based lighting (IBL) — a procedural studio environment (
RoomEnvironment→ PMREM, no HDRI asset to ship) lights PBR materials with real reflections; metallic models no longer read flat. On by default at intensity 1 with the existing key/fill/ambient rig kept as the baseline; turning it off restores the exact pre-IBL look. Agent-controllable viaset_environment { enabled, intensity: 0..5, asBackground }/get_environment, reported ingetState().display.environment. The matte solid render mode suspends the environment so the clay view stays readable, and disabling IBL also clears an environment background. Adversarially verified: monotonic intensity response, model readable from the environment alone with all analytic lights at zero, PMREM resources freed ondestroy(). - Light static web viewer + GitHub Pages CI/CD —
web/index.htmlis a client-only landing page (drag-drop / file-open /?src=<url>) that loads the self-containedmeshvault-viewer.jsbundle and exposes the control API onwindow.mv..github/workflows/pages.ymlbuilds the bundle and publishesweb/to thegh-pagesbranch (also deployable to a separate repo — seeweb/README.md). - Render modes clarified to three first-class views —
set_render_mode { textured | solid | wireframe }(+normals); the viewer toolbar button now cycles mesh+texture → mesh → wireframe with a T/M/W badge. - Standalone embeddable viewer + control API for AI agents — the rendering core is now decoupled from the backend (a single injected
resolveResource()seam) and shipped as a second, server-less bundle (frontend/dist/meshvault-viewer.js, demo at/static/viewer.html).createViewer(container)returns aViewerControlAPIwith one JSON entry pointexecute({action, params}) → {ok, result|error}, self-describinglistCommands(), observablegetState()/getSceneInfo()/get_bounds(world bounding box), and events (loaded,error,animations,measurement). Commands that need a model return{ok:false}when ...
v0.2.0
Full Changelog: https://github.com/lpalbou/MeshVault/commits/v0.2.0