blendiff 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blendiff-0.2.0/.gitignore +34 -0
- blendiff-0.2.0/LICENSE +21 -0
- blendiff-0.2.0/PKG-INFO +147 -0
- blendiff-0.2.0/README.md +120 -0
- blendiff-0.2.0/blendiff/__init__.py +40 -0
- blendiff-0.2.0/blendiff/cli/__init__.py +6 -0
- blendiff-0.2.0/blendiff/cli/__main__.py +214 -0
- blendiff-0.2.0/blendiff/cli/api.py +281 -0
- blendiff-0.2.0/blendiff/data_model/__init__.py +13 -0
- blendiff-0.2.0/blendiff/data_model/conflict.py +196 -0
- blendiff-0.2.0/blendiff/data_model/diff.py +109 -0
- blendiff-0.2.0/blendiff/data_model/material.py +139 -0
- blendiff-0.2.0/blendiff/data_model/scene.py +131 -0
- blendiff-0.2.0/blendiff/diff_engine/__init__.py +2 -0
- blendiff-0.2.0/blendiff/diff_engine/diff_engine.py +249 -0
- blendiff-0.2.0/blendiff/diff_engine/material_diff.py +256 -0
- blendiff-0.2.0/blendiff/diff_engine/render_diff.py +84 -0
- blendiff-0.2.0/blendiff/export/__init__.py +1 -0
- blendiff-0.2.0/blendiff/export/html_exporter.py +547 -0
- blendiff-0.2.0/blendiff/extractor/__init__.py +2 -0
- blendiff-0.2.0/blendiff/extractor/material_extractor.py +185 -0
- blendiff-0.2.0/blendiff/extractor/render_extractor.py +83 -0
- blendiff-0.2.0/blendiff/extractor/scene_extractor.py +173 -0
- blendiff-0.2.0/blendiff/merge_engine/__init__.py +2 -0
- blendiff-0.2.0/blendiff/merge_engine/applier.py +252 -0
- blendiff-0.2.0/blendiff/merge_engine/merge_engine.py +305 -0
- blendiff-0.2.0/blendiff/serializer/__init__.py +2 -0
- blendiff-0.2.0/blendiff/serializer/scene_serializer.py +150 -0
- blendiff-0.2.0/blendiff/storage/__init__.py +1 -0
- blendiff-0.2.0/blendiff/storage/sidecar.py +243 -0
- blendiff-0.2.0/blendiff/ui/__init__.py +11 -0
- blendiff-0.2.0/blendiff/ui/merge_panel.py +219 -0
- blendiff-0.2.0/blendiff/ui/operators.py +580 -0
- blendiff-0.2.0/blendiff/ui/panels.py +257 -0
- blendiff-0.2.0/docs/architecture.md +181 -0
- blendiff-0.2.0/docs/ci_example.yml +78 -0
- blendiff-0.2.0/pyproject.toml +60 -0
- blendiff-0.2.0/tests/test_cli_api.py +381 -0
- blendiff-0.2.0/tests/test_data_model.py +66 -0
- blendiff-0.2.0/tests/test_diff_engine.py +242 -0
- blendiff-0.2.0/tests/test_html_exporter.py +294 -0
- blendiff-0.2.0/tests/test_material_diff.py +393 -0
- blendiff-0.2.0/tests/test_merge_engine.py +432 -0
- blendiff-0.2.0/tests/test_render_diff.py +385 -0
- blendiff-0.2.0/tests/test_serializer.py +168 -0
- blendiff-0.2.0/tests/test_sidecar.py +256 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
.Python
|
|
7
|
+
|
|
8
|
+
# Packaging
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
*.egg
|
|
13
|
+
MANIFEST
|
|
14
|
+
|
|
15
|
+
# PyPI / Hatch
|
|
16
|
+
.hatch/
|
|
17
|
+
|
|
18
|
+
# Testing
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
htmlcov/
|
|
22
|
+
|
|
23
|
+
# Blender
|
|
24
|
+
blendiff.zip
|
|
25
|
+
*.blend1
|
|
26
|
+
*.blend2
|
|
27
|
+
|
|
28
|
+
# OS
|
|
29
|
+
.DS_Store
|
|
30
|
+
Thumbs.db
|
|
31
|
+
|
|
32
|
+
# IDE
|
|
33
|
+
.vscode/
|
|
34
|
+
.idea/
|
blendiff-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vishrut
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
blendiff-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blendiff
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Semantic diff, snapshot history, and assisted merge for Blender .blend files
|
|
5
|
+
Project-URL: Homepage, https://github.com/Vishrut2403/blendiff
|
|
6
|
+
Project-URL: Repository, https://github.com/Vishrut2403/blendiff
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/Vishrut2403/blendiff/issues
|
|
8
|
+
Author: Vishrut
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: 3d,addon,blender,diff,pipeline,vfx
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
|
|
21
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# BlenDiff
|
|
29
|
+
|
|
30
|
+
**Semantic diff, snapshot history, and assisted merge for Blender `.blend` files.**
|
|
31
|
+
|
|
32
|
+
BlenDiff compares two `.blend` file states using Blender's Python API — not binary diffing — and provides a full assisted merge system. It ships as both a Blender addon and a pip-installable pure-Python library for headless/CI use.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **Snapshot history** — named, timestamped snapshots stored in a human-readable `.blendiff` JSON sidecar next to your `.blend` file
|
|
39
|
+
- **Object & collection diffing** — detects added, removed, and modified objects with per-property change tracking (transform, visibility, collection membership)
|
|
40
|
+
- **Material node graph diffing** — per-node, per-socket comparison including image names, input values, and rewired links
|
|
41
|
+
- **Render settings diffing** — engine, resolution, sampling, output format, color management, Cycles and EEVEE sub-settings
|
|
42
|
+
- **Three-way merge** — conflict detection and per-property resolution (Use A / Use B / Use Base) with a Blender UI
|
|
43
|
+
- **Annotated HTML export** — self-contained dark-themed report with per-entry annotation textareas and JSON round-trip
|
|
44
|
+
- **Headless CLI** — run diffs in CI without launching Blender
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
### As a pip library (no Blender required)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install blendiff
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### As a Blender addon
|
|
57
|
+
|
|
58
|
+
Download the latest `blendiff.zip` from [Releases](https://github.com/Vishrut2403/blendiff/releases) and install via **Edit → Preferences → Add-ons → Install**.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## CLI Usage
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# List snapshots in a sidecar file
|
|
66
|
+
blendiff list scene.blendiff
|
|
67
|
+
|
|
68
|
+
# Compare two snapshots
|
|
69
|
+
blendiff compare scene.blendiff "Before rigging" "After rigging"
|
|
70
|
+
|
|
71
|
+
# Fail CI if any changes exist
|
|
72
|
+
blendiff latest scene.blendiff --fail-on-changes
|
|
73
|
+
|
|
74
|
+
# Export an HTML report
|
|
75
|
+
blendiff compare scene.blendiff "v1" "v2" --output report.html
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Python API
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from blendiff.storage.sidecar import SidecarManager
|
|
84
|
+
from blendiff.diff_engine.diff_engine import DiffEngine
|
|
85
|
+
|
|
86
|
+
mgr = SidecarManager("scene.blendiff")
|
|
87
|
+
snaps = {s.label: s for s in mgr.list_snapshots()}
|
|
88
|
+
|
|
89
|
+
engine = DiffEngine()
|
|
90
|
+
result = engine.compare(snaps["v1"].data, snaps["v2"].data)
|
|
91
|
+
|
|
92
|
+
# Render settings diff
|
|
93
|
+
print(result.render_diff.summary())
|
|
94
|
+
|
|
95
|
+
# Object diffs
|
|
96
|
+
for diff in result.object_diffs:
|
|
97
|
+
print(diff.name, diff.kind)
|
|
98
|
+
for change in diff.changes:
|
|
99
|
+
print(" ", change.property_path, change.old_value, "→", change.new_value)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Architecture
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
blendiff/
|
|
108
|
+
├── data_model/ # Dataclasses — SceneDiff, RenderDiff, MergeProposal, …
|
|
109
|
+
├── diff_engine/ # Pure comparison logic — no bpy
|
|
110
|
+
├── serializer/ # mathutils → JSON-safe types
|
|
111
|
+
├── storage/ # .blendiff sidecar CRUD
|
|
112
|
+
├── export/ # Self-contained HTML report generation
|
|
113
|
+
├── cli/ # Headless CLI + importable Python API
|
|
114
|
+
├── extractor/ # bpy readers (Blender-only)
|
|
115
|
+
└── ui/ # Blender panels and operators (Blender-only)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The extractor is the **only** bpy-touching module. Everything downstream is pure Python and fully testable without Blender.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Running Tests
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pip install blendiff[dev]
|
|
126
|
+
pytest tests/ -v
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
224+ tests, all passing without a Blender installation.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## CI Integration
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
# .github/workflows/diff.yml
|
|
137
|
+
- name: Check for scene changes
|
|
138
|
+
run: blendiff latest scene.blendiff --fail-on-changes
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
See `docs/ci_example.yml` for a full GitHub Actions workflow template.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
blendiff-0.2.0/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# BlenDiff
|
|
2
|
+
|
|
3
|
+
**Semantic diff, snapshot history, and assisted merge for Blender `.blend` files.**
|
|
4
|
+
|
|
5
|
+
BlenDiff compares two `.blend` file states using Blender's Python API — not binary diffing — and provides a full assisted merge system. It ships as both a Blender addon and a pip-installable pure-Python library for headless/CI use.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Snapshot history** — named, timestamped snapshots stored in a human-readable `.blendiff` JSON sidecar next to your `.blend` file
|
|
12
|
+
- **Object & collection diffing** — detects added, removed, and modified objects with per-property change tracking (transform, visibility, collection membership)
|
|
13
|
+
- **Material node graph diffing** — per-node, per-socket comparison including image names, input values, and rewired links
|
|
14
|
+
- **Render settings diffing** — engine, resolution, sampling, output format, color management, Cycles and EEVEE sub-settings
|
|
15
|
+
- **Three-way merge** — conflict detection and per-property resolution (Use A / Use B / Use Base) with a Blender UI
|
|
16
|
+
- **Annotated HTML export** — self-contained dark-themed report with per-entry annotation textareas and JSON round-trip
|
|
17
|
+
- **Headless CLI** — run diffs in CI without launching Blender
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### As a pip library (no Blender required)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install blendiff
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### As a Blender addon
|
|
30
|
+
|
|
31
|
+
Download the latest `blendiff.zip` from [Releases](https://github.com/Vishrut2403/blendiff/releases) and install via **Edit → Preferences → Add-ons → Install**.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## CLI Usage
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# List snapshots in a sidecar file
|
|
39
|
+
blendiff list scene.blendiff
|
|
40
|
+
|
|
41
|
+
# Compare two snapshots
|
|
42
|
+
blendiff compare scene.blendiff "Before rigging" "After rigging"
|
|
43
|
+
|
|
44
|
+
# Fail CI if any changes exist
|
|
45
|
+
blendiff latest scene.blendiff --fail-on-changes
|
|
46
|
+
|
|
47
|
+
# Export an HTML report
|
|
48
|
+
blendiff compare scene.blendiff "v1" "v2" --output report.html
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Python API
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from blendiff.storage.sidecar import SidecarManager
|
|
57
|
+
from blendiff.diff_engine.diff_engine import DiffEngine
|
|
58
|
+
|
|
59
|
+
mgr = SidecarManager("scene.blendiff")
|
|
60
|
+
snaps = {s.label: s for s in mgr.list_snapshots()}
|
|
61
|
+
|
|
62
|
+
engine = DiffEngine()
|
|
63
|
+
result = engine.compare(snaps["v1"].data, snaps["v2"].data)
|
|
64
|
+
|
|
65
|
+
# Render settings diff
|
|
66
|
+
print(result.render_diff.summary())
|
|
67
|
+
|
|
68
|
+
# Object diffs
|
|
69
|
+
for diff in result.object_diffs:
|
|
70
|
+
print(diff.name, diff.kind)
|
|
71
|
+
for change in diff.changes:
|
|
72
|
+
print(" ", change.property_path, change.old_value, "→", change.new_value)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Architecture
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
blendiff/
|
|
81
|
+
├── data_model/ # Dataclasses — SceneDiff, RenderDiff, MergeProposal, …
|
|
82
|
+
├── diff_engine/ # Pure comparison logic — no bpy
|
|
83
|
+
├── serializer/ # mathutils → JSON-safe types
|
|
84
|
+
├── storage/ # .blendiff sidecar CRUD
|
|
85
|
+
├── export/ # Self-contained HTML report generation
|
|
86
|
+
├── cli/ # Headless CLI + importable Python API
|
|
87
|
+
├── extractor/ # bpy readers (Blender-only)
|
|
88
|
+
└── ui/ # Blender panels and operators (Blender-only)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The extractor is the **only** bpy-touching module. Everything downstream is pure Python and fully testable without Blender.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Running Tests
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install blendiff[dev]
|
|
99
|
+
pytest tests/ -v
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
224+ tests, all passing without a Blender installation.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## CI Integration
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
# .github/workflows/diff.yml
|
|
110
|
+
- name: Check for scene changes
|
|
111
|
+
run: blendiff latest scene.blendiff --fail-on-changes
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
See `docs/ci_example.yml` for a full GitHub Actions workflow template.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BlenDiff — semantic diff, snapshot history, and assisted merge for .blend files.
|
|
3
|
+
|
|
4
|
+
When imported outside Blender (e.g. via pip), only the pure-Python core is
|
|
5
|
+
available: data_model, diff_engine, serializer, storage, export, cli.
|
|
6
|
+
|
|
7
|
+
When imported inside Blender, the full addon registers its UI panels and
|
|
8
|
+
operators on top of the core.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.2.0"
|
|
12
|
+
|
|
13
|
+
bl_info = {
|
|
14
|
+
"name": "BlenDiff",
|
|
15
|
+
"author": "Vishrut",
|
|
16
|
+
"version": (0, 2, 0),
|
|
17
|
+
"blender": (3, 6, 0),
|
|
18
|
+
"location": "3D Viewport > Sidebar > BlenDiff",
|
|
19
|
+
"description": "Semantic scene diff, snapshot history, and assisted merge for .blend files",
|
|
20
|
+
"category": "Scene",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import bpy
|
|
25
|
+
_IN_BLENDER = True
|
|
26
|
+
except ModuleNotFoundError:
|
|
27
|
+
_IN_BLENDER = False
|
|
28
|
+
|
|
29
|
+
if _IN_BLENDER:
|
|
30
|
+
from .ui import panels, operators, merge_panel
|
|
31
|
+
|
|
32
|
+
def register() -> None:
|
|
33
|
+
operators.register()
|
|
34
|
+
panels.register()
|
|
35
|
+
merge_panel.register()
|
|
36
|
+
|
|
37
|
+
def unregister() -> None:
|
|
38
|
+
merge_panel.unregister()
|
|
39
|
+
panels.unregister()
|
|
40
|
+
operators.unregister()
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
blendiff.cli.__main__
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Entry point for: python -m blendiff.cli <command> [args]
|
|
5
|
+
|
|
6
|
+
Commands
|
|
7
|
+
--------
|
|
8
|
+
list <sidecar> List all snapshots in a sidecar
|
|
9
|
+
compare <sidecar> <label_a> <label_b> Diff two snapshots by label
|
|
10
|
+
latest <sidecar> Diff the two most recent snapshots
|
|
11
|
+
|
|
12
|
+
Global flags
|
|
13
|
+
------------
|
|
14
|
+
--output <path> Write HTML report to this path (default: print JSON)
|
|
15
|
+
--json Print result as JSON to stdout
|
|
16
|
+
--fail-on-changes Exit with code 1 if any changes detected (for CI gates)
|
|
17
|
+
--quiet Suppress all output except errors
|
|
18
|
+
|
|
19
|
+
Exit codes
|
|
20
|
+
----------
|
|
21
|
+
0 — success, no changes (or changes detected but --fail-on-changes not set)
|
|
22
|
+
1 — changes detected and --fail-on-changes is set
|
|
23
|
+
2 — error (file not found, snapshot not found, parse failure, etc.)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
35
|
+
parser = argparse.ArgumentParser(
|
|
36
|
+
prog="python -m blendiff.cli",
|
|
37
|
+
description="BlenDiff headless CLI — diff Blender scene snapshots without opening Blender.",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
sub = parser.add_subparsers(dest="command", metavar="COMMAND")
|
|
41
|
+
sub.required = True
|
|
42
|
+
|
|
43
|
+
# ── list ──────────────────────────────────────────────────────────────
|
|
44
|
+
p_list = sub.add_parser("list", help="List all snapshots in a .blendiff sidecar")
|
|
45
|
+
p_list.add_argument("sidecar", help="Path to the .blendiff file")
|
|
46
|
+
p_list.add_argument("--json", dest="as_json", action="store_true",
|
|
47
|
+
help="Output as JSON")
|
|
48
|
+
|
|
49
|
+
# ── compare ───────────────────────────────────────────────────────────
|
|
50
|
+
p_compare = sub.add_parser(
|
|
51
|
+
"compare",
|
|
52
|
+
help="Diff two snapshots by label (most recent match used if duplicates exist)",
|
|
53
|
+
)
|
|
54
|
+
p_compare.add_argument("sidecar", help="Path to the .blendiff file")
|
|
55
|
+
p_compare.add_argument("label_a", help="Label of the base snapshot (before)")
|
|
56
|
+
p_compare.add_argument("label_b", help="Label of the target snapshot (after)")
|
|
57
|
+
p_compare.add_argument("--output", metavar="PATH",
|
|
58
|
+
help="Write HTML report to this path")
|
|
59
|
+
p_compare.add_argument("--json", dest="as_json", action="store_true",
|
|
60
|
+
help="Print result as JSON to stdout")
|
|
61
|
+
p_compare.add_argument("--fail-on-changes", action="store_true",
|
|
62
|
+
help="Exit with code 1 if changes are detected")
|
|
63
|
+
p_compare.add_argument("--quiet", action="store_true",
|
|
64
|
+
help="Suppress output except errors")
|
|
65
|
+
|
|
66
|
+
# ── latest ────────────────────────────────────────────────────────────
|
|
67
|
+
p_latest = sub.add_parser(
|
|
68
|
+
"latest",
|
|
69
|
+
help="Diff the two most recent snapshots",
|
|
70
|
+
)
|
|
71
|
+
p_latest.add_argument("sidecar", help="Path to the .blendiff file")
|
|
72
|
+
p_latest.add_argument("--output", metavar="PATH",
|
|
73
|
+
help="Write HTML report to this path")
|
|
74
|
+
p_latest.add_argument("--json", dest="as_json", action="store_true",
|
|
75
|
+
help="Print result as JSON to stdout")
|
|
76
|
+
p_latest.add_argument("--fail-on-changes", action="store_true",
|
|
77
|
+
help="Exit with code 1 if changes are detected")
|
|
78
|
+
p_latest.add_argument("--quiet", action="store_true",
|
|
79
|
+
help="Suppress output except errors")
|
|
80
|
+
|
|
81
|
+
return parser
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _cmd_list(args) -> int:
|
|
85
|
+
from .api import list_snapshots
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
snapshots = list_snapshots(args.sidecar)
|
|
89
|
+
except FileNotFoundError as e:
|
|
90
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
91
|
+
return 2
|
|
92
|
+
|
|
93
|
+
if args.as_json:
|
|
94
|
+
print(json.dumps(snapshots, indent=2))
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
if not snapshots:
|
|
98
|
+
print("No snapshots found.")
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
print(f"{'#':<4} {'Label':<30} {'Timestamp':<22} {'ID'}")
|
|
102
|
+
print("-" * 80)
|
|
103
|
+
for i, s in enumerate(snapshots, 1):
|
|
104
|
+
print(f"{i:<4} {s['label']:<30} {s['timestamp_display']:<22} {s['id'][:8]}")
|
|
105
|
+
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _cmd_compare(args, use_latest: bool = False) -> int:
|
|
110
|
+
from .api import compare_snapshots_by_label, compare_latest_two
|
|
111
|
+
from ..export.html_exporter import export_to_file, build_output_path
|
|
112
|
+
|
|
113
|
+
quiet = getattr(args, "quiet", False)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
if use_latest:
|
|
117
|
+
result = compare_latest_two(args.sidecar)
|
|
118
|
+
else:
|
|
119
|
+
result = compare_snapshots_by_label(
|
|
120
|
+
args.sidecar, args.label_a, args.label_b
|
|
121
|
+
)
|
|
122
|
+
except FileNotFoundError as e:
|
|
123
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
124
|
+
return 2
|
|
125
|
+
except ValueError as e:
|
|
126
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
127
|
+
return 2
|
|
128
|
+
|
|
129
|
+
# JSON output
|
|
130
|
+
if getattr(args, "as_json", False):
|
|
131
|
+
print(json.dumps(result, indent=2, default=str))
|
|
132
|
+
|
|
133
|
+
# HTML output
|
|
134
|
+
elif getattr(args, "output", None):
|
|
135
|
+
output_path = args.output
|
|
136
|
+
snapshot_label = (
|
|
137
|
+
f"{result['snapshot_a']['label']} → {result['snapshot_b']['label']}"
|
|
138
|
+
)
|
|
139
|
+
export_to_file(
|
|
140
|
+
result=result,
|
|
141
|
+
snapshot_label=snapshot_label,
|
|
142
|
+
blend_filepath=args.sidecar,
|
|
143
|
+
output_path=output_path,
|
|
144
|
+
)
|
|
145
|
+
if not quiet:
|
|
146
|
+
print(f"HTML report written to: {output_path}")
|
|
147
|
+
|
|
148
|
+
# Default: human-readable summary
|
|
149
|
+
else:
|
|
150
|
+
if not quiet:
|
|
151
|
+
snap_a = result["snapshot_a"]
|
|
152
|
+
snap_b = result["snapshot_b"]
|
|
153
|
+
print(f"\nBlenDiff — comparing snapshots")
|
|
154
|
+
print(f" Before : '{snap_a['label']}' ({snap_a['timestamp']})")
|
|
155
|
+
print(f" After : '{snap_b['label']}' ({snap_b['timestamp']})")
|
|
156
|
+
print(f"\n {result['summary']}\n")
|
|
157
|
+
|
|
158
|
+
added = result["added_objects"]
|
|
159
|
+
removed = result["removed_objects"]
|
|
160
|
+
modified = result["modified_objects"]
|
|
161
|
+
col_diffs = result["collection_diffs"]
|
|
162
|
+
|
|
163
|
+
if added:
|
|
164
|
+
print(f" Added objects ({len(added)}):")
|
|
165
|
+
for name in added:
|
|
166
|
+
print(f" + {name}")
|
|
167
|
+
|
|
168
|
+
if removed:
|
|
169
|
+
print(f" Removed objects ({len(removed)}):")
|
|
170
|
+
for name in removed:
|
|
171
|
+
print(f" - {name}")
|
|
172
|
+
|
|
173
|
+
if modified:
|
|
174
|
+
print(f" Modified objects ({len(modified)}):")
|
|
175
|
+
for obj in modified:
|
|
176
|
+
print(f" ~ {obj['name']}")
|
|
177
|
+
for c in obj["changes"]:
|
|
178
|
+
print(f" {c['property_path']}")
|
|
179
|
+
print(f" {c['old_value']} → {c['new_value']}")
|
|
180
|
+
|
|
181
|
+
if col_diffs:
|
|
182
|
+
print(f" Collection changes ({len(col_diffs)}):")
|
|
183
|
+
for cd in col_diffs:
|
|
184
|
+
print(f" {cd['kind'].capitalize()}: {cd['path']}")
|
|
185
|
+
|
|
186
|
+
if not result["has_changes"]:
|
|
187
|
+
print(" No changes detected.")
|
|
188
|
+
|
|
189
|
+
print()
|
|
190
|
+
|
|
191
|
+
# Exit code
|
|
192
|
+
if getattr(args, "fail_on_changes", False) and result["has_changes"]:
|
|
193
|
+
return 1
|
|
194
|
+
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def main(argv=None) -> int:
|
|
199
|
+
parser = _build_parser()
|
|
200
|
+
args = parser.parse_args(argv)
|
|
201
|
+
|
|
202
|
+
if args.command == "list":
|
|
203
|
+
return _cmd_list(args)
|
|
204
|
+
elif args.command == "compare":
|
|
205
|
+
return _cmd_compare(args, use_latest=False)
|
|
206
|
+
elif args.command == "latest":
|
|
207
|
+
return _cmd_compare(args, use_latest=True)
|
|
208
|
+
|
|
209
|
+
parser.print_help()
|
|
210
|
+
return 2
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
sys.exit(main())
|