notso-glb 0.1.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.
Files changed (35) hide show
  1. notso_glb-0.1.0/PKG-INFO +150 -0
  2. notso_glb-0.1.0/README.md +124 -0
  3. notso_glb-0.1.0/pyproject.toml +90 -0
  4. notso_glb-0.1.0/src/notso_glb/__init__.py +38 -0
  5. notso_glb-0.1.0/src/notso_glb/__main__.py +6 -0
  6. notso_glb-0.1.0/src/notso_glb/analyzers/__init__.py +20 -0
  7. notso_glb-0.1.0/src/notso_glb/analyzers/bloat.py +117 -0
  8. notso_glb-0.1.0/src/notso_glb/analyzers/bones.py +100 -0
  9. notso_glb-0.1.0/src/notso_glb/analyzers/duplicates.py +71 -0
  10. notso_glb-0.1.0/src/notso_glb/analyzers/skinned_mesh.py +47 -0
  11. notso_glb-0.1.0/src/notso_glb/analyzers/uv_maps.py +59 -0
  12. notso_glb-0.1.0/src/notso_glb/cleaners/__init__.py +23 -0
  13. notso_glb-0.1.0/src/notso_glb/cleaners/bones.py +49 -0
  14. notso_glb-0.1.0/src/notso_glb/cleaners/duplicates.py +110 -0
  15. notso_glb-0.1.0/src/notso_glb/cleaners/mesh.py +183 -0
  16. notso_glb-0.1.0/src/notso_glb/cleaners/textures.py +116 -0
  17. notso_glb-0.1.0/src/notso_glb/cleaners/uv_maps.py +29 -0
  18. notso_glb-0.1.0/src/notso_glb/cleaners/vertex_groups.py +34 -0
  19. notso_glb-0.1.0/src/notso_glb/cli.py +330 -0
  20. notso_glb-0.1.0/src/notso_glb/exporters/__init__.py +8 -0
  21. notso_glb-0.1.0/src/notso_glb/exporters/gltf.py +647 -0
  22. notso_glb-0.1.0/src/notso_glb/utils/__init__.py +20 -0
  23. notso_glb-0.1.0/src/notso_glb/utils/blender.py +49 -0
  24. notso_glb-0.1.0/src/notso_glb/utils/constants.py +41 -0
  25. notso_glb-0.1.0/src/notso_glb/utils/gltfpack.py +273 -0
  26. notso_glb-0.1.0/src/notso_glb/utils/logging.py +421 -0
  27. notso_glb-0.1.0/src/notso_glb/utils/naming.py +24 -0
  28. notso_glb-0.1.0/src/notso_glb/wasm/__init__.py +32 -0
  29. notso_glb-0.1.0/src/notso_glb/wasm/constants.py +8 -0
  30. notso_glb-0.1.0/src/notso_glb/wasm/gltfpack.version +1 -0
  31. notso_glb-0.1.0/src/notso_glb/wasm/gltfpack.wasm +0 -0
  32. notso_glb-0.1.0/src/notso_glb/wasm/py.typed +0 -0
  33. notso_glb-0.1.0/src/notso_glb/wasm/runner.py +137 -0
  34. notso_glb-0.1.0/src/notso_glb/wasm/runtime.py +244 -0
  35. notso_glb-0.1.0/src/notso_glb/wasm/wasi.py +347 -0
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.3
2
+ Name: notso-glb
3
+ Version: 0.1.0
4
+ Summary: GLB Export Optimizer for Mascot Models
5
+ Keywords: 3d,blender,compression,draco,glb,gltf,mascot,optimization,webgl
6
+ Author: Kaj Kowalski
7
+ Author-email: Kaj Kowalski <info@kajkowalski.nl>
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
16
+ Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
17
+ Requires-Dist: bpy>=5,<6 ; python_full_version == '3.11.*'
18
+ Requires-Dist: rich>=14.2.0
19
+ Requires-Dist: typer>=0.20.1
20
+ Requires-Dist: wasmtime>=41.0.0
21
+ Requires-Python: ~=3.11.0
22
+ Project-URL: Homepage, https://github.com/kjanat/notso-glb
23
+ Project-URL: Issues, https://github.com/kjanat/notso-glb/issues
24
+ Project-URL: Repository, https://github.com/kjanat/notso-glb.git
25
+ Description-Content-Type: text/markdown
26
+
27
+ # GLB Export Optimizer for Mascot Models
28
+
29
+ > Cleans up Blender files and exports optimized GLB for web delivery.
30
+
31
+ ```bash
32
+ uvx notso-glb [OPTIONS] FILE
33
+ ```
34
+
35
+ <p align="center">
36
+ <a href="https://github.com/kjanat/notso-glb/blob/master/CLI.md" target="_blank" rel="noopener" title="View CLI Options">
37
+ <img alt="Screenshot with cli options" width="100%" src="https://raw.githubusercontent.com/kjanat/notso-glb/refs/heads/master/screenshot.webp">
38
+ </a>
39
+ </p>
40
+
41
+ <p align="center">
42
+ <a href="https://pypi.org/project/notso-glb/"><img src="https://img.shields.io/pypi/v/notso-glb" alt="PyPI"></a>
43
+ <a href="https://pypi.org/project/notso-glb/"><img src="https://img.shields.io/pypi/dm/notso-glb" alt="Downloads"></a> <!--<a href="https://github.com/kjanat/notso-glb/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/kjanat/notso-glb/ci.yml?branch=master" alt="CI"></a>-->
44
+ <a href="https://github.com/kjanat/notso-glb/blob/master/LICENSE"><img src="https://img.shields.io/github/license/kjanat/notso-glb" alt="License"></a> <!--<a href="https://notso-glb.kjanat.com"><img src="https://img.shields.io/badge/docs-mkdocs-blue" alt="Docs"></a>-->
45
+ <img src="https://img.shields.io/badge/python-3.11-blue" alt="Python 3.11">
46
+ </p>
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ uv tool install notso-glb
52
+ ```
53
+
54
+ ```bash
55
+ # Or install directly from GitHub:
56
+ uv tool install -p3.11 git+https://github.com/kjanat/notso-glb
57
+ ```
58
+
59
+ then just run `notso-glb` from the command line.
60
+
61
+ ### Upgrade
62
+
63
+ ```bash
64
+ uv tool upgrade notso-glb
65
+ ```
66
+
67
+ ## Features
68
+
69
+ Optimizations:
70
+
71
+ - Detects bloated props (high-vert non-skinned meshes, repetitive geometry)
72
+ - Detects skinned meshes with non-root parents (glTF spec issue)
73
+ - Detects unused UV maps (`TEXCOORD` bloat)
74
+ - Detects duplicate names and sanitization collisions
75
+ - Removes unused vertex groups (bone weight bloat)
76
+ - Marks static bones as non-deform (animation bloat)
77
+ - Removes bone shape objects (Icosphere artifacts)
78
+ - Resizes textures to max 1024px (optional `POT` enforcement)
79
+ - Exports with Draco mesh compression
80
+ - Exports with WebP textures
81
+
82
+ Bloat Detection:
83
+
84
+ - CRITICAL: Props >2000 verts, repetitive detail (many islands with high verts)
85
+ - WARNING: Props >1000 verts, scene total >15000 verts, non-root skinned meshes
86
+
87
+ Experimental Auto-fix (`--autofix`):
88
+
89
+ - BMesh cleanup (remove doubles, degenerate geometry, loose verts)
90
+ - Decimate bloated props to ~1600 verts
91
+ - Auto-rename duplicate objects/meshes/materials/actions (using pointer ID)
92
+ - Remove unused UV maps
93
+
94
+ ## Usage
95
+
96
+ See [CLI.md]
97
+
98
+ ## Requirements
99
+
100
+ - Blender 5.0+
101
+ - Python 3.11 (same as bundled with Blender)
102
+ - uv (optional, for easy install/upgrade)
103
+ - gltfpack (optional, for extra compression - WASM fallback included)
104
+
105
+ ## Development Setup
106
+
107
+ For local development, download the gltfpack WASM binary:
108
+
109
+ ```bash
110
+ # Download latest WASM from npm
111
+ uv run scripts/update_wasm.py
112
+
113
+ # Or check current version
114
+ uv run scripts/update_wasm.py --show-version
115
+
116
+ # Download specific version
117
+ uv run scripts/update_wasm.py --version 1.0.0
118
+ ```
119
+
120
+ The WASM binary (`src/notso_glb/wasm/gltfpack.wasm`) is not committed to git and
121
+ must be downloaded locally. CI/CD pipelines handle this automatically.
122
+
123
+ ## Useful Links
124
+
125
+ - [glTF 2.0 Specification]
126
+ - [glTF 2.0 API Reference Guide]
127
+ - [Khronos Resources]
128
+ - [Blender 5.0 glTF 2.0]
129
+ - [Blender 5.0 Python API Documentation]
130
+ - [Blender 5.0 Reference Manual]
131
+
132
+ ## License
133
+
134
+ This project is licensed under the GNU General Public License v3.0 - see the
135
+ [LICENSE] file for details.
136
+
137
+ This project uses [Blender] as a Python module (bpy), which is also GPL-3.0
138
+ licensed.
139
+
140
+ [Khronos Resources]: https://github.khronos.org/
141
+ [glTF 2.0 Specification]: https://www.khronos.org/gltf/#gltf-spec
142
+ [glTF 2.0 API Reference Guide]: https://www.khronos.org/files/gltf20-reference-guide.pdf
143
+ [Blender 5.0 Reference Manual]: https://docs.blender.org/manual/en/latest
144
+ [Blender 5.0 glTF 2.0]: https://docs.blender.org/manual/en/5.0/addons/import_export/scene_gltf2.html
145
+ [Blender 5.0 Python API Documentation]: https://docs.blender.org/api/current/index.html
146
+ [Blender]: https://www.blender.org/
147
+ [CLI.md]: https://github.com/kjanat/notso-glb/blob/master/CLI.md
148
+ [LICENSE]: https://github.com/kjanat/notso-glb/blob/master/LICENSE
149
+
150
+ <!-- markdownlint-disable-file MD033 -->
@@ -0,0 +1,124 @@
1
+ # GLB Export Optimizer for Mascot Models
2
+
3
+ > Cleans up Blender files and exports optimized GLB for web delivery.
4
+
5
+ ```bash
6
+ uvx notso-glb [OPTIONS] FILE
7
+ ```
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/kjanat/notso-glb/blob/master/CLI.md" target="_blank" rel="noopener" title="View CLI Options">
11
+ <img alt="Screenshot with cli options" width="100%" src="https://raw.githubusercontent.com/kjanat/notso-glb/refs/heads/master/screenshot.webp">
12
+ </a>
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="https://pypi.org/project/notso-glb/"><img src="https://img.shields.io/pypi/v/notso-glb" alt="PyPI"></a>
17
+ <a href="https://pypi.org/project/notso-glb/"><img src="https://img.shields.io/pypi/dm/notso-glb" alt="Downloads"></a> <!--<a href="https://github.com/kjanat/notso-glb/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/kjanat/notso-glb/ci.yml?branch=master" alt="CI"></a>-->
18
+ <a href="https://github.com/kjanat/notso-glb/blob/master/LICENSE"><img src="https://img.shields.io/github/license/kjanat/notso-glb" alt="License"></a> <!--<a href="https://notso-glb.kjanat.com"><img src="https://img.shields.io/badge/docs-mkdocs-blue" alt="Docs"></a>-->
19
+ <img src="https://img.shields.io/badge/python-3.11-blue" alt="Python 3.11">
20
+ </p>
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ uv tool install notso-glb
26
+ ```
27
+
28
+ ```bash
29
+ # Or install directly from GitHub:
30
+ uv tool install -p3.11 git+https://github.com/kjanat/notso-glb
31
+ ```
32
+
33
+ then just run `notso-glb` from the command line.
34
+
35
+ ### Upgrade
36
+
37
+ ```bash
38
+ uv tool upgrade notso-glb
39
+ ```
40
+
41
+ ## Features
42
+
43
+ Optimizations:
44
+
45
+ - Detects bloated props (high-vert non-skinned meshes, repetitive geometry)
46
+ - Detects skinned meshes with non-root parents (glTF spec issue)
47
+ - Detects unused UV maps (`TEXCOORD` bloat)
48
+ - Detects duplicate names and sanitization collisions
49
+ - Removes unused vertex groups (bone weight bloat)
50
+ - Marks static bones as non-deform (animation bloat)
51
+ - Removes bone shape objects (Icosphere artifacts)
52
+ - Resizes textures to max 1024px (optional `POT` enforcement)
53
+ - Exports with Draco mesh compression
54
+ - Exports with WebP textures
55
+
56
+ Bloat Detection:
57
+
58
+ - CRITICAL: Props >2000 verts, repetitive detail (many islands with high verts)
59
+ - WARNING: Props >1000 verts, scene total >15000 verts, non-root skinned meshes
60
+
61
+ Experimental Auto-fix (`--autofix`):
62
+
63
+ - BMesh cleanup (remove doubles, degenerate geometry, loose verts)
64
+ - Decimate bloated props to ~1600 verts
65
+ - Auto-rename duplicate objects/meshes/materials/actions (using pointer ID)
66
+ - Remove unused UV maps
67
+
68
+ ## Usage
69
+
70
+ See [CLI.md]
71
+
72
+ ## Requirements
73
+
74
+ - Blender 5.0+
75
+ - Python 3.11 (same as bundled with Blender)
76
+ - uv (optional, for easy install/upgrade)
77
+ - gltfpack (optional, for extra compression - WASM fallback included)
78
+
79
+ ## Development Setup
80
+
81
+ For local development, download the gltfpack WASM binary:
82
+
83
+ ```bash
84
+ # Download latest WASM from npm
85
+ uv run scripts/update_wasm.py
86
+
87
+ # Or check current version
88
+ uv run scripts/update_wasm.py --show-version
89
+
90
+ # Download specific version
91
+ uv run scripts/update_wasm.py --version 1.0.0
92
+ ```
93
+
94
+ The WASM binary (`src/notso_glb/wasm/gltfpack.wasm`) is not committed to git and
95
+ must be downloaded locally. CI/CD pipelines handle this automatically.
96
+
97
+ ## Useful Links
98
+
99
+ - [glTF 2.0 Specification]
100
+ - [glTF 2.0 API Reference Guide]
101
+ - [Khronos Resources]
102
+ - [Blender 5.0 glTF 2.0]
103
+ - [Blender 5.0 Python API Documentation]
104
+ - [Blender 5.0 Reference Manual]
105
+
106
+ ## License
107
+
108
+ This project is licensed under the GNU General Public License v3.0 - see the
109
+ [LICENSE] file for details.
110
+
111
+ This project uses [Blender] as a Python module (bpy), which is also GPL-3.0
112
+ licensed.
113
+
114
+ [Khronos Resources]: https://github.khronos.org/
115
+ [glTF 2.0 Specification]: https://www.khronos.org/gltf/#gltf-spec
116
+ [glTF 2.0 API Reference Guide]: https://www.khronos.org/files/gltf20-reference-guide.pdf
117
+ [Blender 5.0 Reference Manual]: https://docs.blender.org/manual/en/latest
118
+ [Blender 5.0 glTF 2.0]: https://docs.blender.org/manual/en/5.0/addons/import_export/scene_gltf2.html
119
+ [Blender 5.0 Python API Documentation]: https://docs.blender.org/api/current/index.html
120
+ [Blender]: https://www.blender.org/
121
+ [CLI.md]: https://github.com/kjanat/notso-glb/blob/master/CLI.md
122
+ [LICENSE]: https://github.com/kjanat/notso-glb/blob/master/LICENSE
123
+
124
+ <!-- markdownlint-disable-file MD033 -->
@@ -0,0 +1,90 @@
1
+ [project]
2
+ name = "notso-glb"
3
+ version = "0.1.0"
4
+ description = "GLB Export Optimizer for Mascot Models"
5
+ readme = "README.md"
6
+ requires-python = "~=3.11.0"
7
+ authors = [{ name = "Kaj Kowalski", email = "info@kajkowalski.nl" }]
8
+ keywords = ["3d", "blender", "compression", "draco", "glb", "gltf", "mascot", "optimization", "webgl"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Environment :: Console",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Topic :: Multimedia :: Graphics :: 3D Modeling",
18
+ "Topic :: Multimedia :: Graphics :: Graphics Conversion"
19
+ ]
20
+ dependencies = [
21
+ "bpy>=5,<6; python_version ~= '3.11.0'",
22
+ "rich>=14.2.0",
23
+ "typer>=0.20.1",
24
+ "wasmtime>=41.0.0",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/kjanat/notso-glb"
29
+ Issues = "https://github.com/kjanat/notso-glb/issues"
30
+ Repository = "https://github.com/kjanat/notso-glb.git"
31
+
32
+ [project.scripts]
33
+ notso-glb = "notso_glb:main"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ { include-group = "lint" },
38
+ { include-group = "test" },
39
+ { include-group = "types" },
40
+ ]
41
+ lint = [
42
+ "ruff>=0.14.10",
43
+ "tombi>=0.7.26",
44
+ "ty>=0.0.5",
45
+ ]
46
+ test = [
47
+ "pytest>=9.0.2",
48
+ "pytest-cov>=7.0.0",
49
+ ]
50
+ types = [
51
+ "fake-bpy-module>=20251003",
52
+ "ty>=0.0.5",
53
+ ]
54
+
55
+ [build-system]
56
+ requires = ["uv_build>=0.9.18,<0.10.0"]
57
+ build-backend = "uv_build"
58
+
59
+ [tool.pytest]
60
+ addopts = ["--cov", "--import-mode=importlib", "-ra"]
61
+ minversion = "9.0"
62
+ strict = true
63
+ testpaths = ["tests"]
64
+
65
+ [tool.ruff]
66
+ line-length = 88
67
+ indent-width = 4
68
+ target-version = "py311"
69
+
70
+ [tool.ruff.lint]
71
+ preview = true
72
+ select = ["E4", "E7", "E9", "F", "F", "B", "UP"]
73
+ ignore = []
74
+ fixable = ["ALL"]
75
+ unfixable = []
76
+
77
+ [tool.ruff.format]
78
+ preview = true
79
+ quote-style = "double"
80
+ indent-style = "space"
81
+ skip-magic-trailing-comma = false
82
+ line-ending = "lf"
83
+ docstring-code-format = true
84
+ docstring-code-line-length = "dynamic"
85
+
86
+ [tool.uv]
87
+ default-groups = "all"
88
+
89
+ [tool.uv.build-backend]
90
+ source-include = ["src/notso_glb/wasm/**"]
@@ -0,0 +1,38 @@
1
+ """
2
+ GLB Export Optimizer for Mascot Models
3
+ ======================================
4
+ Cleans up Blender files and exports optimized GLB for web delivery.
5
+
6
+ Optimizations:
7
+ - Detects bloated props (high-vert non-skinned meshes, repetitive geometry)
8
+ - Detects skinned meshes with non-root parents (glTF spec issue)
9
+ - Detects unused UV maps (TEXCOORD bloat)
10
+ - Detects duplicate names and sanitization collisions
11
+ - Removes unused vertex groups (bone weight bloat)
12
+ - Marks static bones as non-deform (animation bloat)
13
+ - Removes bone shape objects (Icosphere artifacts)
14
+ - Resizes textures to max 1024px (optional POT enforcement)
15
+ - Exports with Draco mesh compression
16
+ - Exports with WebP textures
17
+
18
+ Usage:
19
+ CLI:
20
+ notso-glb model.glb -o output.glb
21
+ notso-glb model.blend --format gltf-embedded
22
+ notso-glb model.gltf --no-draco --max-texture 2048
23
+
24
+ Python:
25
+ from notso_glb import main
26
+ main()
27
+ """
28
+
29
+ from importlib.metadata import PackageNotFoundError, version
30
+
31
+ from notso_glb.cli import main
32
+
33
+ try:
34
+ __version__ = version("notso-glb")
35
+ except PackageNotFoundError:
36
+ __version__ = "unknown"
37
+
38
+ __all__ = ["main"]
@@ -0,0 +1,6 @@
1
+ """Allow running as python -m notso_glb."""
2
+
3
+ from notso_glb.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,20 @@
1
+ """Analyzers for mesh bloat, bones, duplicates, and UV maps."""
2
+
3
+ from notso_glb.analyzers.bloat import analyze_mesh_bloat, count_mesh_islands
4
+ from notso_glb.analyzers.bones import (
5
+ analyze_bone_animation,
6
+ get_bones_used_for_skinning,
7
+ )
8
+ from notso_glb.analyzers.duplicates import analyze_duplicate_names
9
+ from notso_glb.analyzers.skinned_mesh import analyze_skinned_mesh_parents
10
+ from notso_glb.analyzers.uv_maps import analyze_unused_uv_maps
11
+
12
+ __all__ = [
13
+ "analyze_bone_animation",
14
+ "analyze_duplicate_names",
15
+ "analyze_mesh_bloat",
16
+ "analyze_skinned_mesh_parents",
17
+ "analyze_unused_uv_maps",
18
+ "count_mesh_islands",
19
+ "get_bones_used_for_skinning",
20
+ ]
@@ -0,0 +1,117 @@
1
+ """Mesh bloat analysis for detecting overly complex geometry."""
2
+
3
+ import bpy
4
+
5
+ from notso_glb.utils import get_mesh_data
6
+ from notso_glb.utils.constants import BLOAT_THRESHOLDS
7
+
8
+
9
+ def count_mesh_islands(obj) -> int:
10
+ """Count disconnected mesh parts (islands) using BFS."""
11
+ import bmesh
12
+
13
+ bm = bmesh.new()
14
+ bm.from_mesh(obj.data)
15
+ bm.verts.ensure_lookup_table()
16
+
17
+ visited: set[int] = set()
18
+ islands = 0
19
+
20
+ for v in bm.verts:
21
+ if v.index in visited:
22
+ continue
23
+ islands += 1
24
+ stack = [v]
25
+ while stack:
26
+ current = stack.pop()
27
+ if current.index in visited:
28
+ continue
29
+ visited.add(current.index)
30
+ for edge in current.link_edges:
31
+ other = edge.other_vert(current)
32
+ if other.index not in visited:
33
+ stack.append(other)
34
+
35
+ bm.free()
36
+ return islands
37
+
38
+
39
+ def analyze_mesh_bloat() -> list[dict[str, object]]:
40
+ """
41
+ Detect unreasonably complex meshes for web delivery.
42
+
43
+ Returns list of warnings with severity levels:
44
+ - CRITICAL: Must fix before web deployment
45
+ - WARNING: Should review, likely bloated
46
+ - INFO: Notable but may be intentional
47
+ """
48
+ warnings: list[dict[str, object]] = []
49
+
50
+ total_verts = 0
51
+ for obj in bpy.data.objects:
52
+ if obj.type != "MESH":
53
+ continue
54
+
55
+ mesh = get_mesh_data(obj)
56
+ verts = len(mesh.vertices)
57
+ total_verts += verts
58
+
59
+ if verts < 100:
60
+ continue
61
+
62
+ # Check if skinned (character mesh vs prop)
63
+ is_skinned = any(mod.type == "ARMATURE" for mod in obj.modifiers)
64
+
65
+ # Count islands for non-skinned meshes (expensive operation)
66
+ islands = 1
67
+ if not is_skinned and verts < 20000:
68
+ islands = count_mesh_islands(obj)
69
+
70
+ verts_per_island = verts / max(islands, 1)
71
+
72
+ # Bloat detection rules
73
+ if not is_skinned:
74
+ if verts > BLOAT_THRESHOLDS["prop_critical"]:
75
+ warnings.append({
76
+ "severity": "CRITICAL",
77
+ "object": obj.name,
78
+ "issue": "BLOATED_PROP",
79
+ "detail": f"{verts:,} verts (limit: {BLOAT_THRESHOLDS['prop_critical']:,})",
80
+ "suggestion": "Decimate or replace with baked texture",
81
+ })
82
+ elif verts > BLOAT_THRESHOLDS["prop_warning"]:
83
+ warnings.append({
84
+ "severity": "WARNING",
85
+ "object": obj.name,
86
+ "issue": "HIGH_VERT_PROP",
87
+ "detail": f"{verts:,} verts",
88
+ "suggestion": "Consider simplifying",
89
+ })
90
+
91
+ if (
92
+ islands > BLOAT_THRESHOLDS["repetitive_islands"]
93
+ and verts_per_island > BLOAT_THRESHOLDS["repetitive_verts"]
94
+ ):
95
+ warnings.append({
96
+ "severity": "CRITICAL",
97
+ "object": obj.name,
98
+ "issue": "REPETITIVE_DETAIL",
99
+ "detail": f"{islands} islands x {verts_per_island:.0f} verts each",
100
+ "suggestion": "Merge islands or use instancing/texture",
101
+ })
102
+
103
+ # Scene-level check
104
+ if total_verts > BLOAT_THRESHOLDS["scene_total"]:
105
+ warnings.append({
106
+ "severity": "WARNING",
107
+ "object": "SCENE",
108
+ "issue": "HIGH_TOTAL_VERTS",
109
+ "detail": f"{total_verts:,} verts (target: <{BLOAT_THRESHOLDS['scene_total']:,})",
110
+ "suggestion": "Review all meshes for optimization opportunities",
111
+ })
112
+
113
+ # Sort by severity
114
+ severity_order = {"CRITICAL": 0, "WARNING": 1, "INFO": 2}
115
+ warnings.sort(key=lambda w: severity_order.get(str(w["severity"]), 99))
116
+
117
+ return warnings
@@ -0,0 +1,100 @@
1
+ """Bone animation analysis for detecting static bones."""
2
+
3
+ import bpy
4
+ from bpy.types import Object
5
+
6
+ from notso_glb.utils import get_scene, get_view_layer
7
+ from notso_glb.utils.logging import log_debug
8
+
9
+
10
+ def get_bones_used_for_skinning() -> set[str]:
11
+ """Find all bones that have vertex weights on skinned meshes."""
12
+ used_bones: set[str] = set()
13
+
14
+ for obj in bpy.data.objects:
15
+ if obj.type != "MESH":
16
+ continue
17
+
18
+ # Check if mesh is skinned (has armature modifier)
19
+ has_armature = any(mod.type == "ARMATURE" for mod in obj.modifiers)
20
+ if not has_armature:
21
+ continue
22
+
23
+ # All vertex groups on skinned meshes are bone references
24
+ for vg in obj.vertex_groups:
25
+ used_bones.add(vg.name)
26
+
27
+ return used_bones
28
+
29
+
30
+ def analyze_bone_animation() -> set[str]:
31
+ """Find bones that never animate across all actions.
32
+
33
+ Optimized to batch frame evaluations - evaluates all bones at once per frame
34
+ instead of switching frames per-bone, reducing scene updates from O(bones*actions)
35
+ to O(actions).
36
+ """
37
+ armature: Object | None = None
38
+ for obj in bpy.data.objects:
39
+ if obj.type == "ARMATURE":
40
+ armature = obj
41
+ break
42
+
43
+ if not armature or not armature.animation_data or not armature.pose:
44
+ log_debug("No armature with animation data found")
45
+ return set()
46
+
47
+ scene = get_scene()
48
+ view_layer = get_view_layer()
49
+ bone_movement: dict[str, float] = {b.name: 0.0 for b in armature.pose.bones}
50
+ num_bones = len(armature.pose.bones)
51
+ num_actions = len(bpy.data.actions)
52
+
53
+ log_debug(f"Analyzing {num_bones} bones across {num_actions} actions")
54
+
55
+ orig_action = armature.animation_data.action
56
+ orig_frame = scene.frame_current
57
+
58
+ for action in bpy.data.actions:
59
+ armature.animation_data.action = action
60
+ frame_start = int(action.frame_range[0])
61
+ frame_end = int(action.frame_range[1])
62
+
63
+ # Evaluate start frame ONCE for all bones
64
+ scene.frame_set(frame_start)
65
+ view_layer.update()
66
+ start_poses: dict[str, tuple] = {}
67
+ for bone in armature.pose.bones:
68
+ start_poses[bone.name] = (
69
+ bone.location.copy(),
70
+ bone.rotation_quaternion.copy(),
71
+ bone.rotation_euler.copy(),
72
+ bone.rotation_mode,
73
+ )
74
+
75
+ # Evaluate end frame ONCE for all bones
76
+ scene.frame_set(frame_end)
77
+ view_layer.update()
78
+
79
+ # Now calculate diffs without any frame switching
80
+ for bone in armature.pose.bones:
81
+ start_loc, start_rot_q, start_rot_e, rot_mode = start_poses[bone.name]
82
+ end_loc = bone.location.copy()
83
+ end_rot_q = bone.rotation_quaternion.copy()
84
+ end_rot_e = bone.rotation_euler.copy()
85
+
86
+ loc_diff = (end_loc - start_loc).length
87
+ if rot_mode == "QUATERNION":
88
+ rot_diff = (end_rot_q - start_rot_q).magnitude
89
+ else:
90
+ rot_diff = (
91
+ end_rot_e.to_quaternion() - start_rot_e.to_quaternion()
92
+ ).magnitude
93
+
94
+ bone_movement[bone.name] += loc_diff + rot_diff
95
+
96
+ if orig_action:
97
+ armature.animation_data.action = orig_action
98
+ scene.frame_set(orig_frame)
99
+
100
+ return {name for name, movement in bone_movement.items() if movement < 0.01}