Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d06b8a8
fix: narrow broad excepts in collision_export mesh/visual writers
paul-nechifor Jul 2, 2026
85ec55d
fix: narrow broad except in collision_policy primitive fitting
paul-nechifor Jul 2, 2026
4f986b0
fix: stop mislabeling missing CoACD as a decomposition failure
paul-nechifor Jul 2, 2026
dcf35c8
refactor: dedupe CoACD log-silencing into a shared helper
paul-nechifor Jul 2, 2026
5b10a0f
refactor: delete dead GeomEmission dataclass, fix stale docstrings
paul-nechifor Jul 2, 2026
6f395f7
chore: drop scene extra deps already declared as core dependencies
paul-nechifor Jul 2, 2026
25e0d00
refactor: name min-extent epsilon, dedupe AABB-extent clamp helper
paul-nechifor Jul 2, 2026
4ba6ce8
refactor: name the floor-probe ray height constant in mesh.py
paul-nechifor Jul 2, 2026
998c189
refactor: name magic numbers in glb.py
paul-nechifor Jul 2, 2026
6949ec0
fix: narrow broad except in normalized_texture_bytes
paul-nechifor Jul 2, 2026
eeb155b
refactor: reuse glb.py's public GLB layout constants in test_glb.py
paul-nechifor Jul 2, 2026
73c5233
refactor: drop redundant rerun browser visual profile
paul-nechifor Jul 2, 2026
3e0c74e
refactor: name magic numbers in collision_export.py
paul-nechifor Jul 2, 2026
548c407
refactor: name the entity id sample cap constant in cook.py
paul-nechifor Jul 2, 2026
b7213b7
refactor: name the gltfpack warning tail-lines constant
paul-nechifor Jul 2, 2026
cb19aec
fix: respect XDG_CACHE_HOME for scene cook cache directories
paul-nechifor Jul 2, 2026
e764235
fix: fail entity collision tests on missing open3d/coacd instead of s…
paul-nechifor Jul 2, 2026
aa2b07e
refactor: object -> Any in scene_cooking type hints, drop stale ignores
paul-nechifor Jul 2, 2026
7ddb7b5
refactor: hoist numpy import to module scope in entities/collision.py
paul-nechifor Jul 2, 2026
249fbba
refactor: hoist json/scipy imports to module scope in collision_expor…
paul-nechifor Jul 2, 2026
3a73f33
chore: add lazy-import comment for mujoco in cook.py
paul-nechifor Jul 2, 2026
a166308
refactor: hoist trimesh import to module scope in browser/collision.py
paul-nechifor Jul 2, 2026
dd7dbeb
chore: comment the inline browser.collision import in test_cooking.py
paul-nechifor Jul 2, 2026
18c32cf
refactor: printf-style logging -> structured kwargs
paul-nechifor Jul 2, 2026
001def1
perf: vectorize duplicated fan-triangulation in mesh.py
paul-nechifor Jul 2, 2026
6fa949f
refactor: bare np.ndarray -> NDArray[...] in browser/collision.py
paul-nechifor Jul 2, 2026
d762370
refactor: bare np.ndarray -> NDArray[...] in planning.py
paul-nechifor Jul 2, 2026
8e192ef
refactor: bare np.ndarray -> NDArray[...] in mesh.py
paul-nechifor Jul 2, 2026
8400a4d
refactor: bare np.ndarray -> NDArray[...] in collision_export.py
paul-nechifor Jul 2, 2026
d2d410a
refactor: bare np.ndarray -> NDArray[...] in collision_policy.py
paul-nechifor Jul 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dimos/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
except ImportError:
CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
STATE_DIR = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state")) / "dimos"
CACHE_DIR = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "dimos"
else:
CONFIG_DIR = Path(GLib.get_user_config_dir())
STATE_DIR = Path(GLib.get_user_state_dir()) / "dimos"
CACHE_DIR = Path(GLib.get_user_cache_dir()) / "dimos"

