Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,40 @@ launchers/_shared/config.local.sh

# Browser automation scratch
/.playwright-mcp/

# Fleet size floor: generated payloads stay outside checkouts.
build/
builds/
out/
output/
outputs/
exports/
dist/
tmp/
temp/
.venv/
.venv-*/
node_modules/
.worktrees/
worktrees/
.claude/worktrees/
runs/
evidence/
*.log
*.zip
*.7z
*.rar
*.iso
*.chd
*.bin
*.mov
*.wav
*.psd
!data/**

# Local compatibility symlinks are files to Git (directory patterns do not match).
/.playwright-mcp
/.venv
/.venvs
/build
/outputs
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
- Robustness fixes for issues #5-#11 (#18): VD offsets are bounded in `vdtool`/`uo_vd_writer`, outfit-lab `vd.py` clips
runs, `vdtool` prints usage on bad arguments, `jsonio` writes are atomic, `restore()` compares sha256, annotation
errors are narrowed and warn, and the studio skips an unreadable job status.
- Issue #12 (portability) is partly done in #18: the `region_fonts` font fallback and `find_blender` discovery. The
rest is still open (SM-12).
- Issue #12 (portability) fixed (#18, #30): the `region_fonts` font fallback, one `find_blender` discovery shared by
the tools, and no hardcoded Windows paths (the hardcoded-path test matches backslash and slash).

### Local servers (security)
- Content Studio, Fit Lab and the live pose editor bind to the loopback interface only and check `Host` and `Origin`
Expand Down
36 changes: 36 additions & 0 deletions tests/integration/blender_import_asset_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""A skinned GLB keeps its real mesh but excludes imported bone-display geometry."""
import ast
from pathlib import Path
import tempfile

import bpy
from mathutils import Vector
import numpy as np


root = Path(__file__).resolve().parents[2]
tree = ast.parse((root / 'tools/uo-content/blender_build.py').read_text(encoding='utf-8'))
function = next(node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == 'import_asset')
exec(compile(ast.Module(body=[function], type_ignores=[]), 'import_asset', 'exec'))

with tempfile.TemporaryDirectory() as temp:
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.object.armature_add()
rig = bpy.context.object
bpy.ops.mesh.primitive_cube_add(size=0.25, location=(0, 0, 2))
mesh = bpy.context.object
# A legitimate mesh may itself be named Icosphere; filtering by name is wrong.
mesh.name = 'Icosphere'
mesh.vertex_groups.new(name=rig.data.bones[0].name).add(list(range(8)), 1, 'REPLACE')
mesh.modifiers.new('Skin', 'ARMATURE').object = rig
mesh.parent = rig
path = Path(temp) / 'skinned.glb'
bpy.ops.export_scene.gltf(filepath=str(path), export_format='GLB')
bpy.ops.wm.read_factory_settings(use_empty=True)
objects = import_asset(path)
assert len(objects) == 1, [obj.name for obj in objects]
points = np.array([tuple(objects[0].matrix_world @ vertex.co) for vertex in objects[0].data.vertices])
np.testing.assert_allclose(points.min(axis=0), [-0.125, -0.125, 1.875], atol=1e-5)
np.testing.assert_allclose(points.max(axis=0), [0.125, 0.125, 2.125], atol=1e-5)
assert set(bpy.data.objects) == set(objects)
print('Skinned GLB imports only supplied geometry; fit bounds preserved.')
6 changes: 5 additions & 1 deletion tools/uo-content/blender_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,11 @@ def import_asset(path):
for o in dst.objects:
if o: bpy.context.scene.collection.objects.link(o)
imported=set(bpy.data.objects)-before
meshes=[o for o in imported if o.type=='MESH']
# glTF import creates mesh custom shapes for its skeleton display. They are
# helpers, not supplied geometry, and would corrupt both fit bounds and sprites.
bone_shapes={bone.custom_shape for o in imported if o.type=='ARMATURE'
for bone in o.pose.bones if bone.custom_shape is not None}
meshes=[o for o in imported if o.type=='MESH' and o not in bone_shapes]
if not meshes: raise ValueError('No mesh found in supplied asset.')
dg=bpy.context.evaluated_depsgraph_get()
# Flatten an imported asset's current shape before binding it to the canonical rig.
Expand Down
6 changes: 6 additions & 0 deletions workspace/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# workspace/

Task 553 moved the existing local payloads into the shared work area's `workspace/`
directory. This checkout keeps local symbolic links for legacy paths used by the
editor, pipeline launchers and authoring scripts. The data is stored outside the
checkout; preserve the links when using those legacy paths. No authoring data was
deleted. New jobs should use `SPRITEMOTION_WORK_ROOT` directly.

Generated payloads now belong under `SPRITEMOTION_WORK_ROOT` (Windows default:
`D:/_agentShared/work/spritemotion`); review screenshots, logs and clips belong in
central evidence runs. New content-studio jobs and checkout-specific Python
Expand Down
Loading