DIMOS_PROJECT_ROOT = Path(__file__).parent.parent

Expand Down
12 changes: 7 additions & 5 deletions dimos/experimental/scene_cooking/browser/collision.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
from typing import Any

import numpy as np
from numpy.typing import NDArray
import open3d as o3d # type: ignore[import-untyped]
import trimesh # type: ignore[import-untyped]

from dimos.experimental.scene_cooking.mujoco.collision_policy import CollisionSpec
from dimos.experimental.scene_cooking.package_config import BrowserCollisionSpec
Expand Down Expand Up @@ -115,8 +117,6 @@ def cook_browser_collision(


def _write_glb(mesh: o3d.geometry.TriangleMesh, path: Path) -> None:
import trimesh

# Quadric decimation collapses triangles but leaves the original input
# vertices in the buffer (referenced by nothing). Drop them before export,
# else the GLB carries millions of orphan vertices (~25x larger on a 100k
Expand Down Expand Up @@ -184,8 +184,8 @@ def _build_fused_collision_mesh(
prims: list[ScenePrimMesh],
spec: CollisionSpec,
) -> o3d.geometry.TriangleMesh:
vertices: list[np.ndarray] = []
faces: list[np.ndarray] = []
vertices: list[NDArray[np.float64]] = []
faces: list[NDArray[np.int64]] = []
vertex_offset = 0
for prim in prims:
mesh = _mesh_for_prim(prim, spec)
Expand Down Expand Up @@ -257,7 +257,9 @@ def _mesh_for_prim(
return mesh


def _mesh_from_arrays(vertices: np.ndarray, faces: np.ndarray) -> o3d.geometry.TriangleMesh:
def _mesh_from_arrays(
vertices: NDArray[np.float64], faces: NDArray[np.int64]
) -> o3d.geometry.TriangleMesh:
mesh = o3d.geometry.TriangleMesh()
mesh.vertices = o3d.utility.Vector3dVector(vertices)
mesh.triangles = o3d.utility.Vector3iVector(faces.astype(np.int32))
Expand Down
3 changes: 2 additions & 1 deletion dimos/experimental/scene_cooking/browser/visuals.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
".ply",
}
_GLTFPACK_INPUT_SUFFIXES = {".gltf", ".glb", ".obj"}
_GLTFPACK_WARNING_TAIL_LINES = 30

_BLENDER_SCRIPT = r"""
import pathlib
Expand Down Expand Up @@ -340,7 +341,7 @@ def _export_with_gltfpack(
) from exc
raise
if output and "Warning:" in output:
logger.warning("gltfpack output:\n%s", _tail(output, 30))
logger.warning("gltfpack output:\n%s", _tail(output, _GLTFPACK_WARNING_TAIL_LINES))
report = _read_json(report_path)
return ("gltfpack", report)

Expand Down
29 changes: 29 additions & 0 deletions dimos/experimental/scene_cooking/coacd_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2025-2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Shared CoACD setup for scene-cooking convex decomposition."""

from __future__ import annotations


def silence_coacd_logging() -> None:
"""Quiet CoACD's per-invocation C-library log spam.

``set_log_level`` is a cheap call, so this is safe to call before
every ``coacd.run_coacd`` invocation instead of caching "already
silenced" state on a function attribute.
"""
import coacd # type: ignore[import-not-found, import-untyped]

coacd.set_log_level("error")
13 changes: 9 additions & 4 deletions dimos/experimental/scene_cooking/cook.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
SCENE_PACKAGE_DIR = get_data_dir("scene_packages")
_PACKAGE_KEY_LEN = 12
_COOK_VERSION = 4
#: Cap on entity id samples recorded in cook stats -- diagnostics only, not
#: the full entity list (that lives in ``scene.meta.json``).
_ENTITY_ID_SAMPLE_CAP = 100


def cook_scene_package(
Expand Down Expand Up @@ -140,7 +143,7 @@ def cook_scene_package(
if entities:
stats["interactables"] = {
"count": len(entities),
"id_samples": [entity["id"] for entity in entities[:100]],
"id_samples": [entity["id"] for entity in entities[:_ENTITY_ID_SAMPLE_CAP]],
"static_visual_filter": "plan/blender",
}

Expand Down Expand Up @@ -237,7 +240,7 @@ def cook_scene_package(
stats=stats,
)
package.write_metadata()
logger.info("scene package cooked: %s", package.metadata_path)
logger.info("scene package cooked", metadata_path=package.metadata_path)
return package


Expand All @@ -262,9 +265,9 @@ def _cook_entity_collision(
visual_path = entity.get("visual_path")
if not visual_path or not Path(visual_path).exists():
logger.warning(
"entity %s: mesh entity has no cooked visual GLB; "
"mesh entity has no cooked visual GLB; "
"no collision hulls (runtime falls back to AABB box)",
entity.get("id"),
entity_id=entity.get("id"),
)
continue
hull_paths = cook_entity_collision_hulls(
Expand Down Expand Up @@ -330,6 +333,8 @@ def _compile_mujoco_binary(scene_xml_path: Path, *, rebake: bool) -> tuple[Path,
requires the XML wrapper unless a robot-specific composed binary is
produced separately.
"""
# Lazy: mujoco is a `sim` extra, not a `scene` one -- browser/rerun-only
# cooks (mujoco.compile_binary=False) shouldn't require it installed.
import mujoco

binary_path = scene_xml_path.with_suffix(".mjb")
Expand Down
31 changes: 15 additions & 16 deletions dimos/experimental/scene_cooking/entities/collision.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@
from __future__ import annotations

from pathlib import Path
from typing import Any

import numpy as np

from dimos.experimental.scene_cooking.coacd_util import silence_coacd_logging
from dimos.utils.logging_config import setup_logger

logger = setup_logger()
Expand All @@ -59,9 +63,9 @@ def cook_entity_collision_hulls(
"""Decompose one entity mesh into convex hulls under ``out_dir``.

Idempotent: existing ``hull_*.obj`` files are reused unless ``rebake``.
Falls back to a single convex hull when CoACD is unavailable or fails
on the mesh. Returns ``[]`` (with a warning) when the mesh can't be
read at all — the runtime composer then uses an AABB box.
Falls back to a single convex hull when CoACD fails on the mesh.
Returns ``[]`` (with a warning) when the mesh can't be read at all —
the runtime composer then uses an AABB box.
"""
mesh_path = Path(visual_mesh_path)
out_dir = Path(out_dir)
Expand All @@ -88,8 +92,6 @@ def cook_entity_collision_hulls(
for stale in out_dir.glob("hull_*.obj"):
stale.unlink()

import numpy as np

out_paths: list[Path] = []
if parts:
for i, (vertices, triangles) in enumerate(parts):
Expand All @@ -108,19 +110,16 @@ def cook_entity_collision_hulls(
return out_paths


def _run_coacd(mesh: object, mesh_path: Path) -> list[tuple[object, object]]:
def _run_coacd(mesh: Any, mesh_path: Path) -> list[tuple[Any, Any]]:
"""CoACD parts for an open3d mesh; ``[]`` means fall back to one hull."""
try:
import coacd # type: ignore[import-not-found, import-untyped]
import numpy as np
import coacd # type: ignore[import-not-found, import-untyped]

if not getattr(_run_coacd, "_coacd_silenced", False):
coacd.set_log_level("error")
_run_coacd._coacd_silenced = True # type: ignore[attr-defined]
silence_coacd_logging()
Comment on lines +115 to +117

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 import coacd now outside tryModuleNotFoundError is no longer caught

Previously the entire coacd call (including the import) was inside the except Exception block, so a missing coacd installation would return [] and fall back to a single convex hull silently. Now import coacd is unconditional, so a missing module raises ModuleNotFoundError straight to the caller. The docstring and test file (import coacd # noqa: F401 ensures collection fails loudly) confirm this is intentional — but it is a breaking behaviour change for callers that relied on the silent fallback and may be running without the scene extra installed. Is this intentional — i.e. coacd is now a hard requirement whenever cook_entity_collision_hulls is called, not an optional accelerator?


try:
cm = coacd.Mesh(
np.asarray(mesh.vertices, dtype=np.float64), # type: ignore[attr-defined]
np.asarray(mesh.triangles, dtype=np.int32), # type: ignore[attr-defined]
np.asarray(mesh.vertices, dtype=np.float64),
np.asarray(mesh.triangles, dtype=np.int32),
)
parts = coacd.run_coacd(
cm,
Expand All @@ -130,9 +129,9 @@ def _run_coacd(mesh: object, mesh_path: Path) -> list[tuple[object, object]]:
mcts_iterations=_COACD_MCTS_ITERATIONS,
mcts_nodes=_COACD_MCTS_NODES,
)
return list(parts)
except Exception as exc:
except (AssertionError, RuntimeError, ValueError) as exc:
logger.warning(
"entity hulls: CoACD failed for %s (%s); using single convex hull", mesh_path, exc
)
return []
return list(parts)
7 changes: 2 additions & 5 deletions dimos/experimental/scene_cooking/entities/test_collision.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,20 @@

from pathlib import Path

import pytest
import coacd # noqa: F401 ensures collection fails loudly if coacd is missing
import open3d as o3d

from dimos.experimental.scene_cooking.entities.collision import cook_entity_collision_hulls


def _write_box_mesh(path: Path) -> None:
o3d = pytest.importorskip("open3d")
mesh = o3d.geometry.TriangleMesh.create_box(0.1, 0.1, 0.1)
o3d.io.write_triangle_mesh(str(path), mesh)


def test_cook_entity_collision_hulls(tmp_path: Path) -> None:
source = tmp_path / "visual.obj"
_write_box_mesh(source)
pytest.importorskip("coacd")
out_dir = tmp_path / "mujoco_collision"

hulls = cook_entity_collision_hulls(source, out_dir)
Expand All @@ -44,7 +43,6 @@ def test_cook_entity_collision_hulls(tmp_path: Path) -> None:
def test_cook_entity_collision_hulls_is_idempotent(tmp_path: Path) -> None:
source = tmp_path / "visual.obj"
_write_box_mesh(source)
pytest.importorskip("coacd")
out_dir = tmp_path / "mujoco_collision"

first = cook_entity_collision_hulls(source, out_dir)
Expand All @@ -60,7 +58,6 @@ def test_cook_entity_collision_hulls_is_idempotent(tmp_path: Path) -> None:


def test_cook_entity_collision_hulls_unreadable_mesh(tmp_path: Path) -> None:
pytest.importorskip("open3d")
source = tmp_path / "visual.obj"
source.write_text("not a mesh\n")

Expand Down
5 changes: 3 additions & 2 deletions dimos/experimental/scene_cooking/entities/visuals.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pathlib import Path
import shutil
import tempfile
from typing import Any

from dimos.experimental.scene_cooking.command import (
blender_output_line_is_interesting,
Expand Down Expand Up @@ -280,7 +281,7 @@ def cook_plan_visual_assets(
return static_visual_source


def _blender_plan_json(plan: SceneCookPlan, static_visual_source: Path) -> dict[str, object]:
def _blender_plan_json(plan: SceneCookPlan, static_visual_source: Path) -> dict[str, Any]:
return {
"visual_plan_version": _VISUAL_PLAN_VERSION,
"alignment": {
Expand All @@ -307,7 +308,7 @@ def _blender_plan_json(plan: SceneCookPlan, static_visual_source: Path) -> dict[
}


def _manifest_matches(path: Path, expected: dict[str, object]) -> bool:
def _manifest_matches(path: Path, expected: dict[str, Any]) -> bool:
if not path.exists():
return False
try:
Expand Down
Loading
Loading